// // FSAAppList.m // F-Script Anywhere // // Created by Nicholas Riley on Fri Feb 01 2002. // Copyright (c) 2002 Nicholas Riley. All rights reserved. // /* F-Script Anywhere is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. F-Script Anywhere is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with F-Script Anywhere; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #import "FSAAppList.h" #import "FSAnywhere.h" #import "libMatch.h" #import "DeVercruesseProcessManager.h" #import "NJRLabeledImageCell.h" #import "NSTableView-NJRExtensions.h" // for appIsPEF: #import #import #import NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon"; NSString * const FSATableColumnIdentifier_checkMark = @"checkMark"; NSString *FSACheckMarkCharacter; NSImage *FSACheckMarkImage; NSString *FSAEllipsisCharacter; NSImage *FSAEllipsisImage; static const char *FSACocoaFrameworks[] = { "/System/Library/Frameworks/AppKit.framework", "/System/Library/Frameworks/Foundation.framework", "/System/Library/Frameworks/Cocoa.framework", NULL }; @implementation FSAAppList + (void)initialize; { FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1]; FSACheckMarkImage = [NSImage imageNamed: @"NSMenuCheckmark"]; if (FSACheckMarkImage == nil) { FSACheckMarkImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Fallback checkmark" ofType: @"tiff"]]; if (FSACheckMarkImage != nil && ![FSACheckMarkImage isValid]) { [FSACheckMarkImage release]; FSACheckMarkImage = nil; } FSALog(@"Falling back to checkmark image from bundle: %@", FSACheckMarkImage); } FSAEllipsisCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x20\x26" length: 1]; FSAEllipsisImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Ellipsis" ofType: @"tiff"]]; if (FSAEllipsisImage != nil && ![FSAEllipsisImage isValid]) { [FSAEllipsisImage release]; FSAEllipsisImage = nil; } } - (void)awakeFromNib; { NSWindow *window = [tableView window]; processManager = [DeVercruesseProcessManager defaultManager]; cocoaApps = [[NSMutableArray alloc] init]; patchedApps = [[NSMutableSet alloc] init]; patchingApps = [[NSMutableSet alloc] init]; appsByPID = [[NSMutableDictionary alloc] init]; [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon] setDataCell: [NJRLabeledImageCell cell]]; if (FSACheckMarkImage != nil) [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_checkMark] setDataCell: [[[NSImageCell alloc] init] autorelease]]; [window setResizeIncrements: NSMakeSize(1, [tableView cellHeight])]; [self update]; [window makeFirstResponder: tableView]; } - (void)dealloc; { // don't release processManager, we don't own it [cocoaApps release]; [patchedApps release]; [appsByPID release]; [super dealloc]; } - (pid_t)selectedProcessID; { int row = [tableView selectedRow]; if (row == -1) return -1; return [[cocoaApps objectAtIndex: row] pid]; } - (void)_processStatusChanged; { [tableView reloadData]; [self tableView: tableView shouldSelectRow: [tableView selectedRow]]; } - (DeVercruesseProcess *)_applicationForPID:(pid_t)pid; { return [appsByPID objectForKey: [NSNumber numberWithInt: pid]]; } - (void)didPatchProcessID:(pid_t)pid; { DeVercruesseProcess *app = [self _applicationForPID: pid]; [patchingApps removeObject: app]; [patchedApps addObject: app]; [self _processStatusChanged]; } - (void)isPatchingProcessID:(pid_t)pid; { [patchingApps addObject: [self _applicationForPID: pid]]; [self _processStatusChanged]; } - (BOOL)appIsPEF:(DeVercruesseProcess *)app; { NSString *bundleExecutableLoc = [app executableLoc]; const char *bundleExecutablePath; int fd; PEFContainerHeader pefHeader; if (bundleExecutableLoc == NULL) return NO; if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL) return NO; if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1) return NO; if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader)) return NO; if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2) return NO; return YES; } - (BOOL)appIsCocoa:(DeVercruesseProcess *)app; { NSString *bundleExecutableLoc = [app executableLoc]; if (bundleExecutableLoc == NULL) return NO; return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks); } - (void)addApp:(DeVercruesseProcess *)app; { /* Try to determine if the application is a foreground Cocoa application. In Jaguar, itŐs possible to mix Cocoa in a primarily Carbon application, but we don't support such hybrids because the menu items we add depend on Cocoa dispatch mechanisms. The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X 10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps are Carbon apps. So we use some code extracted from otool to check whether the application links to the Foundation, AppKit or Cocoa frameworks. This problem is fixed in Jaguar, except that certain CFM Carbon apps are reported to be Cocoa apps (Drop Drawers is one example). Conversely, the appIsCocoa: code works on _most_ applications, but Jaguar always correctly identifies Cocoa apps as isCocoa. So, our checks go like this: Is the application background-only? Is the application Cocoa or Carbon according to the CPS flavor? If it's Cocoa, is it a CFM app? If so, CPS is lying to us. If it's "Carbon", does it link to AppKit, Foundation or Cocoa? If so, it's really a Cocoa app. If not, it's a Carbon app. Be careful not to call appIsCocoa: on a Classic application, you will crash. */ /* if ([app isCocoa] || [app isCarbon]) { NSLog(@"%@ |%@%@%@%@%@", [app name], [app isBackgroundOnly] ? @" bgOnly" : @"", [app isCocoa] ? @" isCocoa" : @"", [app isCarbon] ? @" isCarbon" : @"", [self appIsPEF: app] ? @" appIsPEF" : @"", [self appIsCocoa: app] ? @" appIsCocoa" : @""); } */ if ( ![app isBackgroundOnly] && ( ( [app isCocoa] && ![self appIsPEF: app]) || ( [app isCarbon] && [self appIsCocoa: app]))) { [cocoaApps addObject: app]; } [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]]; } // XXX should insert/resort on launch too; this is harder because of synchronization issues - (void)update; { NSEnumerator *e; NSArray *allApps; DeVercruesseProcess *app; [cocoaApps removeAllObjects]; [appsByPID removeAllObjects]; // [processManager update] unneeded: [processManager processes] sends update allApps = [processManager processes]; e = [allApps objectEnumerator]; while ( (app = [e nextObject]) != nil) { [self addApp: app]; } [tableView noteNumberOfRowsChanged]; [self _processStatusChanged]; } - (NSArray *)cocoaAppProcessIDs; { NSEnumerator *e = [cocoaApps objectEnumerator]; NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]]; DeVercruesseProcess *app; while ( (app = [e nextObject]) != nil) { [pids addObject: [NSNumber numberWithInt: [app pid]]]; } return pids; } - (void)applicationLaunchedWithProcessID:(pid_t)pid; { if ([self _applicationForPID: pid] == nil) { [self update]; } } - (void)applicationQuitWithProcessID:(pid_t)pid; { DeVercruesseProcess *app = [self _applicationForPID: pid]; if (app != nil) { [cocoaApps removeObject: app]; [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]]; [patchedApps removeObject: app]; } [tableView noteNumberOfRowsChanged]; [self _processStatusChanged]; } @end @implementation FSAAppList (NSTableViewDelegate) - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row; { if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) { DeVercruesseProcess *app = [cocoaApps objectAtIndex: row]; NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell); [(NJRLabeledImageCell *)cell setImage: [app img]]; [(NJRLabeledImageCell *)cell setImageCacheSource: app]; } } - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row; { BOOL canInstall = NO; if (row != -1) { DeVercruesseProcess *app = [cocoaApps objectAtIndex: row]; canInstall = !([patchedApps containsObject: app] || [patchingApps containsObject: app]); } [installButton setEnabled: canInstall]; return YES; } @end @implementation FSAAppList (NSTableDataSource) - (int)numberOfRowsInTableView:(NSTableView *)aTableView { return [cocoaApps count]; } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex { NSString *columnIdentifier = [aTableColumn identifier]; if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) { return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name]; } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) { DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex]; if ([patchedApps containsObject: app]) { if (FSACheckMarkImage == nil) return FSACheckMarkCharacter; else return FSACheckMarkImage; } if ([patchingApps containsObject: app]) { if (FSAEllipsisImage == nil) return FSAEllipsisCharacter; else return FSAEllipsisImage; } } return nil; } @end @implementation FSAAppList (NSWindowDelegate) - (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame; { NSWindow *window = [tableView window]; NSRect frame = [window frame]; NSScrollView *scrollView = [tableView enclosingScrollView]; float displayedHeight = [[scrollView contentView] bounds].size.height; float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight; float heightExcess; if (heightChange >= 0 && heightChange <= 1) { // either the window is already optimal size, or it's too big float rowHeight = [tableView cellHeight]; heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight; } frame.size.height += heightChange; if ( (heightExcess = [window minSize].height - frame.size.height) > 1 || (heightExcess = [window maxSize].height - frame.size.height) < 1) { heightChange += heightExcess; frame.size.height += heightExcess; } frame.origin.y -= heightChange; return frame; } @end @implementation FSAAppList (NSApplicationDelegate) - (NSMenu *)applicationDockMenu:(NSApplication *)sender; { static NSMenu *dockMenu = nil; id menuItem; DeVercruesseProcess *frontApp = [processManager frontProcess]; NSString *appName = [frontApp name]; NSString *status = nil; // XXX workaround for broken dock menu sender NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)]; NSInvocation *inv; if (dockMenu != nil) { // XXX release invocation [[[dockMenu itemAtIndex: 0] target] release]; [dockMenu removeItemAtIndex: 0]; } else { dockMenu = [[NSMenu alloc] init]; } NSAssert(frontApp != nil && appName != nil, @"Can't obtain information on the frontmost application"); if ([patchedApps containsObject: frontApp]) { status = [NSString stringWithFormat: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName]; } else if (![cocoaApps containsObject: frontApp]) { 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]; } if (status == nil) { menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), appName] action: @selector(invoke) keyEquivalent: @""]; inv = [NSInvocation invocationWithMethodSignature: sig]; [inv setSelector: @selector(installBundleInFrontmostApp:)]; [inv setTarget: NSApp]; [inv setArgument: &menuItem atIndex: 2]; [menuItem setTag: [frontApp pid]]; [menuItem setTarget: [inv retain]]; } else { menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""]; [menuItem setEnabled: NO]; } return dockMenu; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender; { return YES; } @end