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

Last change on this file since 153 was 153, checked in by Nicholas Riley, 17 years ago

Integrates SCPatch and mach_inject; unfinished, buggy.

File size: 13.8 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#import "FSAAppList.h"
28#import "FSAnywhere.h"
29#import "libMatch.h"
30#import "DeVercruesseProcessManager.h"
31#import "NJRLabeledImageCell.h"
32#import "NSTableView-NJRExtensions.h"
33
34// for appIsPEF:
35#import <Carbon/Carbon.h>
36#import <fcntl.h>
37#import <unistd.h>
38
39NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon";
40NSString * const FSATableColumnIdentifier_checkMark = @"checkMark";
41
42NSString *FSACheckMarkCharacter;
43NSImage *FSACheckMarkImage;
44
45NSString *FSAEllipsisCharacter;
46NSImage *FSAEllipsisImage;
47
48static const char *FSACocoaFrameworks[] = {
49    "/System/Library/Frameworks/AppKit.framework",
50    "/System/Library/Frameworks/Foundation.framework",
51    "/System/Library/Frameworks/Cocoa.framework",
52    NULL
53};
54
55@implementation FSAAppList
56
57+ (void)initialize;
58{
59    FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1];
60    FSACheckMarkImage = [NSImage imageNamed: @"NSMenuCheckmark"];
61    if (FSACheckMarkImage == nil) {
62        FSACheckMarkImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Fallback checkmark" ofType: @"tiff"]];
63        if (FSACheckMarkImage != nil && ![FSACheckMarkImage isValid]) {
64            [FSACheckMarkImage release];
65            FSACheckMarkImage = nil;
66        }
67        FSALog(@"Falling back to checkmark image from bundle: %@", FSACheckMarkImage);
68    }
69    FSAEllipsisCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x20\x26" length: 1];
70    FSAEllipsisImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Ellipsis" ofType: @"tiff"]];
71    if (FSAEllipsisImage != nil && ![FSAEllipsisImage isValid]) {
72        [FSAEllipsisImage release];
73        FSAEllipsisImage = nil;
74    }
75}
76
77- (void)awakeFromNib;
78{
79    NSWindow *window = [tableView window];
80   
81    processManager = [DeVercruesseProcessManager defaultManager];
82    cocoaApps = [[NSMutableArray alloc] init];
83    patchedApps = [[NSMutableSet alloc] init];
84    patchingApps = [[NSMutableSet alloc] init];
85    appsByPID = [[NSMutableDictionary alloc] init];
86
87    [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon]
88        setDataCell: [NJRLabeledImageCell cell]];
89    if (FSACheckMarkImage != nil)
90        [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_checkMark]
91            setDataCell: [[[NSImageCell alloc] init] autorelease]];
92    [window setResizeIncrements: NSMakeSize(1, [tableView cellHeight])];
93
94    [self update];
95    [window makeFirstResponder: tableView];
96}
97
98- (void)dealloc;
99{
100    // don't release processManager, we don't own it
101    [cocoaApps release];
102    [patchedApps release];
103    [appsByPID release];
104    [super dealloc];
105}
106
107- (pid_t)selectedProcessID;
108{
109    int row = [tableView selectedRow];
110    if (row == -1) return -1;
111
112    return [[cocoaApps objectAtIndex: row] pid];
113}
114
115- (void)_processStatusChanged;
116{
117    [tableView reloadData];
118    [self tableView: tableView shouldSelectRow: [tableView selectedRow]];
119}
120
121- (DeVercruesseProcess *)_applicationForPID:(pid_t)pid;
122{
123    return [appsByPID objectForKey: [NSNumber numberWithInt: pid]];
124}
125
126- (void)didPatchProcessID:(pid_t)pid;
127{
128    DeVercruesseProcess *app = [self _applicationForPID: pid];
129    [patchingApps removeObject: app];
130    [patchedApps addObject: app];
131    [self _processStatusChanged];
132}
133
134- (void)isPatchingProcessID:(pid_t)pid;
135{
136    [patchingApps addObject: [self _applicationForPID: pid]];
137    [self _processStatusChanged];
138}
139
140- (BOOL)appIsPEF:(DeVercruesseProcess *)app;
141{
142    NSString *bundleExecutableLoc = [app executableLoc];
143    const char *bundleExecutablePath;
144    int fd;
145    PEFContainerHeader pefHeader;
146
147    if (bundleExecutableLoc == NULL)
148        return NO;
149   
150    if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL)
151        return NO;
152
153    if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1)
154        return NO;
155
156    if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader))
157        return NO;
158   
159    if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2)
160        return NO;
161
162    return YES;
163}
164
165- (BOOL)appIsCocoa:(DeVercruesseProcess *)app;
166{
167    NSString *bundleExecutableLoc = [app executableLoc];
168    if (bundleExecutableLoc == NULL)
169        return NO;
170    return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks);
171}
172
173- (void)addApp:(DeVercruesseProcess *)app;
174{
175    /* Try to determine if the application is a foreground Cocoa application.
176       In Jaguar, itÕs possible to mix Cocoa in a primarily Carbon application,
177       but we don't support such hybrids because the menu items we add depend
178       on Cocoa dispatch mechanisms.
179   
180       The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X
181       10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps
182       are Carbon apps.  So we use some code extracted from otool to check
183       whether the application links to the Foundation, AppKit or Cocoa
184       frameworks.  This problem is fixed in Jaguar, except that certain CFM
185       Carbon apps are reported to be Cocoa apps (Drop Drawers is one example).
186       Conversely, the appIsCocoa: code works on _most_ applications, but
187       Jaguar always correctly identifies Cocoa apps as isCocoa.
188
189       So, our checks go like this:
190         Is the application background-only?
191         Is the application Cocoa or Carbon according to the CPS flavor?
192           If it's Cocoa, is it a CFM app?  If so, CPS is lying to us.
193           If it's "Carbon", does it link to AppKit, Foundation or Cocoa?
194             If so, it's really a Cocoa app.  If not, it's a Carbon app.
195   
196       Be careful not to call appIsCocoa: on a Classic application, you will
197       crash.
198    */
199
200    /*
201    if ([app isCocoa] || [app isCarbon]) {
202        NSLog(@"%@ |%@%@%@%@%@", [app name],
203              [app isBackgroundOnly] ? @" bgOnly" : @"",
204              [app isCocoa] ? @" isCocoa" : @"",
205              [app isCarbon] ? @" isCarbon" : @"",
206              [self appIsPEF: app] ? @" appIsPEF" : @"",
207              [self appIsCocoa: app] ? @" appIsCocoa" : @"");
208    }
209    */
210         
211    if ( ![app isBackgroundOnly] &&
212         ( ( [app isCocoa] && ![self appIsPEF: app]) ||
213           ( [app isCarbon] && [self appIsCocoa: app]))) {
214        [cocoaApps addObject: app];
215    }
216    [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]];
217}
218
219// XXX should insert/resort on launch too; this is harder because of synchronization issues
220
221- (void)update;
222{
223    NSEnumerator *e;
224    NSArray *allApps;
225    DeVercruesseProcess *app;
226
227    [cocoaApps removeAllObjects];
228    [appsByPID removeAllObjects];
229    // [processManager update] unneeded: [processManager processes] sends update
230
231    allApps = [processManager processes];
232    e = [allApps objectEnumerator];
233
234    while ( (app = [e nextObject]) != nil) {
235        [self addApp: app];
236    }
237
238    [tableView noteNumberOfRowsChanged];
239    [self _processStatusChanged];
240}
241
242- (NSArray *)cocoaAppProcessIDs;
243{
244    NSEnumerator *e = [cocoaApps objectEnumerator];
245    NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]];
246    DeVercruesseProcess *app;
247    while ( (app = [e nextObject]) != nil) {
248        [pids addObject: [NSNumber numberWithInt: [app pid]]];
249    }
250    return pids;
251}
252
253- (void)applicationLaunchedWithProcessID:(pid_t)pid;
254{
255    if ([self _applicationForPID: pid] == nil) {
256        [self update];
257    }
258}
259
260- (void)applicationQuitWithProcessID:(pid_t)pid;
261{
262    DeVercruesseProcess *app = [self _applicationForPID: pid];
263
264    if (app != nil) {
265        [cocoaApps removeObject: app];
266        [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]];
267        [patchedApps removeObject: app];
268    }
269
270    [tableView noteNumberOfRowsChanged];
271    [self _processStatusChanged];
272}
273
274@end
275
276@implementation FSAAppList (NSTableViewDelegate)
277
278- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row;
279{
280    if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
281        DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
282
283        NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell);
284        [(NJRLabeledImageCell *)cell setImage: [app img]];
285        [(NJRLabeledImageCell *)cell setImageCacheSource: app];
286    }
287}
288
289- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
290{
291    BOOL canInstall = NO;
292
293    if (row != -1) {
294        DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
295        canInstall = !([patchedApps containsObject: app] ||
296                       [patchingApps containsObject: app]);
297    }
298
299    [installButton setEnabled: canInstall];
300   
301    return YES;
302}
303
304@end
305
306@implementation FSAAppList (NSTableDataSource)
307
308- (int)numberOfRowsInTableView:(NSTableView *)aTableView
309{
310    return [cocoaApps count];
311}
312
313- (id)tableView:(NSTableView *)aTableView
314    objectValueForTableColumn:(NSTableColumn *)aTableColumn
315    row:(int)rowIndex
316{
317    NSString *columnIdentifier = [aTableColumn identifier];
318   
319    if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
320        return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name];
321    } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) {
322        DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex];
323        if ([patchedApps containsObject: app]) {
324            if (FSACheckMarkImage == nil)
325                return FSACheckMarkCharacter;
326            else
327                return FSACheckMarkImage;
328        }
329        if ([patchingApps containsObject: app]) {
330            if (FSAEllipsisImage == nil)
331                return FSAEllipsisCharacter;
332            else
333                return FSAEllipsisImage;
334        }
335    }
336    return nil;
337}
338
339@end
340
341@implementation FSAAppList (NSWindowDelegate)
342
343- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
344{
345    NSWindow *window = [tableView window];
346    NSRect frame = [window frame];
347    NSScrollView *scrollView = [tableView enclosingScrollView];
348    float displayedHeight = [[scrollView contentView] bounds].size.height;
349    float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
350    float heightExcess;
351
352    if (heightChange >= 0 && heightChange <= 1) {
353        // either the window is already optimal size, or it's too big
354        float rowHeight = [tableView cellHeight];
355        heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight;
356    }
357
358    frame.size.height += heightChange;
359
360    if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
361         (heightExcess = [window maxSize].height - frame.size.height) < 1) {
362        heightChange += heightExcess;
363        frame.size.height += heightExcess;
364    }
365
366    frame.origin.y -= heightChange;
367
368    return frame;
369}
370
371@end
372
373@implementation FSAAppList (NSApplicationDelegate)
374
375- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
376{
377    static NSMenu *dockMenu = nil;
378    id<NSMenuItem> menuItem;
379    DeVercruesseProcess *frontApp = [processManager frontProcess];
380    NSString *appName = [frontApp name];
381    NSString *status = nil;
382    // XXX workaround for broken dock menu sender
383    NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)];
384    NSInvocation *inv;
385
386    if (dockMenu != nil) {
387        // XXX release invocation
388        [[[dockMenu itemAtIndex: 0] target] release];
389        [dockMenu removeItemAtIndex: 0];
390    } else {
391        dockMenu = [[NSMenu alloc] init];
392    }
393
394    NSAssert(frontApp != nil && appName != nil, @"Can't obtain information on the frontmost application");
395
396    if ([patchedApps containsObject: frontApp]) {
397        status = [NSString stringWithFormat: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName];
398    } else if (![cocoaApps containsObject: frontApp]) {
399        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];
400    }
401
402    if (status == nil) {
403        menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), appName]
404                                       action: @selector(invoke)
405                                keyEquivalent: @""];
406        inv = [NSInvocation invocationWithMethodSignature: sig];
407        [inv setSelector: @selector(installBundleInFrontmostApp:)];
408        [inv setTarget: NSApp];
409        [inv setArgument: &menuItem atIndex: 2];
410        [menuItem setTag: [frontApp pid]];
411        [menuItem setTarget: [inv retain]];
412    } else {
413        menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""];
414        [menuItem setEnabled: NO];
415    }
416
417    return dockMenu;
418}
419
420- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender;
421{
422    return YES;
423}
424
425@end
Note: See TracBrowser for help on using the repository browser.