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

Last change on this file since 320 was 320, checked in by Nicholas Riley, 13 years ago

ICeCoffEE.m: I was wrong in [319] about the selection extension stuff
being dumb; explain why in code, even though it's not fixed.
Implement some of the same parens support I did in Terminal. Update
for more robust service info dictionary format.

ICeCoffEEServicePrefController.m: Update for more robust service info
dictionary format.

ICeCoffEEServices.[hm]: Renamed from ICeCoffEESetServicesMenu.[hm],
since we do more now. Service info dictionary-creating code now makes
more sense and no longer has naming collision issues.

ICeCoffEEWebKit.m: Some preliminary Safari 3 compatibility stuff, not
quite working yet. An outstanding question - is it better to rely on
"public" WebCore? API or private WebKit? API? So far it seems the
latter is more stable.

English.lproj/APEInfo.rtfd: The Bored Zo has a name: use it. Remove
now-erroneous reference to SimpleText? since TextEdit? support is gone.

File size: 26.6 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 "ICeCoffEEServices.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
346expandFront:
347        // XXX instead of 0, make this stop at the max URL length to prevent protracted searches
348           
349        // XXX here's how this is supposed to work:
350        // (http://web.sabi.net/) and <http://web.sabi.net/> should work if they are the entire document, even if clicking at the end/beginning of the document, not barfing with "no URL" (correct, as now) or selecting the final >, or ) (what would happen if we remove this "add 1" accommodation).  But how about "http://web.sabi.net/(foo)"?  That should work too, as long as it's not preceded by a (.
351        // Should probably backport to ICeCoffEETerminal, now I finally understand the method to this madness.
352        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...)
353        delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
354                                            options: NSLiteralSearch | NSBackwardsSearch
355                                              range: NSMakeRange(0, range.location + (range.location != [s length]))];
356        if (delimiterRange.location == NSNotFound) {
357            // extend to beginning of string
358            range.length += range.location;
359            range.location = 0;
360        } else {
361            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
362            range.length += range.location - delimiterRange.location - 1;
363            range.location = delimiterRange.location + 1;
364
365            // in url/(parens)stuff, handle clicking inside or after (parens).
366            if ([s characterAtIndex: delimiterRange.location] == '(' &&
367                range.location > 2 /* prevent wrapping, ordinarily not necessary */) {
368                if ([s rangeOfString: @")" options: NSLiteralSearch range: range].location != NSNotFound ||
369                    [s rangeOfCharacterFromSet: [NSCharacterSet characterSetWithCharactersInString: @"/."]
370                                       options: NSLiteralSearch range: range].location == NSNotFound) {
371                    range.location -= 2;
372                    range.length += 2;
373                    ICLog(@"expanding past (, now |%@|", [s substringWithRange: range]);
374                    goto expandFront;
375                }
376            }       
377        }
378       
379        ICCF_CheckRange(range);
380
381expandBack:
382        // XXX instead of length of string, make this stop at the max URL length to prevent protracted searches
383        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., ...>)
384        extraLen = [s length] - range.location - range.length;
385        delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
386                                            options: NSLiteralSearch
387                                              range: NSMakeRange(range.location + range.length - (range.length != 0),
388                                                                 extraLen + (range.length != 0))];
389        if (delimiterRange.location == NSNotFound) {
390            // extend to end of string
391            range.length += extraLen;
392        } else {
393            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
394            range.length += delimiterRange.location - range.location - range.length;
395           
396            // grow URL past closing paren if we've seen an open paren
397            if ([s characterAtIndex: delimiterRange.location] == ')' &&
398                [s rangeOfString: @"(" options: NSLiteralSearch range: range].location != NSNotFound) {
399                range.length += 2;
400                ICLog(@"expanding past ), now |%@|", [s substringWithRange: range]);
401                goto expandBack;
402            }
403        }
404
405        ICCF_CheckRange(range);
406
407        ICCF_ParseURL([s substringWithRange: range], &range);
408
409        [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: NO];
410        [self display];
411
412        if (ICCF_LaunchURL([s substringWithRange: range], ICCF_KeyboardAction(triggeringEvent)) && ICCF_prefs.textBlinkEnabled) {
413            for (i = 0 ; i < ICCF_prefs.textBlinkCount ; i++) {
414                NSRange emptyRange = {range.location, 0};
415                [self setSelectedRange: emptyRange affinity: NSSelectionAffinityDownstream stillSelecting: YES];
416                [self display];
417                usleep(kICBlinkDelayUsecs);
418                [self setInsertionPointColor: [self backgroundColor]];
419                [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: YES];
420                [self display];
421                usleep(kICBlinkDelayUsecs);
422            }
423        }
424
425    NS_HANDLER
426        ICCF_HandleException(localException);
427    NS_ENDHANDLER
428
429    ICCF_StopIC();
430    [self setInsertionPointColor: insertionPointColor];
431}
432
433NSString * const ICCF_SERVICES_ITEM = @"ICeCoffEE Services Item";
434
435NSMenuItem *ICCF_ServicesMenuItem() {
436    NSMenuItem *servicesItem;
437    NSString *servicesTitle = nil;
438    NSMenu *servicesMenu = [NSApp servicesMenu];
439   
440    if (servicesMenu != nil) {
441        servicesTitle = [servicesMenu title];
442        if (servicesTitle == nil) {
443            ICLog(@"Can't get service menu title");
444            servicesTitle = @"Services";
445        }
446    } else {
447        servicesTitle = [[NSBundle bundleWithIdentifier: @"com.apple.AppKit"] localizedStringForKey: @"Services" value: nil table: @"ServicesMenu"];
448        if (servicesTitle == nil) {
449            ICLog(@"Can't get localized text for 'Services' in AppKit.framework");
450            servicesTitle = @"Services";
451        }
452    }
453    servicesMenu = [[NSMenu alloc] initWithTitle: servicesTitle];
454    servicesItem = [[NSMenuItem alloc] initWithTitle: servicesTitle action:nil keyEquivalent:@""];
455    ICCF_SetServicesMenu(servicesMenu);
456    [servicesItem setSubmenu: servicesMenu];
457    [servicesItem setRepresentedObject: ICCF_SERVICES_ITEM];
458    [servicesMenu release];
459    return [servicesItem autorelease];
460}
461
462static const unichar UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE = 0x25b8;
463
464// returns YES if menu contains useful items, NO otherwise
465static BOOL ICCF_ConsolidateServicesMenu(NSMenu *menu, NSDictionary *serviceOptions, NSDictionary *serviceInfo) {
466    [menu update]; // doesn't propagate to submenus, so we need to do this first
467    NSEnumerator *enumerator = [[menu itemArray] objectEnumerator];
468    NSMenuItem *menuItem;
469    NSMenu *submenu;
470    NSDictionary *itemOptions = nil, *itemInfo = nil;
471    BOOL shouldKeepItem = NO, shouldKeepMenu = NO;
472
473    while ( (menuItem = [enumerator nextObject]) != nil) {
474        if (serviceOptions != nil)
475            itemOptions = [serviceOptions objectForKey: [menuItem title]];
476        if (serviceInfo != nil)
477            itemInfo = [serviceInfo objectForKey: [menuItem title]];
478        if ([[itemOptions objectForKey: (NSString *)kICServiceHidden] boolValue]) {
479            shouldKeepItem = NO;
480        } else if ( (submenu = [menuItem submenu]) != nil) {
481            // XXX don't rely on nil-sending working
482            shouldKeepItem = ICCF_ConsolidateServicesMenu(submenu, [itemOptions objectForKey: (NSString *)kICServiceSubmenu], [itemInfo objectForKey: (NSString *)kICServiceSubmenu]);
483            if (shouldKeepItem && [submenu numberOfItems] == 1) { // consolidate
484                NSMenuItem *serviceItem = [[submenu itemAtIndex: 0] retain];
485                [serviceItem setTitle:
486                    [NSString stringWithFormat: @"%@ %@ %@", [menuItem title], [NSString stringWithCharacters: &UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE length: 1], [serviceItem title]]];
487               
488                int serviceIndex = [menu indexOfItem: menuItem];
489                [submenu removeItemAtIndex: 0]; // can't have item in two menus
490                [menu removeItemAtIndex: serviceIndex];
491                [menu insertItem: serviceItem atIndex: serviceIndex];
492                [serviceItem release];
493                menuItem = serviceItem;
494            }
495        } else {
496            [menuItem setKeyEquivalent: @""];
497            shouldKeepItem = [menuItem isEnabled];
498        }
499        if (!shouldKeepItem) {
500            [menu removeItem: menuItem];
501            continue;
502        }
503        shouldKeepMenu = YES;
504       
505        if (itemInfo == nil) continue;
506        NSString *bundlePath = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceBundlePath];
507        if (bundlePath == NULL) continue;
508        IconRef serviceIcon = ICCF_CopyIconRefForPath(bundlePath);
509        if (serviceIcon == NULL) continue;
510        [menuItem _setIconRef: serviceIcon];
511        ReleaseIconRef(serviceIcon);
512    }
513
514    return shouldKeepMenu;
515}
516
517NSMenuItem *ICCF_ContextualServicesMenuItem() {
518    NSMenuItem *servicesItem = ICCF_ServicesMenuItem();
519    NSDictionary *servicesInfo = ICCF_GetServicesInfo(); // XXX cache/retain
520    if (ICCF_ConsolidateServicesMenu([servicesItem submenu], (NSDictionary *)ICCF_prefs.serviceOptions, servicesInfo))
521        return servicesItem;
522    else
523        return nil;
524}
525
526void ICCF_AddRemoveServicesMenu() {
527    // needed because:
528    // (a) we get called before the runloop has properly started and will crash if we don't delay on app startup
529    // (b) the APE message handler calls us from another thread and nothing happens if we try to add a menu on it
530    [ICeCoffEE performSelectorOnMainThread: @selector(IC_addRemoveServicesMenu) withObject: nil waitUntilDone: NO];
531}
532
533NSMenu *ICCF_MenuForEvent(NSView *self, NSMenu *contextMenu, NSEvent *e) {
534    if (contextMenu != nil && [e type] == NSRightMouseDown || ([e type] == NSLeftMouseDown && [e modifierFlags] & NSControlKeyMask)) {
535        int servicesItemIndex = [contextMenu indexOfItemWithRepresentedObject: ICCF_SERVICES_ITEM];
536        // always regenerate: make sure menu reflects context
537        if (servicesItemIndex != -1) {
538            [contextMenu removeItemAtIndex: servicesItemIndex];
539            [contextMenu removeItemAtIndex: servicesItemIndex - 1];
540        }
541        if (ICCF_prefs.servicesInContextualMenu) {
542            NSMenuItem *contextualServicesItem = ICCF_ContextualServicesMenuItem();
543            if (contextualServicesItem != nil) {
544                [contextMenu addItem: [NSMenuItem separatorItem]];
545                [contextMenu addItem: contextualServicesItem];
546            }
547        }
548    }
549    return contextMenu;
550}
551
552static NSEvent *ICCF_MouseDownEventWithModifierFlags(NSEvent *e, BOOL inheritModifierFlags) {
553    return [NSEvent mouseEventWithType: NSLeftMouseDown
554                              location: [e locationInWindow]
555                         modifierFlags: (inheritModifierFlags ? [e modifierFlags] : 0)
556                             timestamp: [e timestamp]
557                          windowNumber: [e windowNumber]
558                               context: [e context]
559                           eventNumber: [e eventNumber]
560                            clickCount: 1
561                              pressure: 0];
562}
563
564
565@interface NSTextView (IC_NSSharing)
566// only in Mac OS X 10.4 and later
567- (NSArray *)selectedRanges;
568@end
569
570@implementation ICeCoffEE
571
572+ (void)IC_addRemoveServicesMenu;
573{
574    NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
575    static NSMenuItem *servicesItem = nil;
576   
577    if (servicesItem == nil && ICCF_prefs.servicesInMenuBar) {
578        servicesItem = [ICCF_ServicesMenuItem() retain];
579
580        int insertLoc = [mainMenu indexOfItemWithSubmenu: [NSApp windowsMenu]];
581        if (insertLoc == -1)
582            insertLoc = [mainMenu numberOfItems];
583
584        [mainMenu insertItem: servicesItem atIndex: insertLoc];
585    } else if (servicesItem != nil && !ICCF_prefs.servicesInMenuBar) {
586        [mainMenu removeItem: servicesItem];
587        [servicesItem release];
588        servicesItem = nil;
589    }
590    if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_3) {
591        [[NSApp servicesMenu] update]; // enable keyboard equivalents in Mac OS X 10.3
592    }
593}
594
595// XXX localization?
596- (NSMenu *)menuForEvent:(NSEvent *)e;
597{
598    NSMenu *myMenu = [super menuForEvent: e];
599    return ICCF_MenuForEvent(self, myMenu, e);
600}
601
602- (void)mouseDown:(NSEvent *)e;
603{
604#if ICCF_DEBUG
605    static BOOL down = NO;
606    if (down) {
607        ICLog(@"recursive invocation!");
608        return;
609    }
610    down = YES;
611    ICLog(@"ICeCoffEE down: %@", e);
612#endif
613    if (ICCF_sharedTrigger != nil) {
614        ICLog(@"%@ cancelling", ICCF_sharedTrigger);
615        [ICCF_sharedTrigger cancel];
616    }
617    if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(e)) {
618        BOOL inheritModifierFlags;
619        if ([self respondsToSelector: @selector(selectedRanges)]) {
620            // Command-multiple-click or -drag for discontiguous selection, Mac OS X 10.4 or later
621            inheritModifierFlags = YES;
622        } else {
623            // don't want to trigger selection extension or anything else; pass through as a plain click
624            // (on Mac OS X 10.3, command does not modify behavior)
625            inheritModifierFlags = NO;
626        }
627        [super mouseDown: ICCF_MouseDownEventWithModifierFlags(e, inheritModifierFlags)];
628        // we don't actually get a mouseUp event, just wait for mouseDown to return
629        NSEvent *upEvent = [[self window] currentEvent];
630        NSPoint downPt = [e locationInWindow];
631        NSPoint upPt = [upEvent locationInWindow];
632        ICLog(@"next: %@", upEvent);
633        NSAssert([upEvent type] == NSLeftMouseUp, @"NSTextView mouseDown: did not return with current event as mouse up!");
634        if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
635            if (inheritModifierFlags) {
636                // Mac OS X 10.4 and later: make sure we don't have a command-double-click
637                [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
638                ICLog(@"%@ set", ICCF_sharedTrigger);
639            } else {
640                // Mac OS X 10.3
641                ICCF_LaunchURLFromTextView(self, e);
642            }
643        }
644    } else {
645        [super mouseDown: e];
646    }
647#if ICCF_DEBUG
648    down = NO;
649#endif
650}
651
652@end
Note: See TracBrowser for help on using the repository browser.