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

Last change on this file since 127 was 19, checked in by Nicholas Riley, 22 years ago

F-Script Anywhere 1.1.6d1

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