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

Last change on this file since 182 was 182, checked in by Nicholas Riley, 15 years ago

English.lproj/APEInfo.rtfd: Partial documentation update for 1.4.2;
fixed many instances of outdated and incorrect information.

ICeCoffEE.m: Removed completed "to do" comments - that's what
OmniOutliner? and Trac are for. Fixed delimiters to make more sense.
Redid ICCF_ParseURL() to make more sense and strip invalid characters
from beginning of URL. Added note about deprecated getCString:.
Fixed ICCF_ServicesMenuItem() to work on Tiger; moved menu population
logic (where services menu delegate used) to new
ICCF_SetServicesMenu() in ICeCoffEESetServicesMenu.[hm]. Remove key
equivalents from services in ICCF_ConsolidateServicesMenu(). First
pass at a workaround for discontiguous selection: only trigger if
there is no selection. This will be fixed to use a timer.

ICeCoffEEServicePrefController: Fixed service population to work on
Tiger, though keyboard equivalents are not provided; will need to
switch to parsing output of CFServiceControllerCopyServicesEntries()
for that one.

ICeCoffEEWebKit.m: Removed Safari 1.0-1.2 support. Fixed incorrect
comment about -selectionRect? only being in Safari
1.1-1.2.

ICeCoffEESetServicesMenu.[hm]: Handle getting a usable services menu
for Panther and Tiger.

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