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

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

ICeCoffEE 1.4.2b1

VERSION, ui.plist, Info*.plist, InfoPlist?.strings: Updated for 1.4.2b1.

APEMain.m: Removed PBX support.

ICeCoffEE.[hm]: Don't ICCF_OSErr(C)Assert if we get userCanceledErr:
part of fixing extraneous exception when not selecting anything from
the helper app menu. ICCF_KeyboardAction() and
ICCF_LaunchURLFromTextView() now take an event parameter - since we
have stuff on a timer now, means we get the key modifier state at
mousedown time, rather than some arbitrary time later.
ICCF_LaunchURL() returns NO if the user cancelled, so we don't need to
throw an exception to stop the URL blinking. Moved sanitized mouse
down event generation to ICCF_MouseDownEventWithModifierFlags(). Only
update Services menu at app launch on Panther. Use a timer to delay
URL launching in NSTextView on Tiger, so we accommodate
command-multiple clicking for discontiguous selection. Remove that
ugly goto.

ICeCoffEEShared.h: Turn off debugging in preparation for (beta)
release.

ICeCoffEETerminal.m: Update for new ICCF_LaunchURL() return.

ICeCoffEETrigger.[hm]: Singleton timer wrapper for discontiguous
selection compatibility on Tiger. Singleton global is exported for
efficiency since we have to check it on every mouse down.

ICeCoffEEWebKit.m: Removed incorrect comment (what was I thinking?)
Update for new ICCF_LaunchURL() return. Properly highlight before
ICCF_LaunchURL(), especially noticable with menu.

APEInfo.rtfd: More fixes and updates, final for 1.4.2b1.

File size: 23.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 "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    unsigned len = [string length];
246
247    Handle h = NULL;
248   
249    NS_DURING
250        h = NewHandle(len);
251        if (h == NULL)
252            ICCF_OSErrCAssert(MemError(), @"NewHandle");
253
254        if (CFStringGetBytes((CFStringRef)string, CFRangeMake(0, len), kCFStringEncodingASCII, '\0', false, (UInt8 *)*h, len, NULL) != len)
255            ICCF_OSErrCAssert(kTECNoConversionPathErr, @"CFStringGetBytes");
256
257        selStart = 0; selEnd = len;
258
259        Boolean needsSlashes;
260        ConstStringPtr hint = ICCF_GetHint(ICCF_GetInst(), *h, len, &selStart, &selEnd, &needsSlashes);
261        NSCAssert(hint != NULL, @"Internal error: can't get protocol hint for URL");
262
263        if (needsSlashes) {
264            ICCF_AddSlashes(h, hint);
265            len = selEnd = GetHandleSize(h);
266        }
267
268        err = ICCF_DoURLAction(ICCF_GetInst(), hint, *h, selStart, selEnd, action);
269        ICCF_OSErrCAssert(err, @"ICCF_DoURLAction");
270       
271    NS_HANDLER
272        DisposeHandle(h);
273        [localException raise];
274    NS_ENDHANDLER
275
276    DisposeHandle(h);
277
278    return (err == noErr);
279}
280
281// XXX not sure what to do if there's already a selection; BBEdit and MLTE extend it, Tex-Edit Plus doesn't.
282// RFC-ordained max URL length, just to avoid passing IC/LS multi-megabyte documents
283#if ICCF_DEBUG
284const long ICCF_MAX_URL_LEN = 60; // XXX change later
285#else
286const long ICCF_MAX_URL_LEN = 1024;
287#endif
288
289Boolean ICCF_enabled = true;
290
291BOOL ICCF_HandleException(NSException *e) {
292    if ([e reason] == nil || [[e reason] length] == 0)
293        return NO;
294   
295    if (ICCF_prefs.errorSoundEnabled) NSBeep();
296    if (!ICCF_prefs.errorDialogEnabled) return YES;
297   
298    int result = NSRunAlertPanel(ICCF_LocalizedString(@"AlertTitle"), ICCF_LocalizedString(@"AlertMessage%@"), nil, nil, ICCF_LocalizedString(@"AlertDisableButton"), e);
299    if (result != NSAlertDefaultReturn) {
300        result = NSRunAlertPanel(ICCF_LocalizedString(@"DisableAlertTitle"), ICCF_LocalizedString(@"DisableAlertMessage%@"), ICCF_LocalizedString(@"DisableAlertDisableButton"), ICCF_LocalizedString(@"DisableAlertDontDisableButton"), nil,
301           [(NSString *)ICCF_CopyAppName() autorelease]);
302        if (result == NSAlertDefaultReturn)
303            ICCF_enabled = NO;
304    }
305    return YES;
306}
307
308void ICCF_LaunchURLFromTextView(NSTextView *self, NSEvent *triggeringEvent) {
309    NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
310    NSRange range = [self selectedRange], delimiterRange;
311    NSColor *insertionPointColor = [self insertionPointColor];
312    NSString *s = [[self textStorage] string]; // according to the class documentation, sending 'string' is guaranteed to be O(1)
313    unsigned extraLen;
314    int i;
315
316    NS_DURING
317
318        NSCAssert(range.location != NSNotFound, ICCF_LocalizedString(@"There is no insertion point or selection in the text field where you clicked"));
319        NSCAssert(s != nil, ICCF_LocalizedString(@"Sorry, ICeCoffEE is unable to locate the insertion point or selection"));
320
321        ICCF_StartIC();
322
323        NSCAssert([s length] != 0, ICCF_LocalizedString(@"No text was found"));
324
325        if (range.location == [s length]) range.location--; // work around bug in selectionRangeForProposedRange (r. 2845418)
326
327        range = [self selectionRangeForProposedRange: range granularity: NSSelectByWord];
328
329        // However, NSSelectByWord does not capture even the approximate boundaries of a URL
330        // (text to a space/line ending character); it'll stop at a period in the middle of a hostname.
331        // So, we expand it as follows:
332
333        ICCF_CheckRange(range);
334
335        ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);
336
337        // XXX instead of 0, make this stop at the max URL length to prevent protracted searches
338        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...)
339        delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
340                                            options: NSLiteralSearch | NSBackwardsSearch
341                                              range: NSMakeRange(0, range.location + (range.location != [s length]))];
342        if (delimiterRange.location == NSNotFound) {
343            // extend to beginning of string
344            range.length += range.location;
345            range.location = 0;
346        } else {
347            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
348            range.length += range.location - delimiterRange.location - 1;
349            range.location = delimiterRange.location + 1;
350        }
351
352        ICCF_CheckRange(range);
353
354        // XXX instead of length of string, make this stop at the max URL length to prevent protracted searches
355        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., ...>)
356        extraLen = [s length] - range.location - range.length;
357        delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
358                                            options: NSLiteralSearch
359                                              range: NSMakeRange(range.location + range.length - (range.length != 0),
360                                                                 extraLen + (range.length != 0))];
361        if (delimiterRange.location == NSNotFound) {
362            // extend to end of string
363            range.length += extraLen;
364        } else {
365            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
366            range.length += delimiterRange.location - range.location - range.length;
367        }
368
369        ICCF_CheckRange(range);
370
371        ICCF_ParseURL([s substringWithRange: range], &range);
372
373        [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: NO];
374        [self display];
375
376        if (ICCF_LaunchURL([s substringWithRange: range], ICCF_KeyboardAction(triggeringEvent)) && ICCF_prefs.textBlinkEnabled) {
377            for (i = 0 ; i < ICCF_prefs.textBlinkCount ; i++) {
378                NSRange emptyRange = {range.location, 0};
379                [self setSelectedRange: emptyRange affinity: NSSelectionAffinityDownstream stillSelecting: YES];
380                [self display];
381                usleep(kICBlinkDelayUsecs);
382                [self setInsertionPointColor: [self backgroundColor]];
383                [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: YES];
384                [self display];
385                usleep(kICBlinkDelayUsecs);
386            }
387        }
388
389    NS_HANDLER
390        ICCF_HandleException(localException);
391    NS_ENDHANDLER
392
393    ICCF_StopIC();
394    [self setInsertionPointColor: insertionPointColor];
395}
396
397NSString * const ICCF_SERVICES_ITEM = @"ICeCoffEE Services Item";
398
399NSMenuItem *ICCF_ServicesMenuItem() {
400    NSMenuItem *servicesItem;
401    NSString *servicesTitle = nil;
402    NSMenu *servicesMenu = [NSApp servicesMenu];
403   
404    if (servicesMenu != nil) {
405        servicesTitle = [servicesMenu title];
406        if (servicesTitle == nil) {
407            ICLog(@"Can't get service menu title");
408            servicesTitle = @"Services";
409        }
410    } else {
411        servicesTitle = [[NSBundle bundleWithIdentifier: @"com.apple.AppKit"] localizedStringForKey: @"Services" value: nil table: @"ServicesMenu"];
412        if (servicesTitle == nil) {
413            ICLog(@"Can't get localized text for 'Services' in AppKit.framework");
414            servicesTitle = @"Services";
415        }
416    }
417    servicesMenu = [[NSMenu alloc] initWithTitle: servicesTitle];
418    servicesItem = [[NSMenuItem alloc] initWithTitle: servicesTitle action:nil keyEquivalent:@""];
419    ICCF_SetServicesMenu(servicesMenu);
420    [servicesItem setSubmenu: servicesMenu];
421    [servicesItem setRepresentedObject: ICCF_SERVICES_ITEM];
422    [servicesMenu release];
423    return [servicesItem autorelease];
424}
425
426static const unichar UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE = 0x25b8;
427
428// returns YES if menu contains useful items, NO otherwise
429BOOL ICCF_ConsolidateServicesMenu(NSMenu *menu, NSDictionary *serviceOptions) {
430    [menu update]; // doesn't propagate to submenus, so we need to do this first
431    NSEnumerator *enumerator = [[menu itemArray] objectEnumerator];
432    NSMenuItem *menuItem;
433    NSMenu *submenu;
434    NSDictionary *itemOptions = nil;
435    BOOL shouldKeepItem = NO, shouldKeepMenu = NO;
436
437    while ( (menuItem = [enumerator nextObject]) != nil) {
438        if (serviceOptions != nil)
439            itemOptions = [serviceOptions objectForKey: [menuItem title]];
440        if ([[itemOptions objectForKey: (NSString *)kICServiceHidden] boolValue]) {
441            shouldKeepItem = NO;
442        } else if ( (submenu = [menuItem submenu]) != nil) {
443            shouldKeepItem = ICCF_ConsolidateServicesMenu(submenu, [itemOptions objectForKey: (NSString *)kICServiceSubmenu]);
444            if (shouldKeepItem && [submenu numberOfItems] == 1) { // consolidate
445                NSMenuItem *serviceItem = [[submenu itemAtIndex: 0] retain];
446                [serviceItem setTitle:
447                    [NSString stringWithFormat: @"%@ %@ %@", [menuItem title], [NSString stringWithCharacters: &UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE length: 1], [serviceItem title]]];
448
449                int serviceIndex = [menu indexOfItem: menuItem];
450                [submenu removeItemAtIndex: 0]; // can't have item in two menus
451                [menu removeItemAtIndex: serviceIndex];
452                [menu insertItem: serviceItem atIndex: serviceIndex];
453                [serviceItem release];
454            }
455        } else {
456            [menuItem setKeyEquivalent: @""];
457            shouldKeepItem = [menuItem isEnabled];
458        }
459        if (shouldKeepItem) {
460            shouldKeepMenu = YES;
461        } else {
462            [menu removeItem: menuItem];
463        }
464    }
465
466    return shouldKeepMenu;
467}
468
469NSMenuItem *ICCF_ContextualServicesMenuItem() {
470    NSMenuItem *servicesItem = ICCF_ServicesMenuItem();
471    if (ICCF_ConsolidateServicesMenu([servicesItem submenu], (NSDictionary *)ICCF_prefs.serviceOptions))
472        return servicesItem;
473    else
474        return nil;
475}
476
477void ICCF_AddRemoveServicesMenu() {
478    // needed because:
479    // (a) we get called before the runloop has properly started and will crash if we don't delay on app startup
480    // (b) the APE message handler calls us from another thread and nothing happens if we try to add a menu on it
481    [ICeCoffEE performSelectorOnMainThread: @selector(IC_addRemoveServicesMenu) withObject: nil waitUntilDone: NO];
482}
483
484NSMenu *ICCF_MenuForEvent(NSView *self, NSMenu *contextMenu, NSEvent *e) {
485    if (contextMenu != nil && [e type] == NSRightMouseDown || ([e type] == NSLeftMouseDown && [e modifierFlags] & NSControlKeyMask)) {
486        int servicesItemIndex = [contextMenu indexOfItemWithRepresentedObject: ICCF_SERVICES_ITEM];
487        // always regenerate: make sure menu reflects context
488        if (servicesItemIndex != -1) {
489            [contextMenu removeItemAtIndex: servicesItemIndex];
490            [contextMenu removeItemAtIndex: servicesItemIndex - 1];
491        }
492        if (ICCF_prefs.servicesInContextualMenu) {
493            NSMenuItem *contextualServicesItem = ICCF_ContextualServicesMenuItem();
494            if (contextualServicesItem != nil) {
495                [contextMenu addItem: [NSMenuItem separatorItem]];
496                [contextMenu addItem: contextualServicesItem];
497            }
498        }
499    }
500    return contextMenu;
501}
502
503static NSEvent *ICCF_MouseDownEventWithModifierFlags(NSEvent *e, BOOL inheritModifierFlags) {
504    return [NSEvent mouseEventWithType: NSLeftMouseDown
505                              location: [e locationInWindow]
506                         modifierFlags: (inheritModifierFlags ? [e modifierFlags] : 0)
507                             timestamp: [e timestamp]
508                          windowNumber: [e windowNumber]
509                               context: [e context]
510                           eventNumber: [e eventNumber]
511                            clickCount: 1
512                              pressure: 0];
513}
514
515
516@interface NSTextView (IC_NSSharing)
517// only in Mac OS X 10.4 and later
518- (NSArray *)selectedRanges;
519@end
520
521@implementation ICeCoffEE
522
523+ (void)IC_addRemoveServicesMenu;
524{
525    NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
526    static NSMenuItem *servicesItem = nil;
527   
528    if (servicesItem == nil && ICCF_prefs.servicesInMenuBar) {
529        servicesItem = [ICCF_ServicesMenuItem() retain];
530
531        int insertLoc = [mainMenu indexOfItemWithSubmenu: [NSApp windowsMenu]];
532        if (insertLoc == -1)
533            insertLoc = [mainMenu numberOfItems];
534
535        [mainMenu insertItem: servicesItem atIndex: insertLoc];
536    } else if (servicesItem != nil && !ICCF_prefs.servicesInMenuBar) {
537        [mainMenu removeItem: servicesItem];
538        [servicesItem release];
539        servicesItem = nil;
540    }
541    if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_3) {
542        [[NSApp servicesMenu] update]; // enable keyboard equivalents in Mac OS X 10.3
543    }
544}
545
546// XXX localization?
547- (NSMenu *)menuForEvent:(NSEvent *)e;
548{
549    NSMenu *myMenu = [super menuForEvent: e];
550    return ICCF_MenuForEvent(self, myMenu, e);
551}
552
553- (void)mouseDown:(NSEvent *)e;
554{
555#if ICCF_DEBUG
556    static BOOL down = NO;
557    if (down) {
558        ICLog(@"recursive invocation!");
559        return;
560    }
561    down = YES;
562    ICLog(@"ICeCoffEE down: %@", e);
563#endif
564    if (ICCF_sharedTrigger != nil) {
565        ICLog(@"%@ cancelling", ICCF_sharedTrigger);
566        [ICCF_sharedTrigger cancel];
567    }
568    if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(e)) {
569        BOOL inheritModifierFlags;
570        if ([self respondsToSelector: @selector(selectedRanges)]) {
571            // Command-multiple-click or -drag for discontiguous selection, Mac OS X 10.4 or later
572            inheritModifierFlags = YES;
573        } else {
574            // don't want to trigger selection extension or anything else; pass through as a plain click
575            // (on Mac OS X 10.3, command does not modify behavior)
576            inheritModifierFlags = NO;
577        }
578        [super mouseDown: ICCF_MouseDownEventWithModifierFlags(e, inheritModifierFlags)];
579        // we don't actually get a mouseUp event, just wait for mouseDown to return
580        NSEvent *upEvent = [[self window] currentEvent];
581        NSPoint downPt = [e locationInWindow];
582        NSPoint upPt = [upEvent locationInWindow];
583        ICLog(@"next: %@", upEvent);
584        NSAssert([upEvent type] == NSLeftMouseUp, @"NSTextView mouseDown: did not return with current event as mouse up!");
585        if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
586            if (inheritModifierFlags) {
587                // Mac OS X 10.4 and later: make sure we don't have a command-double-click
588                [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
589                ICLog(@"%@ set", ICCF_sharedTrigger);
590            } else {
591                // Mac OS X 10.3
592                ICCF_LaunchURLFromTextView(self, e);
593            }
594        }
595    } else {
596        [super mouseDown: e];
597    }
598#if ICCF_DEBUG
599    down = NO;
600#endif
601}
602
603@end
Note: See TracBrowser for help on using the repository browser.