source: trunk/Cocoa/F-Script Anywhere/Source/FSAAppList.m@ 397

Last change on this file since 397 was 397, checked in by Nicholas Riley, 16 years ago

Replace id<NSMenuItem> with NSMenuItem * to remove warnings.

Minor code style cleanup.

File size: 17.4 KB
Line 
1//
2// FSAAppList.m
3// F-Script Anywhere
4//
5// Created by Nicholas Riley on Fri Feb 01 2002.
6// Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9/*
10
11 F-Script Anywhere is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 2 of the License, or
14 (at your option) any later version.
15
16 F-Script Anywhere is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with F-Script Anywhere; if not, write to the Free Software
23 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24
25 */
26
27#include <sys/types.h>
28#include <sys/sysctl.h>
29#import "FSAAppList.h"
30#import "FSAnywhere.h"
31#import "libMatch.h"
32#import "DeVercruesseProcessManager.h"
33#import "NJRLabeledImageCell.h"
34#import "NSTableView-NJRExtensions.h"
35
36// for appIsPEF:
37#import <Carbon/Carbon.h>
38#import <fcntl.h>
39#import <unistd.h>
40
41NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon";
42NSString * const FSATableColumnIdentifier_checkMark = @"checkMark";
43NSString * const FSATableColumnIdentifier_always = @"always";
44
45NSString *FSACheckMarkCharacter;
46NSImage *FSACheckMarkImage;
47
48NSString *FSAEllipsisCharacter;
49NSImage *FSAEllipsisImage;
50
51static const char *FSACocoaFrameworks[] = {
52 "/System/Library/Frameworks/AppKit.framework",
53 "/System/Library/Frameworks/Foundation.framework",
54 "/System/Library/Frameworks/Cocoa.framework",
55 NULL
56};
57
58static int sysctlbyname_with_pid (const char *name, pid_t pid,
59 void *oldp, size_t *oldlenp,
60 void *newp, size_t newlen);
61int is_pid_native (pid_t pid);
62
63@implementation FSAAppList
64
65+ (void)initialize;
66{
67 FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1];
68 FSACheckMarkImage = [NSImage imageNamed: @"NSMenuCheckmark"];
69 if (FSACheckMarkImage == nil) {
70 FSACheckMarkImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Fallback checkmark" ofType: @"tiff"]];
71 if (FSACheckMarkImage != nil && ![FSACheckMarkImage isValid]) {
72 [FSACheckMarkImage release];
73 FSACheckMarkImage = nil;
74 }
75 FSALog(@"Falling back to checkmark image from bundle: %@", FSACheckMarkImage);
76 }
77 FSAEllipsisCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x20\x26" length: 1];
78 FSAEllipsisImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Ellipsis" ofType: @"tiff"]];
79 if (FSAEllipsisImage != nil && ![FSAEllipsisImage isValid]) {
80 [FSAEllipsisImage release];
81 FSAEllipsisImage = nil;
82 }
83}
84
85- (void)awakeFromNib;
86{
87 NSWindow *window = [tableView window];
88
89 processManager = [DeVercruesseProcessManager defaultManager];
90 cocoaApps = [[NSMutableArray alloc] init];
91 patchedApps = [[NSMutableSet alloc] init];
92 patchingApps = [[NSMutableSet alloc] init];
93 appsByPID = [[NSMutableDictionary alloc] init];
94 alwaysApps = [[NSMutableArray array] retain];
95
96 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon]
97 setDataCell: [NJRLabeledImageCell cell]];
98 if (FSACheckMarkImage != nil)
99 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_checkMark]
100 setDataCell: [[[NSImageCell alloc] init] autorelease]];
101 [window setResizeIncrements: NSMakeSize(1, [tableView cellHeight])];
102 [tableView setTarget:self];
103 [tableView setAction:@selector(clickInTable:)];
104
105 [self update];
106 [window makeFirstResponder: tableView];
107
108 [[NSNotificationCenter defaultCenter] addObserver:self
109 selector:@selector(update)
110 name:NSUserDefaultsDidChangeNotification
111 object:nil];
112}
113
114- (void)dealloc;
115{
116 // don't release processManager, we don't own it
117 [cocoaApps release];
118 [patchedApps release];
119 [appsByPID release];
120 [super dealloc];
121}
122
123- (pid_t)selectedProcessID;
124{
125 int row = [tableView selectedRow];
126 if (row == -1) return -1;
127
128 return [[cocoaApps objectAtIndex: row] pid];
129}
130
131- (void)_processStatusChanged;
132{
133 [tableView reloadData];
134// [self tableView: tableView shouldSelectRow: [tableView selectedRow]];
135}
136
137- (DeVercruesseProcess *)_applicationForPID:(pid_t)pid;
138{
139 return [appsByPID objectForKey: [NSNumber numberWithInt: pid]];
140}
141
142- (void)didPatchProcessID:(pid_t)pid;
143{
144 DeVercruesseProcess *app = [self _applicationForPID: pid];
145 if(app){
146 [patchingApps removeObject: app];
147 [patchedApps addObject: app];
148 }
149 [self _processStatusChanged];
150}
151
152- (void)isPatchingProcessID:(pid_t)pid;
153{
154 [patchingApps addObject: [self _applicationForPID: pid]];
155 [self _processStatusChanged];
156}
157
158- (BOOL)appIsPEF:(DeVercruesseProcess *)app;
159{
160 NSString *bundleExecutableLoc = [app executableLoc];
161 const char *bundleExecutablePath;
162 int fd;
163 PEFContainerHeader pefHeader;
164
165 if (bundleExecutableLoc == NULL)
166 return NO;
167
168 if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL)
169 return NO;
170
171 if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1)
172 return NO;
173
174 if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader)){
175 close(fd);
176 return NO;
177 } else
178 close(fd);
179
180 if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2)
181 return NO;
182
183 return YES;
184}
185
186- (BOOL)appIsCocoa:(DeVercruesseProcess *)app;
187{
188 NSString *bundleExecutableLoc = [app executableLoc];
189 if (bundleExecutableLoc == NULL)
190 return NO;
191 return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks);
192}
193
194-(BOOL)appIsNative:(DeVercruesseProcess *)app
195{
196 return is_pid_native([app pid]);
197}
198
199- (void)addApp:(DeVercruesseProcess *)app;
200{
201 /* Try to determine if the application is a foreground Cocoa application.
202 In Jaguar, itÕs possible to mix Cocoa in a primarily Carbon application,
203 but we don't support such hybrids because the menu items we add depend
204 on Cocoa dispatch mechanisms.
205
206 The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X
207 10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps
208 are Carbon apps. So we use some code extracted from otool to check
209 whether the application links to the Foundation, AppKit or Cocoa
210 frameworks. This problem is fixed in Jaguar, except that certain CFM
211 Carbon apps are reported to be Cocoa apps (Drop Drawers is one example).
212 Conversely, the appIsCocoa: code works on _most_ applications, but
213 Jaguar always correctly identifies Cocoa apps as isCocoa.
214
215 So, our checks go like this:
216 Is the application background-only?
217 Is the application Cocoa or Carbon according to the CPS flavor?
218 If it's Cocoa, is it a CFM app? If so, CPS is lying to us.
219 If it's "Carbon", does it link to AppKit, Foundation or Cocoa?
220 If so, it's really a Cocoa app. If not, it's a Carbon app.
221
222 Be careful not to call appIsCocoa: on a Classic application, you will
223 crash.
224 */
225
226 /*
227 if ([app isCocoa] || [app isCarbon]) {
228 NSLog(@"%@ |%@%@%@%@%@", [app name],
229 [app isBackgroundOnly] ? @" bgOnly" : @"",
230 [app isCocoa] ? @" isCocoa" : @"",
231 [app isCarbon] ? @" isCarbon" : @"",
232 [self appIsPEF: app] ? @" appIsPEF" : @"",
233 [self appIsCocoa: app] ? @" appIsCocoa" : @"");
234 }
235 */
236 if ( ![app isBackgroundOnly] &&
237 ( ( [app isCocoa] && ![self appIsPEF: app]) ||
238 ( [app isCarbon] && [self appIsCocoa: app]))) {
239 if([self appIsNative:app]){
240 [cocoaApps addObject: app];
241 if(finishedLaunch){
242 if([alwaysApps containsObject:[app name]] && ![patchedApps containsObject:app])
243 [NSApp installBundleInAppWithPID:[app pid]];
244 }
245 }
246 }
247 [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]];
248}
249
250// XXX should insert/resort on launch too; this is harder because of synchronization issues
251
252- (void)update;
253{
254 NSEnumerator *e;
255 NSArray *allApps;
256 DeVercruesseProcess *app;
257
258 [cocoaApps removeAllObjects];
259 [appsByPID removeAllObjects];
260 // [processManager update] unneeded: [processManager processes] sends update
261
262 allApps = [processManager processes];
263 e = [allApps objectEnumerator];
264
265 while ( (app = [e nextObject]) != nil) {
266 [self addApp: app];
267 }
268
269 if([[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]){
270 [[NSUserDefaults standardUserDefaults] synchronize];
271 [alwaysApps removeAllObjects];
272 [alwaysApps addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]];
273 [tableView reloadData];
274 }
275 [tableView noteNumberOfRowsChanged];
276 [self _processStatusChanged];
277}
278
279- (NSArray *)cocoaAppProcessIDs;
280{
281 NSEnumerator *e = [cocoaApps objectEnumerator];
282 NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]];
283 DeVercruesseProcess *app;
284 while ( (app = [e nextObject]) != nil) {
285 [pids addObject: [NSNumber numberWithInt: [app pid]]];
286 }
287 return pids;
288}
289
290- (void)applicationLaunchedWithProcessID:(pid_t)pid;
291{
292 if ([self _applicationForPID: pid] == nil) {
293 [self update];
294 }
295}
296
297- (void)applicationQuitWithProcessID:(pid_t)pid;
298{
299 DeVercruesseProcess *app = [self _applicationForPID: pid];
300
301 if (app != nil) {
302 [cocoaApps removeObject: app];
303 [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]];
304 [patchedApps removeObject: app];
305 }
306
307 [tableView noteNumberOfRowsChanged];
308 [self _processStatusChanged];
309}
310
311@end
312
313@implementation FSAAppList (NSTableViewDelegate)
314
315- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row;
316{
317 if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
318 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
319
320 NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell);
321 [(NJRLabeledImageCell *)cell setImage: [app img]];
322 }
323}
324
325- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
326{
327 BOOL canInstall = NO;
328
329 if (row != -1) {
330 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
331 canInstall = !([patchedApps containsObject: app] ||
332 [patchingApps containsObject: app]);
333 }
334
335 [installButton setEnabled: canInstall];
336
337 return YES;
338}
339
340@end
341
342@implementation FSAAppList (NSTableDataSource)
343
344- (int)numberOfRowsInTableView:(NSTableView *)aTableView
345{
346 return [cocoaApps count];
347}
348
349- (id)tableView:(NSTableView *)aTableView
350 objectValueForTableColumn:(NSTableColumn *)aTableColumn
351 row:(int)rowIndex
352{
353 NSString *columnIdentifier = [aTableColumn identifier];
354
355 if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
356 return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name];
357 } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) {
358 DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex];
359 if ([patchedApps containsObject: app]) {
360 if (FSACheckMarkImage == nil)
361 return FSACheckMarkCharacter;
362 else
363 return FSACheckMarkImage;
364 }
365 if ([patchingApps containsObject: app]) {
366 if (FSAEllipsisImage == nil)
367 return FSAEllipsisCharacter;
368 else
369 return FSAEllipsisImage;
370 }
371 } else if([columnIdentifier isEqualToString:FSATableColumnIdentifier_always]){
372 if([alwaysApps containsObject:[[cocoaApps objectAtIndex: rowIndex] name]])
373 return [NSNumber numberWithBool:YES];
374 else
375 return [NSNumber numberWithBool:NO];
376 }
377 return nil;
378}
379
380@end
381
382@implementation FSAAppList (NSWindowDelegate)
383
384- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
385{
386 NSWindow *window = [tableView window];
387 NSRect frame = [window frame];
388 NSScrollView *scrollView = [tableView enclosingScrollView];
389 float displayedHeight = [[scrollView contentView] bounds].size.height;
390 float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
391 float heightExcess;
392
393 if (heightChange >= 0 && heightChange <= 1) {
394 // either the window is already optimal size, or it's too big
395 float rowHeight = [tableView cellHeight];
396 heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight;
397 }
398
399 frame.size.height += heightChange;
400
401 if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
402 (heightExcess = [window maxSize].height - frame.size.height) < 1) {
403 heightChange += heightExcess;
404 frame.size.height += heightExcess;
405 }
406
407 frame.origin.y -= heightChange;
408
409 return frame;
410}
411
412@end
413
414@implementation FSAAppList (NSApplicationDelegate)
415
416- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
417{
418 static NSMenu *dockMenu = nil;
419 NSMenuItem *menuItem;
420 DeVercruesseProcess *frontApp = [processManager frontProcess];
421 NSString *appName = [frontApp name];
422 NSString *status = nil;
423 // XXX workaround for broken dock menu sender
424 NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)];
425 NSInvocation *inv;
426
427 if (dockMenu != nil) {
428 // XXX release invocation
429 [[[dockMenu itemAtIndex: 0] target] release];
430 [dockMenu removeItemAtIndex: 0];
431 } else {
432 dockMenu = [[NSMenu alloc] init];
433 }
434
435 NSAssert(frontApp != nil && appName != nil, @"Can't obtain information on the frontmost application");
436
437 if ([patchedApps containsObject: frontApp]) {
438 status = [NSString stringWithFormat: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName];
439 } else if (![cocoaApps containsObject: frontApp]) {
440 status = [NSString stringWithFormat: NSLocalizedString(@"Can't install because '%@' is not a Cocoa application", "Dock menu disabled item displayed when frontmost app not Cocoa, app name parameter"), appName];
441 }
442
443 if (status == nil) {
444 menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), appName]
445 action: @selector(invoke)
446 keyEquivalent: @""];
447 inv = [NSInvocation invocationWithMethodSignature: sig];
448 [inv setSelector: @selector(installBundleInFrontmostApp:)];
449 [inv setTarget: NSApp];
450 [inv setArgument: &menuItem atIndex: 2];
451 [menuItem setTag: [frontApp pid]];
452 [menuItem setTarget: [inv retain]];
453 } else {
454 menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""];
455 [menuItem setEnabled: NO];
456 }
457
458 return dockMenu;
459}
460
461- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
462{
463 id anApp;
464 id e = [cocoaApps objectEnumerator];
465 while(anApp = [e nextObject]){
466 if([alwaysApps containsObject:[anApp name]]){
467 [NSApp installBundleInAppWithPID:[anApp pid]];
468 }
469 }
470 finishedLaunch = YES;
471}
472
473- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender;
474{
475 return YES;
476}
477
478-(void)clickInTable:(id)sender
479{
480 if([sender clickedColumn] == 2){
481 if([sender clickedRow] >= 0){
482 NSString *appName = [[cocoaApps objectAtIndex:[sender clickedRow]] name];
483 if(![alwaysApps containsObject:appName]){
484 [NSApp installBundleInAppWithPID:[[cocoaApps objectAtIndex:[sender clickedRow]] pid]];
485 [alwaysApps addObject:appName];
486 } else
487 [alwaysApps removeObject:appName];
488 [[NSUserDefaults standardUserDefaults] setObject:alwaysApps forKey:@"AlwaysApps"];
489 }
490 }
491}
492
493@end
494
495static int sysctlbyname_with_pid (const char *name, pid_t pid,
496 void *oldp, size_t *oldlenp,
497 void *newp, size_t newlen)
498{
499 if (pid == 0) {
500 if (sysctlbyname(name, oldp, oldlenp, newp, newlen) == -1) {
501 fprintf(stderr, "sysctlbyname_with_pid(0): sysctlbyname failed:"
502 "%s\n", strerror(errno));
503 return -1;
504 }
505 } else {
506 int mib[CTL_MAXNAME];
507 size_t len = CTL_MAXNAME;
508 if (sysctlnametomib(name, mib, &len) == -1) {
509 fprintf(stderr, "sysctlbyname_with_pid: sysctlnametomib failed:"
510 "%s\n", strerror(errno));
511 return -1;
512 }
513 mib[len] = pid;
514 len++;
515 if (sysctl(mib, len, oldp, oldlenp, newp, newlen) == -1) {
516 fprintf(stderr, "sysctlbyname_with_pid: sysctl failed:"
517 "%s\n", strerror(errno));
518 return -1;
519 }
520 }
521 return 0;
522}
523
524int is_pid_native (pid_t pid)
525{
526 int ret = 0;
527 size_t sz = sizeof(ret);
528
529 if (sysctlbyname_with_pid("sysctl.proc_native", pid,
530 &ret, &sz, NULL, 0) == -1) {
531 if (errno == ENOENT) {
532 // sysctl doesn't exist, which means that this version of Mac OS
533 // pre-dates Rosetta, so the application must be native.
534 return 1;
535 }
536 fprintf(stderr, "is_pid_native: sysctlbyname_with_pid failed:"
537 "%s\n", strerror(errno));
538 return -1;
539 }
540 return ret;
541}
Note: See TracBrowser for help on using the repository browser.