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

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

F-Script Anywhere 1.1.5

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