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

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