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

Last change on this file since 66 was 66, checked in by Nicholas Riley, 19 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.