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 |
|
---|
15 | Carbon support ideas:
|
---|
16 | TEClick - TextEdit
|
---|
17 | TXNClick - MLTE
|
---|
18 | ? ATSUI
|
---|
19 | WASTE has its own support
|
---|
20 |
|
---|
21 | Done:
|
---|
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 |
|
---|
35 | typedef struct {
|
---|
36 | OSStatus status;
|
---|
37 | NSString * const desc;
|
---|
38 | } errRec, errList[];
|
---|
39 |
|
---|
40 | static 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 |
|
---|
50 | NSString *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 |
|
---|
62 | BOOL ICCF_EventIsCommandMouseDown(NSEvent *e) {
|
---|
63 | return ([e type] == NSLeftMouseDown && [e modifierFlags] == NSCommandKeyMask && [e clickCount] == 1);
|
---|
64 | }
|
---|
65 |
|
---|
66 | void 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 |
|
---|
71 | void 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 |
|
---|
96 | static ICInstance ICCF_icInst = NULL;
|
---|
97 |
|
---|
98 | void 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 |
|
---|
109 | void 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 |
|
---|
118 | ICInstance ICCF_GetInst() {
|
---|
119 | NSCAssert(ICCF_icInst != NULL, @"Internal error: Called ICCF_GetInst without ICCF_StartIC");
|
---|
120 | return ICCF_icInst;
|
---|
121 | }
|
---|
122 |
|
---|
123 | void 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 |
|
---|
165 | void 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
|
---|
194 | const long ICCF_MAX_URL_LEN = 1024; // XXX change later
|
---|
195 | #else
|
---|
196 | const long ICCF_MAX_URL_LEN = 1024;
|
---|
197 | #endif
|
---|
198 |
|
---|
199 | BOOL ICCF_enabled = YES;
|
---|
200 |
|
---|
201 | void 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 |
|
---|
210 | void 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 |
|
---|
298 | NSString * const ICCF_SERVICES_ITEM = @"ICeCoffEE Services Item";
|
---|
299 |
|
---|
300 | NSMenuItem *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 |
|
---|
318 | void ICCF_AddServicesMenu() {
|
---|
319 | [ICeCoffEE performSelector: @selector(IC_addServicesMenu) withObject: nil afterDelay: 0.0];
|
---|
320 | }
|
---|
321 |
|
---|
322 | NSMenu *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
|
---|