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

Last change on this file since 16 was 16, checked in by Nicholas Riley, 22 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.