// ICeCoffEE - Internet Config Carbon/Cocoa Editor Extension
// Nicholas Riley <mailto:icecoffee@sabi.net>

/* To do/think about:

- TXNClick - MLTE has its own (lousy) support in Jaguar, seems improved in Panther, good enough to leave?

*/

#import "ICeCoffEE.h"
#import <Carbon/Carbon.h>
#include <unistd.h>
#import "ICeCoffEESuper.h"
#import "ICeCoffEESetServicesMenu.h"
#import "ICeCoffEETrigger.h"

iccfPrefRec ICCF_prefs;

NSString *ICCF_ErrString(OSStatus err, NSString *context) {    
    if (err == noErr || err == userCanceledErr) return nil;

    NSString *errNum = [NSString stringWithFormat: @"%ld", err];
    NSString *errDesc = ICCF_LocalizedString(errNum);

    if (errDesc == NULL || errDesc == errNum)
        errDesc = [NSString stringWithFormat: ICCF_LocalizedString(@"An unknown error occurred in %@"), context];

    return [NSString stringWithFormat: @"%@ (%d)", errDesc, (int)err];
}

CFStringRef ICCF_CopyErrString(OSStatus err, CFStringRef context) {
    if (err == noErr || err == userCanceledErr) return NULL;

    CFStringRef errNum = CFStringCreateWithFormat(NULL, NULL, CFSTR("%ld"), err);
    CFStringRef errDesc = ICCF_CopyLocalizedString(errNum);

    if (errDesc == NULL || errDesc == errNum) {
        CFStringRef errDescFormat = ICCF_CopyLocalizedString(CFSTR("An unknown error occurred in %@"));
        if (errDesc != NULL) CFRelease(errDesc);
        errDesc = CFStringCreateWithFormat(NULL, NULL, errDescFormat, context);
    }

    CFStringRef errStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%d)"), errDesc, (int)err);

    if (errNum != NULL) CFRelease(errNum);
    if (errDesc != NULL) CFRelease(errDesc);
    return errStr;
}

CFStringRef ICCF_CopyAppName() {
    ProcessSerialNumber psn = {0, kCurrentProcess};
    CFStringRef appName = NULL;
    CopyProcessName(&psn, &appName);
    if (appName == NULL) return CFSTR("(unknown)");
    return appName;
}

BOOL ICCF_EventIsCommandMouseDown(NSEvent *e) {
    return ([e type] == NSLeftMouseDown && ([e modifierFlags] & NSCommandKeyMask) != 0 && [e clickCount] == 1);
}

iccfURLAction ICCF_KeyboardAction(NSEvent *e) {
    unsigned int modifierFlags = [e modifierFlags];
    iccfURLAction action;
    action.presentMenu = (modifierFlags & NSAlternateKeyMask) != 0;
    action.launchInBackground = (modifierFlags & NSShiftKeyMask) != 0;
    return action;
}

void ICCF_CheckRange(NSRange range) {
    NSCAssert(range.length > 0, ICCF_LocalizedString(@"No URL is selected"));
    NSCAssert1(range.length <= ICCF_MAX_URL_LEN, ICCF_LocalizedString(@"The potential URL is longer than %lu characters"), ICCF_MAX_URL_LEN);
}

void ICCF_Delimiters(NSCharacterSet **leftPtr, NSCharacterSet **rightPtr) {
    static NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;

    if (urlLeftDelimiters == nil || urlRightDelimiters == nil) {
        NSMutableCharacterSet *set = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
        NSMutableCharacterSet *tmpSet;
        [urlLeftDelimiters release];
        [urlRightDelimiters release];

        [set autorelease];
        [set formUnionWithCharacterSet: [[NSCharacterSet characterSetWithRange: NSMakeRange(0x21, 0x5e)] invertedSet]]; // nonprintable and non-ASCII characters
        [set formUnionWithCharacterSet: [NSCharacterSet punctuationCharacterSet]];
        [set removeCharactersInString: @";/?:@&=+$,-_.!~*'()%#"]; // RFC 2396 ¤2.2, 2.3, 2.4, plus % and # from "delims" set

        tmpSet = [[set mutableCopy] autorelease];
        [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><("]];
        urlLeftDelimiters = [tmpSet copy]; // make immutable again - for efficiency

        tmpSet = [[set mutableCopy] autorelease];
        [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><)"]];
        urlRightDelimiters = [tmpSet copy]; // make immutable again - for efficiency
    }

    *leftPtr = urlLeftDelimiters; *rightPtr = urlRightDelimiters;
}

static ICInstance ICCF_icInst = NULL;

void ICCF_StartIC() {
    OSStatus err;
    
    if (ICCF_icInst != NULL) {
        ICLog(@"ICCF_StartIC: Internet Config is already running!");
        ICCF_StopIC();
    }
    err = ICStart(&ICCF_icInst, kICCFCreator);
    NSCAssert1(err == noErr, ICCF_LocalizedString(@"Unable to start Internet Config (error %d)"), err);
}

void ICCF_StopIC() {
    if (ICCF_icInst == NULL) {
        ICLog(@"ICCF_StopIC: Internet Config is not running!");
    } else {
        ICStop(ICCF_icInst);
        ICCF_icInst = NULL;
    }
}

ICInstance ICCF_GetInst() {
    NSCAssert(ICCF_icInst != NULL, @"Internal error: Called ICCF_GetInst without ICCF_StartIC");
    return ICCF_icInst;
}

ConstStringPtr ICCF_GetHint(ICInstance inst, const char *urlData, Size length, long *selStart, long *selEnd, Boolean *needsSlashes) {
    Handle h = NewHandle(0);
    OSStatus err;

    if (h == NULL) return NULL;

    // parse the URL providing a bogus protocol, to get rid of escaped forms
    err = ICParseURL(inst, "\p*", urlData, length, selStart, selEnd, h);
    if (err != noErr) return NULL;

    // scan through the parsed URL looking for characters not found in email addresses
    Size hSize = GetHandleSize(h);
    if (hSize == 0) return NULL;

    const char *urlParsed = *h;
    long i = 0;
    Boolean sawAt = false;
    if (hSize >= 2 && urlParsed[0] == '*' && urlParsed[1] == ':') {
        // this is an IC-inserted protocol; skip over it
        i = 2;
        *needsSlashes = (hSize < i + 2 || urlParsed[i] != '/' || urlParsed[i + 1] != '/');
    } else *needsSlashes = false;
    for ( ; i < hSize ; i++) {
        char c = urlParsed[i];
        if (c == '@') {
            sawAt = true;
        } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
                     (c == '+' || c == '-' || c == '_' || c == '!' || c == '.'))) {
            DisposeHandle(h);
            return "\phttp";
        }
    }
    DisposeHandle(h);
    if (sawAt) {
        *needsSlashes = false;
        return "\pmailto";
    }
    return "\phttp";
}

static const char *kICSlashes = "//";

void ICCF_AddSlashes(Handle h, ConstStringPtr hint) {
    Size sizeBefore = GetHandleSize(h);
    unsigned char hintLength = StrLength(hint);
    char *copy = (char *)malloc(sizeBefore);
    memcpy(copy, *h, sizeBefore);
    ICLog(@"ICCF_AddSlashes before: |%s|\n", *h);
    ReallocateHandle(h, sizeBefore + 2);

    // if *h begins with '<hint>:', then copy the slashes after it
    if (sizeBefore > hintLength + 1 && strncmp((const char *)&hint[1], copy, hintLength) == 0 && copy[hintLength] == ':') {
        memcpy(*h, copy, hintLength + 1);
        memcpy(*h + hintLength + 1, kICSlashes, 2);
        memcpy(*h + hintLength + 3, &copy[hintLength + 1], sizeBefore - hintLength - 1);
    } else {
        memcpy(*h, kICSlashes, 2);
        memcpy(*h + 2, copy, sizeBefore);
    }
        
    free(copy);
    ICLog(@"ICCF_AddSlashes after: |%s|\n", *h);
}

// input/output 'range' is the range of source document which contains 'string'
void ICCF_ParseURL(NSString *string, NSRange *range) {
    OSStatus err;
    Handle h;
    long selStart = 0, selEnd = range->length; // local offsets within 'string'
    char *urlData = NULL;

    NSCAssert(selEnd == [string length], @"Internal error: URL string is wrong length");
    
    NS_DURING
        if ([[NSCharacterSet characterSetWithCharactersInString: @";,."] characterIsMember:
            [string characterAtIndex: selEnd - 1]]) {
            selEnd--;
        }
	NSCharacterSet *alphanumericCharacterSet = [NSCharacterSet alphanumericCharacterSet];
	while (![alphanumericCharacterSet characterIsMember: [string characterAtIndex: selStart]]) {
	    selStart++;
	    NSCAssert(selStart < selEnd, @"No URL is selected");
	}

        string = [string substringWithRange: NSMakeRange(selStart, selEnd - selStart)];

        ICLog(@"Parsing URL |%@|", string);

        NSCAssert([string canBeConvertedToEncoding: NSASCIIStringEncoding], @"No URL is selected");

        urlData = (char *)malloc( (range->length + 1) * sizeof(char));
        NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string");

	// 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.
        [string getCString: urlData];

        h = NewHandle(0);
        NSCAssert(h != NULL, @"Internal error: can't allocate URL handle");

        err = ICParseURL(ICCF_GetInst(), "\pmailto", urlData, range->length, &selStart, &selEnd, h);
        DisposeHandle(h);

        ICCF_OSErrCAssert(err, @"ICParseURL");
    
        range->length = selEnd - selStart;
        range->location += selStart;
    NS_HANDLER
        free(urlData);
        [localException raise];
    NS_ENDHANDLER
    
    free(urlData);
}

BOOL ICCF_LaunchURL(NSString *string, iccfURLAction action) {
    OSStatus err = noErr;
    long selStart, selEnd;
    unsigned len = [string length];

    Handle h = NULL;
    
    NS_DURING
        h = NewHandle(len);
        if (h == NULL)
            ICCF_OSErrCAssert(MemError(), @"NewHandle");

        if (CFStringGetBytes((CFStringRef)string, CFRangeMake(0, len), kCFStringEncodingASCII, '\0', false, (UInt8 *)*h, len, NULL) != len)
            ICCF_OSErrCAssert(kTECNoConversionPathErr, @"CFStringGetBytes");

        selStart = 0; selEnd = len;

        Boolean needsSlashes;
        ConstStringPtr hint = ICCF_GetHint(ICCF_GetInst(), *h, len, &selStart, &selEnd, &needsSlashes);
        NSCAssert(hint != NULL, @"Internal error: can't get protocol hint for URL");

        if (needsSlashes) {
            ICCF_AddSlashes(h, hint);
            len = selEnd = GetHandleSize(h);
        }

        err = ICCF_DoURLAction(ICCF_GetInst(), hint, *h, selStart, selEnd, action);
        ICCF_OSErrCAssert(err, @"ICCF_DoURLAction");
        
    NS_HANDLER
        DisposeHandle(h);
        [localException raise];
    NS_ENDHANDLER

    DisposeHandle(h);

    return (err == noErr);
}

// XXX not sure what to do if there's already a selection; BBEdit and MLTE extend it, Tex-Edit Plus doesn't.
// RFC-ordained max URL length, just to avoid passing IC/LS multi-megabyte documents
#if ICCF_DEBUG
const long ICCF_MAX_URL_LEN = 60; // XXX change later
#else
const long ICCF_MAX_URL_LEN = 1024;
#endif

Boolean ICCF_enabled = true;

BOOL ICCF_HandleException(NSException *e) {
    if ([e reason] == nil || [[e reason] length] == 0)
        return NO;
    
    if (ICCF_prefs.errorSoundEnabled) NSBeep();
    if (!ICCF_prefs.errorDialogEnabled) return YES;
    
    int result = NSRunAlertPanel(ICCF_LocalizedString(@"AlertTitle"), ICCF_LocalizedString(@"AlertMessage%@"), nil, nil, ICCF_LocalizedString(@"AlertDisableButton"), e);
    if (result != NSAlertDefaultReturn) {
        result = NSRunAlertPanel(ICCF_LocalizedString(@"DisableAlertTitle"), ICCF_LocalizedString(@"DisableAlertMessage%@"), ICCF_LocalizedString(@"DisableAlertDisableButton"), ICCF_LocalizedString(@"DisableAlertDontDisableButton"), nil,
           [(NSString *)ICCF_CopyAppName() autorelease]);
        if (result == NSAlertDefaultReturn)
            ICCF_enabled = NO;
    }
    return YES;
}

void ICCF_LaunchURLFromTextView(NSTextView *self, NSEvent *triggeringEvent) {
    NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
    NSRange range = [self selectedRange], delimiterRange;
    NSColor *insertionPointColor = [self insertionPointColor];
    NSString *s = [[self textStorage] string]; // according to the class documentation, sending 'string' is guaranteed to be O(1)
    unsigned extraLen;
    int i;

    NS_DURING

        NSCAssert(range.location != NSNotFound, ICCF_LocalizedString(@"There is no insertion point or selection in the text field where you clicked"));
        NSCAssert(s != nil, ICCF_LocalizedString(@"Sorry, ICeCoffEE is unable to locate the insertion point or selection"));

        ICCF_StartIC();

        NSCAssert([s length] != 0, ICCF_LocalizedString(@"No text was found"));

        if (range.location == [s length]) range.location--; // work around bug in selectionRangeForProposedRange (r. 2845418)

        range = [self selectionRangeForProposedRange: range granularity: NSSelectByWord];

        // However, NSSelectByWord does not capture even the approximate boundaries of a URL
        // (text to a space/line ending character); it'll stop at a period in the middle of a hostname.
        // So, we expand it as follows:

        ICCF_CheckRange(range);

        ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);

        // XXX instead of 0, make this stop at the max URL length to prevent protracted searches
        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...)
        delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
                                            options: NSLiteralSearch | NSBackwardsSearch
                                              range: NSMakeRange(0, range.location + (range.location != [s length]))];
        if (delimiterRange.location == NSNotFound) {
            // extend to beginning of string
            range.length += range.location;
            range.location = 0;
        } else {
            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
            range.length += range.location - delimiterRange.location - 1;
            range.location = delimiterRange.location + 1;
        }

        ICCF_CheckRange(range);

        // XXX instead of length of string, make this stop at the max URL length to prevent protracted searches
        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., ...>)
        extraLen = [s length] - range.location - range.length;
        delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
                                            options: NSLiteralSearch
                                              range: NSMakeRange(range.location + range.length - (range.length != 0),
                                                                 extraLen + (range.length != 0))];
        if (delimiterRange.location == NSNotFound) {
            // extend to end of string
            range.length += extraLen;
        } else {
            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
            range.length += delimiterRange.location - range.location - range.length;
        }

        ICCF_CheckRange(range);

        ICCF_ParseURL([s substringWithRange: range], &range);

        [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: NO];
        [self display];

        if (ICCF_LaunchURL([s substringWithRange: range], ICCF_KeyboardAction(triggeringEvent)) && ICCF_prefs.textBlinkEnabled) {
            for (i = 0 ; i < ICCF_prefs.textBlinkCount ; i++) {
                NSRange emptyRange = {range.location, 0};
                [self setSelectedRange: emptyRange affinity: NSSelectionAffinityDownstream stillSelecting: YES];
                [self display];
                usleep(kICBlinkDelayUsecs);
                [self setInsertionPointColor: [self backgroundColor]];
                [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: YES];
                [self display];
                usleep(kICBlinkDelayUsecs);
            }
        }

    NS_HANDLER
        ICCF_HandleException(localException);
    NS_ENDHANDLER

    ICCF_StopIC();
    [self setInsertionPointColor: insertionPointColor];
}

NSString * const ICCF_SERVICES_ITEM = @"ICeCoffEE Services Item";

NSMenuItem *ICCF_ServicesMenuItem() {
    NSMenuItem *servicesItem;
    NSString *servicesTitle = nil;
    NSMenu *servicesMenu = [NSApp servicesMenu];
    
    if (servicesMenu != nil) {
	servicesTitle = [servicesMenu title];
	if (servicesTitle == nil) {
	    ICLog(@"Can't get service menu title");
	    servicesTitle = @"Services";
	}
    } else {
	servicesTitle = [[NSBundle bundleWithIdentifier: @"com.apple.AppKit"] localizedStringForKey: @"Services" value: nil table: @"ServicesMenu"];
	if (servicesTitle == nil) {
	    ICLog(@"Can't get localized text for 'Services' in AppKit.framework");
	    servicesTitle = @"Services";
	}
    }
    servicesMenu = [[NSMenu alloc] initWithTitle: servicesTitle];
    servicesItem = [[NSMenuItem alloc] initWithTitle: servicesTitle action:nil keyEquivalent:@""];
    ICCF_SetServicesMenu(servicesMenu);
    [servicesItem setSubmenu: servicesMenu];
    [servicesItem setRepresentedObject: ICCF_SERVICES_ITEM];
    [servicesMenu release];
    return [servicesItem autorelease];
}

static const unichar UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE = 0x25b8;

// returns YES if menu contains useful items, NO otherwise
BOOL ICCF_ConsolidateServicesMenu(NSMenu *menu, NSDictionary *serviceOptions) {
    [menu update]; // doesn't propagate to submenus, so we need to do this first
    NSEnumerator *enumerator = [[menu itemArray] objectEnumerator];
    NSMenuItem *menuItem;
    NSMenu *submenu;
    NSDictionary *itemOptions = nil;
    BOOL shouldKeepItem = NO, shouldKeepMenu = NO;

    while ( (menuItem = [enumerator nextObject]) != nil) {
        if (serviceOptions != nil)
            itemOptions = [serviceOptions objectForKey: [menuItem title]];
        if ([[itemOptions objectForKey: (NSString *)kICServiceHidden] boolValue]) {
            shouldKeepItem = NO;
        } else if ( (submenu = [menuItem submenu]) != nil) {
            shouldKeepItem = ICCF_ConsolidateServicesMenu(submenu, [itemOptions objectForKey: (NSString *)kICServiceSubmenu]);
            if (shouldKeepItem && [submenu numberOfItems] == 1) { // consolidate
                NSMenuItem *serviceItem = [[submenu itemAtIndex: 0] retain];
                [serviceItem setTitle:
                    [NSString stringWithFormat: @"%@ %@ %@", [menuItem title], [NSString stringWithCharacters: &UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE length: 1], [serviceItem title]]];

                int serviceIndex = [menu indexOfItem: menuItem];
                [submenu removeItemAtIndex: 0]; // can't have item in two menus
                [menu removeItemAtIndex: serviceIndex];
                [menu insertItem: serviceItem atIndex: serviceIndex];
                [serviceItem release];
            }
        } else {
	    [menuItem setKeyEquivalent: @""];
            shouldKeepItem = [menuItem isEnabled];
        }
        if (shouldKeepItem) {
            shouldKeepMenu = YES;
        } else {
            [menu removeItem: menuItem];
        }
    }

    return shouldKeepMenu;
}

NSMenuItem *ICCF_ContextualServicesMenuItem() {
    NSMenuItem *servicesItem = ICCF_ServicesMenuItem();
    if (ICCF_ConsolidateServicesMenu([servicesItem submenu], (NSDictionary *)ICCF_prefs.serviceOptions))
        return servicesItem;
    else
        return nil;
}

void ICCF_AddRemoveServicesMenu() {
    // needed because:
    // (a) we get called before the runloop has properly started and will crash if we don't delay on app startup
    // (b) the APE message handler calls us from another thread and nothing happens if we try to add a menu on it
    [ICeCoffEE performSelectorOnMainThread: @selector(IC_addRemoveServicesMenu) withObject: nil waitUntilDone: NO];
}

NSMenu *ICCF_MenuForEvent(NSView *self, NSMenu *contextMenu, NSEvent *e) {
    if (contextMenu != nil && [e type] == NSRightMouseDown || ([e type] == NSLeftMouseDown && [e modifierFlags] & NSControlKeyMask)) {
        int servicesItemIndex = [contextMenu indexOfItemWithRepresentedObject: ICCF_SERVICES_ITEM];
        // always regenerate: make sure menu reflects context
        if (servicesItemIndex != -1) {
            [contextMenu removeItemAtIndex: servicesItemIndex];
            [contextMenu removeItemAtIndex: servicesItemIndex - 1];
        }
        if (ICCF_prefs.servicesInContextualMenu) {
            NSMenuItem *contextualServicesItem = ICCF_ContextualServicesMenuItem();
            if (contextualServicesItem != nil) {
                [contextMenu addItem: [NSMenuItem separatorItem]];
                [contextMenu addItem: contextualServicesItem];
            }
        }
    }
    return contextMenu;
}

static NSEvent *ICCF_MouseDownEventWithModifierFlags(NSEvent *e, BOOL inheritModifierFlags) {
    return [NSEvent mouseEventWithType: NSLeftMouseDown
			      location: [e locationInWindow]
			 modifierFlags: (inheritModifierFlags ? [e modifierFlags] : 0)
			     timestamp: [e timestamp]
			  windowNumber: [e windowNumber]
			       context: [e context]
			   eventNumber: [e eventNumber]
			    clickCount: 1
			      pressure: 0];
}


@interface NSTextView (IC_NSSharing)
// only in Mac OS X 10.4 and later
- (NSArray *)selectedRanges;
@end

@implementation ICeCoffEE

+ (void)IC_addRemoveServicesMenu;
{
    NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
    static NSMenuItem *servicesItem = nil;
    
    if (servicesItem == nil && ICCF_prefs.servicesInMenuBar) {
        servicesItem = [ICCF_ServicesMenuItem() retain];

        int insertLoc = [mainMenu indexOfItemWithSubmenu: [NSApp windowsMenu]];
        if (insertLoc == -1)
            insertLoc = [mainMenu numberOfItems];

        [mainMenu insertItem: servicesItem atIndex: insertLoc];
    } else if (servicesItem != nil && !ICCF_prefs.servicesInMenuBar) {
        [mainMenu removeItem: servicesItem];
        [servicesItem release];
        servicesItem = nil;
    }
    if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_3) {
	[[NSApp servicesMenu] update]; // enable keyboard equivalents in Mac OS X 10.3
    }
}

// XXX localization?
- (NSMenu *)menuForEvent:(NSEvent *)e;
{
    NSMenu *myMenu = [super menuForEvent: e];
    return ICCF_MenuForEvent(self, myMenu, e);
}

- (void)mouseDown:(NSEvent *)e;
{
#if ICCF_DEBUG
    static BOOL down = NO;
    if (down) {
        ICLog(@"recursive invocation!");
        return;
    }
    down = YES;
    ICLog(@"ICeCoffEE down: %@", e);
#endif
    if (ICCF_sharedTrigger != nil) {
	ICLog(@"%@ cancelling", ICCF_sharedTrigger);
	[ICCF_sharedTrigger cancel];
    }
    if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(e)) {
	BOOL inheritModifierFlags;
	if ([self respondsToSelector: @selector(selectedRanges)]) {
	    // Command-multiple-click or -drag for discontiguous selection, Mac OS X 10.4 or later
	    inheritModifierFlags = YES;
	} else {
	    // don't want to trigger selection extension or anything else; pass through as a plain click
	    // (on Mac OS X 10.3, command does not modify behavior)
	    inheritModifierFlags = NO;
	}
	[super mouseDown: ICCF_MouseDownEventWithModifierFlags(e, inheritModifierFlags)];
        // we don't actually get a mouseUp event, just wait for mouseDown to return
        NSEvent *upEvent = [[self window] currentEvent];
        NSPoint downPt = [e locationInWindow];
        NSPoint upPt = [upEvent locationInWindow];
        ICLog(@"next: %@", upEvent);
        NSAssert([upEvent type] == NSLeftMouseUp, @"NSTextView mouseDown: did not return with current event as mouse up!");
        if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
	    if (inheritModifierFlags) {
		// Mac OS X 10.4 and later: make sure we don't have a command-double-click
		[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
		ICLog(@"%@ set", ICCF_sharedTrigger);
	    } else {
		// Mac OS X 10.3
		ICCF_LaunchURLFromTextView(self, e);
	    }
        }
    } else {
        [super mouseDown: e];
    }
#if ICCF_DEBUG
    down = NO;
#endif
}

@end
