source: trunk/ICeCoffEE/ICeCoffEE/ICeCoffEE.m@ 319

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

VERSION: Starting with 1.5d1.

ICeCoffEEKeyEquivalents.m: Support "collision font" for displaying key
equivalent conflicts.

ICeCoffEE.m: Increase debug ICCF_MAX_URL_LEN to 120 for testing. Set
icons in ICCF_ConsolidateServicesMenu (needs better caching).

ICeCoffEEServicePrefController.m: Display icons, proper key
equivalents (instead of #, what was I thinking?!) and conflicts. Fix
a dumb bug in ICCF_PropagateServiceStateChange. Ellipsize long menu
items rather than chopping them off. Fix key equivalent column
getting moved when expanding disclosure triangles.

ICeCoffEELabeledIconCell.[hm]: An IconRef-displaying text cell.

Info-APE Module.plist: Update version to 1.5d1.

ICeCoffEE.xcodeproj: Added files, no significant changes.

English.lproj/InfoPlist.strings: Update version to 1.5d1.

English.lproj/APEInfo.rtfd/TXT.rtf: Some overdue documentation
updates.

ICeCoffEEShared.[hm]: Enable debugging; we're now using
kICServiceShortcut (though not yet for customizable shortcuts) so
define its data type.

ICeCoffEETerminal.m: Remove some useless code to "extend to beginning
of string" which seems to have been stolen from the NSTextView
implementation and not well understood. Handle common uses of
parentheses in URLs; still need to do this for NSTextView.

ICeCoffEESetServicesMenu.[hm]: Needs renaming; now with icon
extraction functionality and semi-working code to create a service
info dictionary.

Info-APEManagerPrefPane.plist: Update version to 1.5d1.

File size: 24.8 KB
Line 
1// ICeCoffEE - Internet Config Carbon/Cocoa Editor Extension
2// Nicholas Riley <mailto:icecoffee@sabi.net>
3
4/* To do/think about:
5
6- TXNClick - MLTE has its own (lousy) support in Jaguar, seems improved in Panther, good enough to leave?
7
8*/
9
10#import "ICeCoffEE.h"
11#import <Carbon/Carbon.h>
12#include <unistd.h>
13#import "ICeCoffEESuper.h"
14#import "ICeCoffEESetServicesMenu.h"
15#import "ICeCoffEETrigger.h"
16
17iccfPrefRec ICCF_prefs;
18
19NSString *ICCF_ErrString(OSStatus err, NSString *context) {
20 if (err == noErr || err == userCanceledErr) return nil;
21
22 NSString *errNum = [NSString stringWithFormat: @"%ld", err];
23 NSString *errDesc = ICCF_LocalizedString(errNum);
24
25 if (errDesc == NULL || errDesc == errNum)
26 errDesc = [NSString stringWithFormat: ICCF_LocalizedString(@"An unknown error occurred in %@"), context];
27
28 return [NSString stringWithFormat: @"%@ (%d)", errDesc, (int)err];
29}
30
31CFStringRef ICCF_CopyErrString(OSStatus err, CFStringRef context) {
32 if (err == noErr || err == userCanceledErr) return NULL;
33
34 CFStringRef errNum = CFStringCreateWithFormat(NULL, NULL, CFSTR("%ld"), err);
35 CFStringRef errDesc = ICCF_CopyLocalizedString(errNum);
36
37 if (errDesc == NULL || errDesc == errNum) {
38 CFStringRef errDescFormat = ICCF_CopyLocalizedString(CFSTR("An unknown error occurred in %@"));
39 if (errDesc != NULL) CFRelease(errDesc);
40 errDesc = CFStringCreateWithFormat(NULL, NULL, errDescFormat, context);
41 }
42
43 CFStringRef errStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%d)"), errDesc, (int)err);
44
45 if (errNum != NULL) CFRelease(errNum);
46 if (errDesc != NULL) CFRelease(errDesc);
47 return errStr;
48}
49
50CFStringRef ICCF_CopyAppName() {
51 ProcessSerialNumber psn = {0, kCurrentProcess};
52 CFStringRef appName = NULL;
53 CopyProcessName(&psn, &appName);
54 if (appName == NULL) return CFSTR("(unknown)");
55 return appName;
56}
57
58BOOL ICCF_EventIsCommandMouseDown(NSEvent *e) {
59 return ([e type] == NSLeftMouseDown && ([e modifierFlags] & NSCommandKeyMask) != 0 && [e clickCount] == 1);
60}
61
62iccfURLAction ICCF_KeyboardAction(NSEvent *e) {
63 unsigned int modifierFlags = [e modifierFlags];
64 iccfURLAction action;
65 action.presentMenu = (modifierFlags & NSAlternateKeyMask) != 0;
66 action.launchInBackground = (modifierFlags & NSShiftKeyMask) != 0;
67 return action;
68}
69
70void ICCF_CheckRange(NSRange range) {
71 NSCAssert(range.length > 0, ICCF_LocalizedString(@"No URL is selected"));
72 NSCAssert1(range.length <= ICCF_MAX_URL_LEN, ICCF_LocalizedString(@"The potential URL is longer than %lu characters"), ICCF_MAX_URL_LEN);
73}
74
75void ICCF_Delimiters(NSCharacterSet **leftPtr, NSCharacterSet **rightPtr) {
76 static NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
77
78 if (urlLeftDelimiters == nil || urlRightDelimiters == nil) {
79 NSMutableCharacterSet *set = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
80 NSMutableCharacterSet *tmpSet;
81 [urlLeftDelimiters release];
82 [urlRightDelimiters release];
83
84 [set autorelease];
85 [set formUnionWithCharacterSet: [[NSCharacterSet characterSetWithRange: NSMakeRange(0x21, 0x5e)] invertedSet]]; // nonprintable and non-ASCII characters
86 [set formUnionWithCharacterSet: [NSCharacterSet punctuationCharacterSet]];
87 [set removeCharactersInString: @";/?:@&=+$,-_.!~*'()%#"]; // RFC 2396 ¤2.2, 2.3, 2.4, plus % and # from "delims" set
88
89 tmpSet = [[set mutableCopy] autorelease];
90 [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><("]];
91 urlLeftDelimiters = [tmpSet copy]; // make immutable again - for efficiency
92
93 tmpSet = [[set mutableCopy] autorelease];
94 [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><)"]];
95 urlRightDelimiters = [tmpSet copy]; // make immutable again - for efficiency
96 }
97
98 *leftPtr = urlLeftDelimiters; *rightPtr = urlRightDelimiters;
99}
100
101static ICInstance ICCF_icInst = NULL;
102
103void ICCF_StartIC() {
104 OSStatus err;
105
106 if (ICCF_icInst != NULL) {
107 ICLog(@"ICCF_StartIC: Internet Config is already running!");
108 ICCF_StopIC();
109 }
110 err = ICStart(&ICCF_icInst, kICCFCreator);
111 NSCAssert1(err == noErr, ICCF_LocalizedString(@"Unable to start Internet Config (error %d)"), err);
112}
113
114void ICCF_StopIC() {
115 if (ICCF_icInst == NULL) {
116 ICLog(@"ICCF_StopIC: Internet Config is not running!");
117 } else {
118 ICStop(ICCF_icInst);
119 ICCF_icInst = NULL;
120 }
121}
122
123ICInstance ICCF_GetInst() {
124 NSCAssert(ICCF_icInst != NULL, @"Internal error: Called ICCF_GetInst without ICCF_StartIC");
125 return ICCF_icInst;
126}
127
128ConstStringPtr ICCF_GetHint(ICInstance inst, const char *urlData, Size length, long *selStart, long *selEnd, Boolean *needsSlashes) {
129 Handle h = NewHandle(0);
130 OSStatus err;
131
132 if (h == NULL) return NULL;
133
134 // parse the URL providing a bogus protocol, to get rid of escaped forms
135 err = ICParseURL(inst, "\p*", urlData, length, selStart, selEnd, h);
136 if (err != noErr) return NULL;
137
138 // scan through the parsed URL looking for characters not found in email addresses
139 Size hSize = GetHandleSize(h);
140 if (hSize == 0) return NULL;
141
142 const char *urlParsed = *h;
143 long i = 0;
144 Boolean sawAt = false;
145 if (hSize >= 2 && urlParsed[0] == '*' && urlParsed[1] == ':') {
146 // this is an IC-inserted protocol; skip over it
147 i = 2;
148 *needsSlashes = (hSize < i + 2 || urlParsed[i] != '/' || urlParsed[i + 1] != '/');
149 } else *needsSlashes = false;
150 for ( ; i < hSize ; i++) {
151 char c = urlParsed[i];
152 if (c == '@') {
153 sawAt = true;
154 } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
155 (c == '+' || c == '-' || c == '_' || c == '!' || c == '.'))) {
156 DisposeHandle(h);
157 return "\phttp";
158 }
159 }
160 DisposeHandle(h);
161 if (sawAt) {
162 *needsSlashes = false;
163 return "\pmailto";
164 }
165 return "\phttp";
166}
167
168static const char *kICSlashes = "//";
169
170void ICCF_AddSlashes(Handle h, ConstStringPtr hint) {
171 Size sizeBefore = GetHandleSize(h);
172 unsigned char hintLength = StrLength(hint);
173 char *copy = (char *)malloc(sizeBefore);
174 memcpy(copy, *h, sizeBefore);
175 ICLog(@"ICCF_AddSlashes before: |%s|\n", *h);
176 ReallocateHandle(h, sizeBefore + 2);
177
178 // if *h begins with '<hint>:', then copy the slashes after it
179 if (sizeBefore > hintLength + 1 && strncmp((const char *)&hint[1], copy, hintLength) == 0 && copy[hintLength] == ':') {
180 memcpy(*h, copy, hintLength + 1);
181 memcpy(*h + hintLength + 1, kICSlashes, 2);
182 memcpy(*h + hintLength + 3, &copy[hintLength + 1], sizeBefore - hintLength - 1);
183 } else {
184 memcpy(*h, kICSlashes, 2);
185 memcpy(*h + 2, copy, sizeBefore);
186 }
187
188 free(copy);
189 ICLog(@"ICCF_AddSlashes after: |%s|\n", *h);
190}
191
192// input/output 'range' is the range of source document which contains 'string'
193void ICCF_ParseURL(NSString *string, NSRange *range) {
194 OSStatus err;
195 Handle h;
196 long selStart = 0, selEnd = range->length; // local offsets within 'string'
197 char *urlData = NULL;
198
199 NSCAssert(selEnd == [string length], @"Internal error: URL string is wrong length");
200
201 NS_DURING
202 if ([[NSCharacterSet characterSetWithCharactersInString: @";,."] characterIsMember:
203 [string characterAtIndex: selEnd - 1]]) {
204 selEnd--;
205 }
206 NSCharacterSet *alphanumericCharacterSet = [NSCharacterSet alphanumericCharacterSet];
207 while (![alphanumericCharacterSet characterIsMember: [string characterAtIndex: selStart]]) {
208 selStart++;
209 NSCAssert(selStart < selEnd, @"No URL is selected");
210 }
211
212 string = [string substringWithRange: NSMakeRange(selStart, selEnd - selStart)];
213
214 ICLog(@"Parsing URL |%@|", string);
215
216 NSCAssert([string canBeConvertedToEncoding: NSASCIIStringEncoding], @"No URL is selected");
217
218 urlData = (char *)malloc( (range->length + 1) * sizeof(char));
219 NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string");
220
221 // XXX getCString: is deprecated in 10.4, but this is safe and shouldn't assert because we've already verified the string can be converted to ASCII, which should be a subset of any possible system encoding. The replacement (getCString:maxLength:encoding:) is not available until 10.4, so we leave this until we dump Internet Config and gain IDN friendliness.
222 [string getCString: urlData];
223
224 h = NewHandle(0);
225 NSCAssert(h != NULL, @"Internal error: can't allocate URL handle");
226
227 err = ICParseURL(ICCF_GetInst(), "\pmailto", urlData, range->length, &selStart, &selEnd, h);
228 DisposeHandle(h);
229
230 ICCF_OSErrCAssert(err, @"ICParseURL");
231
232 range->length = selEnd - selStart;
233 range->location += selStart;
234 NS_HANDLER
235 free(urlData);
236 [localException raise];
237 NS_ENDHANDLER
238
239 free(urlData);
240}
241
242BOOL ICCF_LaunchURL(NSString *string, iccfURLAction action) {
243 OSStatus err = noErr;
244 long selStart, selEnd;
245 NSMutableString *urlString = [[NSMutableString alloc] init];
246 NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet];
247 NSScanner *scanner = [[NSScanner alloc] initWithString: string];
248 NSString *fragmentString;
249 while ([scanner scanUpToCharactersFromSet: whitespace intoString: &fragmentString]) {
250 [urlString appendString: fragmentString];
251 }
252 unsigned len = [urlString length];
253
254 Handle h = NULL;
255
256 NS_DURING
257 h = NewHandle(len);
258 if (h == NULL)
259 ICCF_OSErrCAssert(MemError(), @"NewHandle");
260
261 if (CFStringGetBytes((CFStringRef)urlString, CFRangeMake(0, len), kCFStringEncodingASCII, '\0', false, (UInt8 *)*h, len, NULL) != len)
262 ICCF_OSErrCAssert(kTECNoConversionPathErr, @"CFStringGetBytes");
263
264 selStart = 0; selEnd = len;
265
266 Boolean needsSlashes;
267 ConstStringPtr hint = ICCF_GetHint(ICCF_GetInst(), *h, len, &selStart, &selEnd, &needsSlashes);
268 NSCAssert(hint != NULL, @"Internal error: can't get protocol hint for URL");
269
270 if (needsSlashes) {
271 ICCF_AddSlashes(h, hint);
272 len = selEnd = GetHandleSize(h);
273 }
274
275 err = ICCF_DoURLAction(ICCF_GetInst(), hint, *h, selStart, selEnd, action);
276 ICCF_OSErrCAssert(err, @"ICCF_DoURLAction");
277
278 NS_HANDLER
279 DisposeHandle(h);
280 [urlString release];
281 [localException raise];
282 NS_ENDHANDLER
283
284 DisposeHandle(h);
285 [urlString release];
286
287 return (err == noErr);
288}
289
290// XXX not sure what to do if there's already a selection; BBEdit and MLTE extend it, Tex-Edit Plus doesn't.
291// RFC-ordained max URL length, just to avoid passing IC/LS multi-megabyte documents
292#if ICCF_DEBUG
293const long ICCF_MAX_URL_LEN = 120; // XXX change later
294#else
295const long ICCF_MAX_URL_LEN = 1024;
296#endif
297
298Boolean ICCF_enabled = true;
299
300BOOL ICCF_HandleException(NSException *e) {
301 if ([e reason] == nil || [[e reason] length] == 0)
302 return NO;
303
304 if (ICCF_prefs.errorSoundEnabled) NSBeep();
305 if (!ICCF_prefs.errorDialogEnabled) return YES;
306
307 int result = NSRunAlertPanel(ICCF_LocalizedString(@"AlertTitle"), ICCF_LocalizedString(@"AlertMessage%@"), nil, nil, ICCF_LocalizedString(@"AlertDisableButton"), e);
308 if (result != NSAlertDefaultReturn) {
309 result = NSRunAlertPanel(ICCF_LocalizedString(@"DisableAlertTitle"), ICCF_LocalizedString(@"DisableAlertMessage%@"), ICCF_LocalizedString(@"DisableAlertDisableButton"), ICCF_LocalizedString(@"DisableAlertDontDisableButton"), nil,
310 [(NSString *)ICCF_CopyAppName() autorelease]);
311 if (result == NSAlertDefaultReturn)
312 ICCF_enabled = NO;
313 }
314 return YES;
315}
316
317void ICCF_LaunchURLFromTextView(NSTextView *self, NSEvent *triggeringEvent) {
318 NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
319 NSRange range = [self selectedRange], delimiterRange;
320 NSColor *insertionPointColor = [self insertionPointColor];
321 NSString *s = [[self textStorage] string]; // according to the class documentation, sending 'string' is guaranteed to be O(1)
322 unsigned extraLen;
323 int i;
324
325 NS_DURING
326
327 NSCAssert(range.location != NSNotFound, ICCF_LocalizedString(@"There is no insertion point or selection in the text field where you clicked"));
328 NSCAssert(s != nil, ICCF_LocalizedString(@"Sorry, ICeCoffEE is unable to locate the insertion point or selection"));
329
330 ICCF_StartIC();
331
332 NSCAssert([s length] != 0, ICCF_LocalizedString(@"No text was found"));
333
334 if (range.location == [s length]) range.location--; // work around bug in selectionRangeForProposedRange (r. 2845418)
335
336 range = [self selectionRangeForProposedRange: range granularity: NSSelectByWord];
337
338 // However, NSSelectByWord does not capture even the approximate boundaries of a URL
339 // (text to a space/line ending character); it'll stop at a period in the middle of a hostname.
340 // So, we expand it as follows:
341
342 ICCF_CheckRange(range);
343
344 ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);
345
346 // XXX instead of 0, make this stop at the max URL length to prevent protracted searches
347 // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...)
348 delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
349 options: NSLiteralSearch | NSBackwardsSearch
350 range: NSMakeRange(0, range.location + (range.location != [s length]))];
351 if (delimiterRange.location == NSNotFound) {
352 // extend to beginning of string
353 range.length += range.location;
354 range.location = 0;
355 } else {
356 NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
357 range.length += range.location - delimiterRange.location - 1;
358 range.location = delimiterRange.location + 1;
359 }
360
361 ICCF_CheckRange(range);
362
363 // XXX instead of length of string, make this stop at the max URL length to prevent protracted searches
364 // add 1 to range to trap delimiters that are on the edge of the selection (i.e., ...>)
365 extraLen = [s length] - range.location - range.length;
366 delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
367 options: NSLiteralSearch
368 range: NSMakeRange(range.location + range.length - (range.length != 0),
369 extraLen + (range.length != 0))];
370 if (delimiterRange.location == NSNotFound) {
371 // extend to end of string
372 range.length += extraLen;
373 } else {
374 NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
375 range.length += delimiterRange.location - range.location - range.length;
376 }
377
378 ICCF_CheckRange(range);
379
380 ICCF_ParseURL([s substringWithRange: range], &range);
381
382 [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: NO];
383 [self display];
384
385 if (ICCF_LaunchURL([s substringWithRange: range], ICCF_KeyboardAction(triggeringEvent)) && ICCF_prefs.textBlinkEnabled) {
386 for (i = 0 ; i < ICCF_prefs.textBlinkCount ; i++) {
387 NSRange emptyRange = {range.location, 0};
388 [self setSelectedRange: emptyRange affinity: NSSelectionAffinityDownstream stillSelecting: YES];
389 [self display];
390 usleep(kICBlinkDelayUsecs);
391 [self setInsertionPointColor: [self backgroundColor]];
392 [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: YES];
393 [self display];
394 usleep(kICBlinkDelayUsecs);
395 }
396 }
397
398 NS_HANDLER
399 ICCF_HandleException(localException);
400 NS_ENDHANDLER
401
402 ICCF_StopIC();
403 [self setInsertionPointColor: insertionPointColor];
404}
405
406NSString * const ICCF_SERVICES_ITEM = @"ICeCoffEE Services Item";
407
408NSMenuItem *ICCF_ServicesMenuItem() {
409 NSMenuItem *servicesItem;
410 NSString *servicesTitle = nil;
411 NSMenu *servicesMenu = [NSApp servicesMenu];
412
413 if (servicesMenu != nil) {
414 servicesTitle = [servicesMenu title];
415 if (servicesTitle == nil) {
416 ICLog(@"Can't get service menu title");
417 servicesTitle = @"Services";
418 }
419 } else {
420 servicesTitle = [[NSBundle bundleWithIdentifier: @"com.apple.AppKit"] localizedStringForKey: @"Services" value: nil table: @"ServicesMenu"];
421 if (servicesTitle == nil) {
422 ICLog(@"Can't get localized text for 'Services' in AppKit.framework");
423 servicesTitle = @"Services";
424 }
425 }
426 servicesMenu = [[NSMenu alloc] initWithTitle: servicesTitle];
427 servicesItem = [[NSMenuItem alloc] initWithTitle: servicesTitle action:nil keyEquivalent:@""];
428 ICCF_SetServicesMenu(servicesMenu);
429 [servicesItem setSubmenu: servicesMenu];
430 [servicesItem setRepresentedObject: ICCF_SERVICES_ITEM];
431 [servicesMenu release];
432 return [servicesItem autorelease];
433}
434
435static const unichar UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE = 0x25b8;
436
437// returns YES if menu contains useful items, NO otherwise
438BOOL ICCF_ConsolidateServicesMenu(NSMenu *menu, NSDictionary *serviceOptions, NSDictionary *serviceInfo) {
439 [menu update]; // doesn't propagate to submenus, so we need to do this first
440 NSEnumerator *enumerator = [[menu itemArray] objectEnumerator];
441 NSMenuItem *menuItem;
442 NSMenu *submenu;
443 NSDictionary *itemOptions = nil, *itemInfo = nil;
444 BOOL shouldKeepItem = NO, shouldKeepMenu = NO;
445
446 while ( (menuItem = [enumerator nextObject]) != nil) {
447 if (serviceOptions != nil)
448 itemOptions = [serviceOptions objectForKey: [menuItem title]];
449 if (serviceInfo != nil)
450 itemInfo = [serviceInfo objectForKey: [menuItem title]];
451 if ([[itemOptions objectForKey: (NSString *)kICServiceHidden] boolValue]) {
452 shouldKeepItem = NO;
453 } else if ( (submenu = [menuItem submenu]) != nil) {
454 // XXX don't rely on nil-sending working
455 shouldKeepItem = ICCF_ConsolidateServicesMenu(submenu, [itemOptions objectForKey: (NSString *)kICServiceSubmenu], itemInfo);
456 if (shouldKeepItem && [submenu numberOfItems] == 1) { // consolidate
457 NSMenuItem *serviceItem = [[submenu itemAtIndex: 0] retain];
458 [serviceItem setTitle:
459 [NSString stringWithFormat: @"%@ %@ %@", [menuItem title], [NSString stringWithCharacters: &UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE length: 1], [serviceItem title]]];
460
461 int serviceIndex = [menu indexOfItem: menuItem];
462 [submenu removeItemAtIndex: 0]; // can't have item in two menus
463 [menu removeItemAtIndex: serviceIndex];
464 [menu insertItem: serviceItem atIndex: serviceIndex];
465 [serviceItem release];
466 menuItem = serviceItem;
467 }
468 } else {
469 [menuItem setKeyEquivalent: @""];
470 shouldKeepItem = [menuItem isEnabled];
471 }
472 if (!shouldKeepItem) {
473 [menu removeItem: menuItem];
474 continue;
475 }
476 shouldKeepMenu = YES;
477
478 if (itemInfo == nil) continue;
479 NSString *bundlePath = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceBundlePath];
480 if (bundlePath == NULL) continue;
481 IconRef serviceIcon = ICCF_CopyIconRefForPath(bundlePath);
482 if (serviceIcon == NULL) continue;
483 [menuItem _setIconRef: serviceIcon];
484 ReleaseIconRef(serviceIcon);
485 }
486
487 return shouldKeepMenu;
488}
489
490NSMenuItem *ICCF_ContextualServicesMenuItem() {
491 NSMenuItem *servicesItem = ICCF_ServicesMenuItem();
492 NSDictionary *servicesInfo = ICCF_GetServicesInfo(); // XXX cache/retain
493 if (ICCF_ConsolidateServicesMenu([servicesItem submenu], (NSDictionary *)ICCF_prefs.serviceOptions, servicesInfo))
494 return servicesItem;
495 else
496 return nil;
497}
498
499void ICCF_AddRemoveServicesMenu() {
500 // needed because:
501 // (a) we get called before the runloop has properly started and will crash if we don't delay on app startup
502 // (b) the APE message handler calls us from another thread and nothing happens if we try to add a menu on it
503 [ICeCoffEE performSelectorOnMainThread: @selector(IC_addRemoveServicesMenu) withObject: nil waitUntilDone: NO];
504}
505
506NSMenu *ICCF_MenuForEvent(NSView *self, NSMenu *contextMenu, NSEvent *e) {
507 if (contextMenu != nil && [e type] == NSRightMouseDown || ([e type] == NSLeftMouseDown && [e modifierFlags] & NSControlKeyMask)) {
508 int servicesItemIndex = [contextMenu indexOfItemWithRepresentedObject: ICCF_SERVICES_ITEM];
509 // always regenerate: make sure menu reflects context
510 if (servicesItemIndex != -1) {
511 [contextMenu removeItemAtIndex: servicesItemIndex];
512 [contextMenu removeItemAtIndex: servicesItemIndex - 1];
513 }
514 if (ICCF_prefs.servicesInContextualMenu) {
515 NSMenuItem *contextualServicesItem = ICCF_ContextualServicesMenuItem();
516 if (contextualServicesItem != nil) {
517 [contextMenu addItem: [NSMenuItem separatorItem]];
518 [contextMenu addItem: contextualServicesItem];
519 }
520 }
521 }
522 return contextMenu;
523}
524
525static NSEvent *ICCF_MouseDownEventWithModifierFlags(NSEvent *e, BOOL inheritModifierFlags) {
526 return [NSEvent mouseEventWithType: NSLeftMouseDown
527 location: [e locationInWindow]
528 modifierFlags: (inheritModifierFlags ? [e modifierFlags] : 0)
529 timestamp: [e timestamp]
530 windowNumber: [e windowNumber]
531 context: [e context]
532 eventNumber: [e eventNumber]
533 clickCount: 1
534 pressure: 0];
535}
536
537
538@interface NSTextView (IC_NSSharing)
539// only in Mac OS X 10.4 and later
540- (NSArray *)selectedRanges;
541@end
542
543@implementation ICeCoffEE
544
545+ (void)IC_addRemoveServicesMenu;
546{
547 NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
548 static NSMenuItem *servicesItem = nil;
549
550 if (servicesItem == nil && ICCF_prefs.servicesInMenuBar) {
551 servicesItem = [ICCF_ServicesMenuItem() retain];
552
553 int insertLoc = [mainMenu indexOfItemWithSubmenu: [NSApp windowsMenu]];
554 if (insertLoc == -1)
555 insertLoc = [mainMenu numberOfItems];
556
557 [mainMenu insertItem: servicesItem atIndex: insertLoc];
558 } else if (servicesItem != nil && !ICCF_prefs.servicesInMenuBar) {
559 [mainMenu removeItem: servicesItem];
560 [servicesItem release];
561 servicesItem = nil;
562 }
563 if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_3) {
564 [[NSApp servicesMenu] update]; // enable keyboard equivalents in Mac OS X 10.3
565 }
566}
567
568// XXX localization?
569- (NSMenu *)menuForEvent:(NSEvent *)e;
570{
571 NSMenu *myMenu = [super menuForEvent: e];
572 return ICCF_MenuForEvent(self, myMenu, e);
573}
574
575- (void)mouseDown:(NSEvent *)e;
576{
577#if ICCF_DEBUG
578 static BOOL down = NO;
579 if (down) {
580 ICLog(@"recursive invocation!");
581 return;
582 }
583 down = YES;
584 ICLog(@"ICeCoffEE down: %@", e);
585#endif
586 if (ICCF_sharedTrigger != nil) {
587 ICLog(@"%@ cancelling", ICCF_sharedTrigger);
588 [ICCF_sharedTrigger cancel];
589 }
590 if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(e)) {
591 BOOL inheritModifierFlags;
592 if ([self respondsToSelector: @selector(selectedRanges)]) {
593 // Command-multiple-click or -drag for discontiguous selection, Mac OS X 10.4 or later
594 inheritModifierFlags = YES;
595 } else {
596 // don't want to trigger selection extension or anything else; pass through as a plain click
597 // (on Mac OS X 10.3, command does not modify behavior)
598 inheritModifierFlags = NO;
599 }
600 [super mouseDown: ICCF_MouseDownEventWithModifierFlags(e, inheritModifierFlags)];
601 // we don't actually get a mouseUp event, just wait for mouseDown to return
602 NSEvent *upEvent = [[self window] currentEvent];
603 NSPoint downPt = [e locationInWindow];
604 NSPoint upPt = [upEvent locationInWindow];
605 ICLog(@"next: %@", upEvent);
606 NSAssert([upEvent type] == NSLeftMouseUp, @"NSTextView mouseDown: did not return with current event as mouse up!");
607 if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
608 if (inheritModifierFlags) {
609 // Mac OS X 10.4 and later: make sure we don't have a command-double-click
610 [ICeCoffEETrigger setTriggerForEvent: e onTarget: self]; // gets stored in ICCF_sharedTrigger; the reason for this weird calling pattern is that we don't want to add methods to NSTextView, and we don't want to add a method call on every mouseDown
611 ICLog(@"%@ set", ICCF_sharedTrigger);
612 } else {
613 // Mac OS X 10.3
614 ICCF_LaunchURLFromTextView(self, e);
615 }
616 }
617 } else {
618 [super mouseDown: e];
619 }
620#if ICCF_DEBUG
621 down = NO;
622#endif
623}
624
625@end
Note: See TracBrowser for help on using the repository browser.