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

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

Initial import.

File size: 14.2 KB
Line 
1// ICeCoffEE - Internet Config Cocoa Editor Extension
2// Nicholas Riley <mailto:icecoffee@sabi.net>
3
4/* To do/think about:
5
6- Carbon contextual menu plugin which presents Services (yah!)
7  for both files and text
8- Carbon version
9- app exclusion list - make a pref pane (see AquaShade config)
10- if it's not a URL, try using TextExtras' open list
11- John Hayes' suggestions
12- ICeCoffEE 2 functionality (bookmark helper app for cmd-option-click)
13- adjust URL blinking
14
15Carbon support ideas:
16    TEClick - TextEdit
17    TXNClick - MLTE
18    ? ATSUI
19    WASTE has its own support
20
21Done:
22
23- flash on success (like BBEdit)
24- display dialog on failure (decode OSStatus)
25
26*/
27
28#import "ICeCoffEE.h"
29#import <Carbon/Carbon.h>
30#include <unistd.h>
31#import "ICeCoffEESuper.h"
32
33@implementation ICeCoffEE
34
35typedef struct {
36    OSStatus status;
37    NSString * const desc;
38} errRec, errList[];
39
40static errList ERRS = {
41    // Internet Config errors
42    { icPrefNotFoundErr, @"No helper application is defined for the selected URLÕs scheme (e.g. http:)" },
43    { icNoURLErr, @"The selection is not a URL" },
44    { icInternalErr, @"Internet Config experienced an internal error" },
45    // Misc. errors
46    { paramErr, @"The selection is not a complete URL" },
47    { 0, NULL }
48};
49
50NSString *ICCF_ErrString(OSStatus err, NSString *context) {
51    errRec *rec;
52    NSString *errDesc = [NSString stringWithFormat: @"An unknown error occurred in %@", context];
53    if (err == noErr) return nil;
54    for (rec = &(ERRS[0]) ; rec->status != 0 ; rec++)
55        if (rec->status == err) {
56            errDesc = rec->desc;
57            break;
58        }
59    return [NSString stringWithFormat: @"%@ (%d)", errDesc, (int)err];
60}
61
62BOOL ICCF_EventIsCommandMouseDown(NSEvent *e) {
63    return ([e type] == NSLeftMouseDown && [e modifierFlags] == NSCommandKeyMask && [e clickCount] == 1);
64}
65
66void ICCF_CheckRange(NSRange range) {
67    NSCAssert(range.length > 0, @"No URL is selected");
68    NSCAssert1(range.length <= ICCF_MAX_URL_LEN, @"The potential URL is longer than %ld characters", ICCF_MAX_URL_LEN);
69}
70
71void ICCF_Delimiters(NSCharacterSet **leftPtr, NSCharacterSet **rightPtr) {
72    static NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
73
74    if (urlLeftDelimiters == nil || urlRightDelimiters == nil) {
75        NSMutableCharacterSet *set = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
76        NSMutableCharacterSet *tmpSet;
77        [urlLeftDelimiters release];
78        [urlRightDelimiters release];
79
80        [set autorelease];
81        [set formUnionWithCharacterSet: [NSCharacterSet punctuationCharacterSet]];
82        [set removeCharactersInString: @";/?:@&=+$,-_.!~*'()%#"]; // RFC 2396 ¤2.2, 2.3, 2.4, plus #
83
84        tmpSet = [[set mutableCopy] autorelease];
85        [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><("]];
86        urlLeftDelimiters = [tmpSet copy]; // make immutable again - for efficiency
87
88        tmpSet = [[set mutableCopy] autorelease];
89        [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><)"]];
90        urlRightDelimiters = [tmpSet copy]; // make immutable again - for efficiency
91    }
92
93    *leftPtr = urlLeftDelimiters; *rightPtr = urlRightDelimiters;
94}
95
96static ICInstance ICCF_icInst = NULL;
97
98void ICCF_StartIC() {
99    OSStatus err;
100   
101    if (ICCF_icInst != NULL) {
102        ICLog(@"ICCF_StartIC: Internet Config is already running!");
103        ICCF_StopIC();
104    }
105    err = ICStart(&ICCF_icInst, 'ICCF');
106    NSCAssert1(err == noErr, @"Unable to start Internet Config (error %d)", err);
107}
108
109void ICCF_StopIC() {
110    if (ICCF_icInst == NULL) {
111        ICLog(@"ICCF_StopIC: Internet Config is not running!");
112    } else {
113        ICStop(ICCF_icInst);
114        ICCF_icInst = NULL;
115    }
116}
117
118ICInstance ICCF_GetInst() {
119    NSCAssert(ICCF_icInst != NULL, @"Internal error: Called ICCF_GetInst without ICCF_StartIC");
120    return ICCF_icInst;
121}
122
123void ICCF_ParseURL(NSString *string, NSRange *range) {
124    OSStatus err;
125    Handle h;
126    long selStart, selEnd;
127    char *urlData = NULL;
128
129    NSCAssert(range->length == [string length], @"Internal error: URL string is wrong length");
130   
131    NS_DURING
132        if ([[NSCharacterSet characterSetWithCharactersInString: @";,."] characterIsMember:
133            [string characterAtIndex: range->length - 1]]) {
134            range->length--;
135        }
136
137        string = [string substringToIndex: range->length];
138
139        ICLog(@"Parsing URL |%@|", string);
140
141        urlData = (char *)malloc( (range->length + 1) * sizeof(char));
142        NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string");
143
144        selStart = 0; selEnd = range->length;
145
146        [string getCString: urlData];
147
148        h = NewHandle(0);
149        NSCAssert(h != NULL, @"Internal error: can't allocate URL handle");
150   
151        err = ICParseURL(ICCF_GetInst(), "\pmailto", urlData, range->length, &selStart, &selEnd, h);
152        ICCF_OSErrCAssert(err, @"ICParseURL");
153   
154        DisposeHandle(h);
155        range->length = range->length - (range->length - selEnd) + selStart;
156        range->location += selStart;
157    NS_HANDLER
158        free(urlData);
159        [localException raise];
160    NS_ENDHANDLER
161   
162    free(urlData);
163}
164
165void ICCF_LaunchURL(NSString *string) {
166    OSStatus err;
167    long selStart, selEnd;
168    unsigned len = [string length];
169
170    char *urlData = NULL;
171   
172    NS_DURING
173        urlData = (char *)malloc( (len + 1) * sizeof(char));
174        NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string");
175
176        [string getCString: urlData];
177
178        selStart = 0; selEnd = len;
179
180        err = ICLaunchURL(ICCF_GetInst(), "\pmailto", urlData, len, &selStart, &selEnd);
181        ICCF_OSErrCAssert(err, @"ICLaunchURL");
182       
183    NS_HANDLER
184        free(urlData);
185        [localException raise];
186    NS_ENDHANDLER
187
188    free(urlData);   
189}
190
191// XXX not sure what to do if there's already a selection; BBEdit extends it, Tex-Edit Plus doesn't.
192// RFC-ordained max URL length, just to avoid passing IC multi-megabyte documents
193#if ICCF_DEBUG
194const long ICCF_MAX_URL_LEN = 1024; // XXX change later
195#else
196const long ICCF_MAX_URL_LEN = 1024;
197#endif
198
199BOOL ICCF_enabled = YES;
200
201void ICCF_HandleException(NSException *e) {
202    int result = NSRunAlertPanel(@"Open Internet Location", @"The selected Internet location could not be opened.\n\n%@.", @"OK", nil, @"Disable ICeCoffEEÉ", e);
203    if (result != NSAlertDefaultReturn) {
204        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);
205        if (result == NSAlertDefaultReturn)
206            ICCF_enabled = NO;
207    }
208}
209
210void ICCF_LaunchURLFromTextView(NSTextView *self) {
211    NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
212    NSRange range = [self selectedRange], delimiterRange;
213    NSColor *insertionPointColor = [self insertionPointColor];
214    NSString *s = [[self textStorage] string]; // according to the class documentation, sending 'string' is guaranteed to be O(1)
215    unsigned extraLen;
216    int i;
217
218    NS_DURING
219
220        NSCAssert(range.location != NSNotFound, @"There is no insertion point or selection in the text field you clicked");
221        NSCAssert(s != nil, @"Sorry, ICeCoffEE is unable to locate the insertion point or selection");
222
223        ICCF_StartIC();
224
225        NSCAssert([s length] != 0, @"No text was found");
226
227        if (range.location == [s length]) range.location--; // work around bug in selectionRangeForProposedRange (r. 2845418)
228
229        range = [self selectionRangeForProposedRange: range granularity: NSSelectByWord];
230
231        // However, NSSelectByWord does not capture even the approximate boundaries of a URL
232        // (text to a space/line ending character); it'll stop at a period in the middle of a hostname.
233        // So, we expand it as follows:
234
235        ICCF_CheckRange(range);
236
237        ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);
238
239        // XXX instead of 0, make this stop at the max URL length to prevent protracted searches
240        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...)
241        delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
242                                            options: NSLiteralSearch | NSBackwardsSearch
243                                              range: NSMakeRange(0, range.location + (range.location != [s length]))];
244        if (delimiterRange.location == NSNotFound) {
245            // extend to beginning of string
246            range.length += range.location;
247            range.location = 0;
248        } else {
249            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
250            range.length += range.location - delimiterRange.location - 1;
251            range.location = delimiterRange.location + 1;
252        }
253
254        ICCF_CheckRange(range);
255
256        // XXX instead of length of string, make this stop at the max URL length to prevent protracted searches
257        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., ...>)
258        extraLen = [s length] - range.location - range.length;
259        delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
260                                            options: NSLiteralSearch
261                                              range: NSMakeRange(range.location + range.length - (range.length != 0),
262                                                                 extraLen + (range.length != 0))];
263        if (delimiterRange.location == NSNotFound) {
264            // extend to end of string
265            range.length += extraLen;
266        } else {
267            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
268            range.length += delimiterRange.location - range.location - range.length;
269        }
270
271        ICCF_CheckRange(range);
272
273        ICCF_ParseURL([s substringWithRange: range], &range);
274
275        for (i = 0 ; i < 3 ; i++) {
276            NSRange emptyRange = {range.location, 0};
277            [self setInsertionPointColor: [self backgroundColor]];
278            [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: YES];
279            [self display];
280            usleep(60000);
281            [self setSelectedRange: emptyRange affinity: NSSelectionAffinityDownstream stillSelecting: YES];
282            [self display];
283            usleep(60000);
284        }
285        [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: NO];
286        [self display];
287
288        ICCF_LaunchURL([s substringWithRange: range]);
289
290    NS_HANDLER
291        ICCF_HandleException(localException);
292    NS_ENDHANDLER
293
294    ICCF_StopIC();
295    [self setInsertionPointColor: insertionPointColor];
296}
297
298NSString * const ICCF_SERVICES_ITEM = @"ICeCoffEE Services Item";
299
300NSMenuItem *ICCF_ServicesMenuItem() {
301    NSMenuItem *servicesItem;
302    NSMenu *servicesMenu;
303    // XXX better to just use [[NSApp servicesMenu] title]?  That grabs the title from the existing Services submenu.
304    NSString *servicesTitle = [[NSBundle bundleWithIdentifier: @"com.apple.AppKit"] localizedStringForKey: @"Services" value: nil table: @"ServicesMenu"];
305    if (servicesTitle == nil) {
306        ICLog(@"CanÕt get localized text for ÒServicesÓ in AppKit.framework");
307        servicesTitle = @"Services";
308    }
309    servicesMenu = [[NSMenu alloc] initWithTitle: servicesTitle];
310    servicesItem = [[NSMenuItem alloc] initWithTitle: servicesTitle action:nil keyEquivalent:@""];
311    [[NSApplication sharedApplication] setServicesMenu: servicesMenu];
312    [servicesItem setSubmenu: servicesMenu];
313    [servicesItem setRepresentedObject: ICCF_SERVICES_ITEM];
314    [servicesMenu release];
315    return servicesItem;
316}
317
318void ICCF_AddServicesMenu() {
319    [ICeCoffEE performSelector: @selector(IC_addServicesMenu) withObject: nil afterDelay: 0.0];
320}
321
322NSMenu *ICCF_MenuForEvent(NSTextView *self, NSMenu *contextMenu, NSEvent *e) {   
323    if (contextMenu != nil && [e type] == NSRightMouseDown || ([e type] == NSLeftMouseDown && [e modifierFlags] & NSControlKeyMask)) {
324        int servicesItemIndex = [contextMenu indexOfItemWithRepresentedObject: ICCF_SERVICES_ITEM];
325        if (servicesItemIndex == -1) {
326            [contextMenu addItem: [NSMenuItem separatorItem]];
327            [contextMenu addItem: ICCF_ServicesMenuItem()];
328        }
329    }
330    return contextMenu;
331}
332
333+ (NSString *)IC_version;
334{
335    // XXX get from bundle if possible: centralize
336    return [NSString stringWithCString: ICCF_VERSION];
337}
338
339+ (void)IC_addServicesMenu;
340{
341    NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
342    int insertLoc = [mainMenu indexOfItemWithSubmenu: [NSApp windowsMenu]];
343
344    if (insertLoc == -1)
345        insertLoc = [mainMenu numberOfItems];
346
347    [mainMenu insertItem: ICCF_ServicesMenuItem() atIndex: insertLoc];
348}
349
350// XXX localization?
351- (NSMenu *)menuForEvent:(NSEvent *)e;
352{
353    NSMenu *myMenu = [super menuForEvent: e];
354    return ICCF_MenuForEvent(self, myMenu, e);
355}
356
357- (void)mouseDown:(NSEvent *)e;
358{
359#if ICCF_DEBUG
360    static BOOL down = NO;
361    if (down) {
362        ICLog(@"recursive invocation!");
363        return;
364    }
365    down = YES;
366    ICLog(@"ICeCoffEE down: %@", e);
367    NSLog(@"super is %@", self);
368#endif
369    // we don't actually get a mouseUp event, just wait for mouseDown to return
370    [super mouseDown: e];
371    if (!ICCF_enabled) return;
372    // don't want command-option-click, command-shift-click, etc. to trigger
373    if (ICCF_EventIsCommandMouseDown(e)) {
374        NSEvent *upEvent = [[self window] currentEvent];
375        NSPoint downPt = [e locationInWindow];
376        NSPoint upPt = [upEvent locationInWindow];
377        ICLog(@"next: %@", upEvent);
378        NSAssert([upEvent type] == NSLeftMouseUp, @"NSTextView mouseDown: did not return with current event as mouse up!");
379        if (abs(downPt.x - upPt.x) <= 4 && abs(downPt.y - upPt.y) <= 4) {
380            ICCF_LaunchURLFromTextView(self);
381        }
382    }
383#if ICCF_DEBUG
384    down = NO;
385#endif
386}
387
388@end
Note: See TracBrowser for help on using the repository browser.