Index: trunk/ICeCoffEE/ICeCoffEE/ICeCoffEE.m
===================================================================
--- trunk/ICeCoffEE/ICeCoffEE/ICeCoffEE.m	(revision 66)
+++ trunk/ICeCoffEE/ICeCoffEE/ICeCoffEE.m	(revision 74)
@@ -6,21 +6,17 @@
 - Carbon contextual menu plugin which presents Services (yah!)
   for both files and text
-- Carbon version
-- app exclusion list - make a pref pane (see AquaShade config)
 - if it's not a URL, try using TextExtras' open list
 - John Hayes' suggestions
-- ICeCoffEE 2 functionality (bookmark helper app for cmd-option-click)
-- adjust URL blinking
-
-Carbon support ideas:
-    TEClick - TextEdit
-    TXNClick - MLTE
-    ? ATSUI
-    WASTE has its own support
+- _LSCopyApplicationURLsForItemURL - list apps
+- Menu on command-option-click: add bookmark, open with other helper, pass to configurable service, ...?
+- TXNClick - MLTE has its own support in Jaguar and later, but it's lousy
 
 Done:
 
+- TEClick - TextEdit
 - flash on success (like BBEdit)
 - display dialog on failure (decode OSStatus)
+- adjust URL blinking
+- app exclusion list - make a pref pane (see AquaShade config)
 
 */
@@ -30,41 +26,54 @@
 #include <unistd.h>
 #import "ICeCoffEESuper.h"
-
-@implementation ICeCoffEE
-
-typedef struct {
-    OSStatus status;
-    NSString * const desc;
-} errRec, errList[];
-
-static errList ERRS = {
-    // Internet Config errors
-    { icPrefNotFoundErr, @"No helper application is defined for the selected URLÕs scheme (e.g. http:)" },
-    { icNoURLErr, @"The selection is not a URL" },
-    { icInternalErr, @"Internet Config experienced an internal error" },
-    // Misc. errors
-    { paramErr, @"The selection is not a complete URL" },
-    { 0, NULL }
-};
-
-NSString *ICCF_ErrString(OSStatus err, NSString *context) {
-    errRec *rec;
-    NSString *errDesc = [NSString stringWithFormat: @"An unknown error occurred in %@", context];
-    if (err == noErr) return nil;
-    for (rec = &(ERRS[0]) ; rec->status != 0 ; rec++)
-        if (rec->status == err) {
-            errDesc = rec->desc;
-            break;
-        }
+#import "ICeCoffEEAppMenu.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;
+}
+
 BOOL ICCF_EventIsCommandMouseDown(NSEvent *e) {
-    return ([e type] == NSLeftMouseDown && [e modifierFlags] == NSCommandKeyMask && [e clickCount] == 1);
+    unsigned int modifierFlags = [e modifierFlags];
+    return ([e type] == NSLeftMouseDown &&
+            (modifierFlags == NSCommandKeyMask || modifierFlags == (NSCommandKeyMask | NSAlternateKeyMask))
+            && [e clickCount] == 1);
+}
+
+BOOL ICCF_OptionKeyIsDown() {
+    unsigned int modifierFlags = [[NSApp currentEvent] modifierFlags];
+    return (modifierFlags & NSAlternateKeyMask) != 0;
 }
 
 void ICCF_CheckRange(NSRange range) {
-    NSCAssert(range.length > 0, @"No URL is selected");
-    NSCAssert1(range.length <= ICCF_MAX_URL_LEN, @"The potential URL is longer than %ld characters", ICCF_MAX_URL_LEN);
+    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);
 }
 
@@ -103,6 +112,6 @@
         ICCF_StopIC();
     }
-    err = ICStart(&ICCF_icInst, 'ICCF');
-    NSCAssert1(err == noErr, @"Unable to start Internet Config (error %d)", err);
+    err = ICStart(&ICCF_icInst, kICCFCreator);
+    NSCAssert1(err == noErr, ICCF_LocalizedString(@"Unable to start Internet Config (error %d)"), err);
 }
 
@@ -121,4 +130,41 @@
 }
 
+ConstStringPtr ICCF_GetHint(ICInstance inst, const char *urlData, long startIndex, long endIndex) {
+    Handle h = NewHandle(0);
+    OSStatus err;
+    long tmpStartIndex = startIndex, tmpEndIndex = endIndex;
+    
+    if (h == NULL) return NULL;
+
+    // parse the URL providing a bogus protocol, to get rid of escaped forms
+    err = ICParseURL(inst, "\p*", urlData + startIndex, endIndex - startIndex + 1,
+                     &tmpStartIndex, &tmpEndIndex, 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 (urlParsed[0] == '*' && urlParsed[1] == ':') {
+        // this is an IC-inserted protocol; skip over it
+        i = 2;
+    }
+    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);
+    return (sawAt ? "\pmailto" : "\phttp");
+}
+
 void ICCF_ParseURL(NSString *string, NSRange *range) {
     OSStatus err;
@@ -140,5 +186,5 @@
 
         urlData = (char *)malloc( (range->length + 1) * sizeof(char));
-        NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string");
+        NSCAssert(urlData != NULL, @"Internal error: canÕt allocate memory for URL string");
 
         selStart = 0; selEnd = range->length;
@@ -147,11 +193,12 @@
 
         h = NewHandle(0);
-        NSCAssert(h != NULL, @"Internal error: can't allocate URL handle");
-    
+        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");
     
-        DisposeHandle(h);
-        range->length = range->length - (range->length - selEnd) + selStart;
+        range->length = selEnd - selStart;
         range->location += selStart;
     NS_HANDLER
@@ -163,5 +210,5 @@
 }
 
-void ICCF_LaunchURL(NSString *string) {
+void ICCF_LaunchURL(NSString *string, BOOL chooseApp) {
     OSStatus err;
     long selStart, selEnd;
@@ -172,5 +219,5 @@
     NS_DURING
         urlData = (char *)malloc( (len + 1) * sizeof(char));
-        NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string");
+        NSCAssert(urlData != NULL, @"Internal error: canÕt allocate memory for URL string");
 
         [string getCString: urlData];
@@ -178,6 +225,14 @@
         selStart = 0; selEnd = len;
 
-        err = ICLaunchURL(ICCF_GetInst(), "\pmailto", urlData, len, &selStart, &selEnd);
-        ICCF_OSErrCAssert(err, @"ICLaunchURL");
+        ConstStringPtr hint = ICCF_GetHint(ICCF_GetInst(), urlData, selStart, selEnd);
+        NSCAssert(hint != NULL, @"Internal error: canÕt get protocol hint for URL");
+
+        if (chooseApp) {
+            err = ICCF_LaunchURLWithApplication(ICCF_GetInst(), hint, urlData, selStart, selEnd);
+            ICCF_OSErrCAssert(err, @"ICCF_LaunchURLWithApplication");
+        } else {
+            err = ICLaunchURL(ICCF_GetInst(), hint, urlData, len, &selStart, &selEnd);
+            ICCF_OSErrCAssert(err, @"ICLaunchURL");
+        }
         
     NS_HANDLER
@@ -189,5 +244,5 @@
 }
 
-// XXX not sure what to do if there's already a selection; BBEdit extends it, Tex-Edit Plus doesn't.
+// 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 multi-megabyte documents
 #if ICCF_DEBUG
@@ -197,13 +252,20 @@
 #endif
 
-BOOL ICCF_enabled = YES;
-
-void ICCF_HandleException(NSException *e) {
-    int result = NSRunAlertPanel(@"Open Internet Location", @"The selected Internet location could not be opened.\n\n%@.", @"OK", nil, @"Disable ICeCoffEEÉ", e);
+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(@"Disable ICeCoffEE", @"If you believe ICeCoffEE is interfering with the normal functioning of this application, you can turn it off in this application until the application has quit.\n\nIf this is the first time you are experiencing this problem, please email icecoffee@sabi.net with the details of the conflict.\n\nTo remove ICeCoffEE permanently, drag its icon to the Trash or use the ICeCoffEE Installer.", @"Disable", @"DonÕt Disable", nil);
+        result = NSRunAlertPanel(ICCF_LocalizedString(@"DisableAlertTitle"), ICCF_LocalizedString(@"DisableAlertMessage"), ICCF_LocalizedString(@"DisableAlertDisableButton"), ICCF_LocalizedString(@"DisableAlertDontDisableButton"), nil);
         if (result == NSAlertDefaultReturn)
             ICCF_enabled = NO;
     }
+    return YES;
 }
 
@@ -218,10 +280,10 @@
     NS_DURING
 
-        NSCAssert(range.location != NSNotFound, @"There is no insertion point or selection in the text field you clicked");
-        NSCAssert(s != nil, @"Sorry, ICeCoffEE is unable to locate the insertion point or selection");
+        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, @"No text was found");
+        NSCAssert([s length] != 0, ICCF_LocalizedString(@"No text was found"));
 
         if (range.location == [s length]) range.location--; // work around bug in selectionRangeForProposedRange (r. 2845418)
@@ -273,18 +335,21 @@
         ICCF_ParseURL([s substringWithRange: range], &range);
 
-        for (i = 0 ; i < 3 ; i++) {
-            NSRange emptyRange = {range.location, 0};
-            [self setInsertionPointColor: [self backgroundColor]];
-            [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: YES];
-            [self display];
-            usleep(60000);
-            [self setSelectedRange: emptyRange affinity: NSSelectionAffinityDownstream stillSelecting: YES];
-            [self display];
-            usleep(60000);
-        }
         [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: NO];
         [self display];
 
-        ICCF_LaunchURL([s substringWithRange: range]);
+        ICCF_LaunchURL([s substringWithRange: range], ICCF_OptionKeyIsDown());
+
+        if (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
@@ -316,6 +381,9 @@
 }
 
-void ICCF_AddServicesMenu() {
-    [ICeCoffEE performSelector: @selector(IC_addServicesMenu) withObject: nil afterDelay: 0.0];
+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];
 }
 
@@ -323,11 +391,16 @@
     if (contextMenu != nil && [e type] == NSRightMouseDown || ([e type] == NSLeftMouseDown && [e modifierFlags] & NSControlKeyMask)) {
         int servicesItemIndex = [contextMenu indexOfItemWithRepresentedObject: ICCF_SERVICES_ITEM];
-        if (servicesItemIndex == -1) {
+        if (servicesItemIndex == -1 && ICCF_prefs.servicesInContextualMenu) {
             [contextMenu addItem: [NSMenuItem separatorItem]];
             [contextMenu addItem: ICCF_ServicesMenuItem()];
+        } else if (servicesItemIndex != -1 && !ICCF_prefs.servicesInContextualMenu) {
+            [contextMenu removeItemAtIndex: servicesItemIndex];
+            [contextMenu removeItemAtIndex: servicesItemIndex - 1];
         }
     }
     return contextMenu;
 }
+
+@implementation ICeCoffEE
 
 + (NSString *)IC_version;
@@ -337,13 +410,22 @@
 }
 
-+ (void)IC_addServicesMenu;
++ (void)IC_addRemoveServicesMenu;
 {
     NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
-    int insertLoc = [mainMenu indexOfItemWithSubmenu: [NSApp windowsMenu]];
-
-    if (insertLoc == -1)
-        insertLoc = [mainMenu numberOfItems];
-
-    [mainMenu insertItem: ICCF_ServicesMenuItem() atIndex: insertLoc];
+    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;
+    }
 }
 
@@ -365,10 +447,14 @@
     down = YES;
     ICLog(@"ICeCoffEE down: %@", e);
-    NSLog(@"super is %@", self);
 #endif
     // we don't actually get a mouseUp event, just wait for mouseDown to return
     [super mouseDown: e];
-    if (!ICCF_enabled) return;
-    // don't want command-option-click, command-shift-click, etc. to trigger
+    if (!ICCF_enabled || !ICCF_prefs.commandClickEnabled) {
+#if ICCF_DEBUG
+        down = NO;
+#endif
+        return;
+    }
+    // don't want command-shift-click, etc. to trigger
     if (ICCF_EventIsCommandMouseDown(e)) {
         NSEvent *upEvent = [[self window] currentEvent];
@@ -377,5 +463,5 @@
         ICLog(@"next: %@", upEvent);
         NSAssert([upEvent type] == NSLeftMouseUp, @"NSTextView mouseDown: did not return with current event as mouse up!");
-        if (abs(downPt.x - upPt.x) <= 4 && abs(downPt.y - upPt.y) <= 4) {
+        if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
             ICCF_LaunchURLFromTextView(self);
         }
