// // 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 "FSAApp.h" #import "libMatch.h" #import "DeVercruesseProcessManager.h" #import "NJRLabeledImageCell.h" #import "NSTableView-NJRExtensions.h" NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon"; NSString * const FSATableColumnIdentifier_checkMark = @"checkMark"; NSString *FSACheckMarkCharacter; static const char *FSACocoaFrameworks[] = { "/System/Library/Frameworks/AppKit.framework", "/System/Library/Frameworks/Foundation.framework", "/System/Library/Frameworks/Cocoa.framework", NULL }; @implementation FSAAppList + (void)load; { FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1]; } - (void)awakeFromNib; { processManager = [DeVercruesseProcessManager defaultManager]; cocoaApps = [[NSMutableArray alloc] init]; patchedApps = [[NSMutableSet alloc] init]; appsByPID = [[NSMutableDictionary alloc] init]; [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon] setDataCell: [NJRLabeledImageCell cell]]; [[tableView window] setResizeIncrements: NSMakeSize(1, [tableView cellHeight])]; [self update]; [[tableView 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)didPatchProcessID:(pid_t)pid; { [patchedApps addObject: [appsByPID objectForKey: [NSNumber numberWithInt: pid]]]; [tableView reloadData]; [installButton setEnabled: NO]; } - (BOOL)appIsCocoa:(DeVercruesseProcess *)app; { NSString *bundleExecutableLoc = [app executableLoc]; if (bundleExecutableLoc == NULL) return NO; return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks); } - (void)addApp:(DeVercruesseProcess *)app; { if ( ([app isCarbon] || [app isCocoa]) // XXX OS X 10.1.2 (and earlier?) bug, Cocoa reported as Carbon && ![app isBackgroundOnly] && [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]; // [processManager processes] sends update allApps = [processManager processes]; e = [allApps objectEnumerator]; while ( (app = [e nextObject]) != nil) { [self addApp: app]; } [tableView noteNumberOfRowsChanged]; [tableView reloadData]; // XXX this is broken, it doesn't update properly [self tableView: tableView shouldSelectRow: [tableView selectedRow]]; } - (void)applicationLaunchedWithProcessID:(pid_t)pid; { NSNumber *pidNum = [NSNumber numberWithInt: pid]; DeVercruesseProcess *app = [appsByPID objectForKey: pidNum]; if (app == nil) { [self update]; } } - (void)applicationQuitWithProcessID:(pid_t)pid; { NSNumber *pidNum = [NSNumber numberWithInt: pid]; DeVercruesseProcess *app = [appsByPID objectForKey: pidNum]; if (app != nil) { [cocoaApps removeObject: app]; [appsByPID removeObjectForKey: pidNum]; [patchedApps removeObject: app]; } [tableView reloadData]; } @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) { canInstall = ![patchedApps containsObject: [cocoaApps objectAtIndex: row]]; } [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]) { return [patchedApps containsObject: [cocoaApps objectAtIndex: rowIndex]] ? FSACheckMarkCharacter : @""; } 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; } - (BOOL)windowShouldClose:(id)sender; { if ([patchedApps count] != 0) { NSMutableString *message = [@"F-Script Anywhere is installed in the following applications:\n\n" mutableCopy]; NSEnumerator *e = [patchedApps objectEnumerator]; DeVercruesseProcess *app; int retval; while ( (app = [e nextObject]) != nil) { [message appendFormat: @" ¥ %@\n", [app name]]; } [message appendString: @"\nIf F-Script Anywhere quits now, these applications will be forced to quit, and any changes you have made in them will be lost.\n\nPlease quit these applications before quitting F-Script Anywhere, or click Force Quit to continue."]; retval = NSRunAlertPanel(@"Force applications to quit?", message, @"DonÕt Quit", @"Force Quit", nil); if (retval != NSAlertAlternateReturn) { return NO; } [(FSAApp *)NSApp unloadBundles: self]; } return YES; } @end @implementation FSAAppList (NSApplicationDelegate) - (NSMenu *)applicationDockMenu:(NSApplication *)sender; { static NSMenu *dockMenu = nil; NSMenuItem *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: @"Installed in Ò%@Ó", appName]; } else if (![cocoaApps containsObject: frontApp]) { status = [NSString stringWithFormat: @"CanÕt install because Ò%@Ó is not a Cocoa application", appName]; } if (status == nil) { menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: @"Install in Ò%@Ó", 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; } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; { return ([self windowShouldClose: self] ? NSTerminateNow : NSTerminateCancel); } @end