1 | // ICeCoffEE - Internet Config Carbon/Cocoa Editor Extension |
---|
2 | // Nicholas Riley <mailto:icecoffee@sabi.net> |
---|
3 | |
---|
4 | /* To do/think about: |
---|
5 | |
---|
6 | - TXNClick - MLTE has its own (lousy) support in Jaguar, seems improved in Panther, good enough to leave? |
---|
7 | |
---|
8 | */ |
---|
9 | |
---|
10 | #import "ICeCoffEE.h" |
---|
11 | #import <Carbon/Carbon.h> |
---|
12 | #include <unistd.h> |
---|
13 | #import "ICeCoffEESuper.h" |
---|
14 | #import "ICeCoffEESetServicesMenu.h" |
---|
15 | #import "ICeCoffEETrigger.h" |
---|
16 | |
---|
17 | iccfPrefRec ICCF_prefs; |
---|
18 | |
---|
19 | NSString *ICCF_ErrString(OSStatus err, NSString *context) { |
---|
20 | if (err == noErr || err == userCanceledErr) return nil; |
---|
21 | |
---|
22 | NSString *errNum = [NSString stringWithFormat: @"%ld", err]; |
---|
23 | NSString *errDesc = ICCF_LocalizedString(errNum); |
---|
24 | |
---|
25 | if (errDesc == NULL || errDesc == errNum) |
---|
26 | errDesc = [NSString stringWithFormat: ICCF_LocalizedString(@"An unknown error occurred in %@"), context]; |
---|
27 | |
---|
28 | return [NSString stringWithFormat: @"%@ (%d)", errDesc, (int)err]; |
---|
29 | } |
---|
30 | |
---|
31 | CFStringRef ICCF_CopyErrString(OSStatus err, CFStringRef context) { |
---|
32 | if (err == noErr || err == userCanceledErr) return NULL; |
---|
33 | |
---|
34 | CFStringRef errNum = CFStringCreateWithFormat(NULL, NULL, CFSTR("%ld"), err); |
---|
35 | CFStringRef errDesc = ICCF_CopyLocalizedString(errNum); |
---|
36 | |
---|
37 | if (errDesc == NULL || errDesc == errNum) { |
---|
38 | CFStringRef errDescFormat = ICCF_CopyLocalizedString(CFSTR("An unknown error occurred in %@")); |
---|
39 | if (errDesc != NULL) CFRelease(errDesc); |
---|
40 | errDesc = CFStringCreateWithFormat(NULL, NULL, errDescFormat, context); |
---|
41 | } |
---|
42 | |
---|
43 | CFStringRef errStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%d)"), errDesc, (int)err); |
---|
44 | |
---|
45 | if (errNum != NULL) CFRelease(errNum); |
---|
46 | if (errDesc != NULL) CFRelease(errDesc); |
---|
47 | return errStr; |
---|
48 | } |
---|
49 | |
---|
50 | CFStringRef ICCF_CopyAppName() { |
---|
51 | ProcessSerialNumber psn = {0, kCurrentProcess}; |
---|
52 | CFStringRef appName = NULL; |
---|
53 | CopyProcessName(&psn, &appName); |
---|
54 | if (appName == NULL) return CFSTR("(unknown)"); |
---|
55 | return appName; |
---|
56 | } |
---|
57 | |
---|
58 | BOOL ICCF_EventIsCommandMouseDown(NSEvent *e) { |
---|
59 | return ([e type] == NSLeftMouseDown && ([e modifierFlags] & NSCommandKeyMask) != 0 && [e clickCount] == 1); |
---|
60 | } |
---|
61 | |
---|
62 | iccfURLAction ICCF_KeyboardAction(NSEvent *e) { |
---|
63 | unsigned int modifierFlags = [e modifierFlags]; |
---|
64 | iccfURLAction action; |
---|
65 | action.presentMenu = (modifierFlags & NSAlternateKeyMask) != 0; |
---|
66 | action.launchInBackground = (modifierFlags & NSShiftKeyMask) != 0; |
---|
67 | return action; |
---|
68 | } |
---|
69 | |
---|
70 | void ICCF_CheckRange(NSRange range) { |
---|
71 | NSCAssert(range.length > 0, ICCF_LocalizedString(@"No URL is selected")); |
---|
72 | NSCAssert1(range.length <= ICCF_MAX_URL_LEN, ICCF_LocalizedString(@"The potential URL is longer than %lu characters"), ICCF_MAX_URL_LEN); |
---|
73 | } |
---|
74 | |
---|
75 | void ICCF_Delimiters(NSCharacterSet **leftPtr, NSCharacterSet **rightPtr) { |
---|
76 | static NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil; |
---|
77 | |
---|
78 | if (urlLeftDelimiters == nil || urlRightDelimiters == nil) { |
---|
79 | NSMutableCharacterSet *set = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy]; |
---|
80 | NSMutableCharacterSet *tmpSet; |
---|
81 | [urlLeftDelimiters release]; |
---|
82 | [urlRightDelimiters release]; |
---|
83 | |
---|
84 | [set autorelease]; |
---|
85 | [set formUnionWithCharacterSet: [[NSCharacterSet characterSetWithRange: NSMakeRange(0x21, 0x5e)] invertedSet]]; // nonprintable and non-ASCII characters |
---|
86 | [set formUnionWithCharacterSet: [NSCharacterSet punctuationCharacterSet]]; |
---|
87 | [set removeCharactersInString: @";/?:@&=+$,-_.!~*'()%#"]; // RFC 2396 ¤2.2, 2.3, 2.4, plus % and # from "delims" set |
---|
88 | |
---|
89 | tmpSet = [[set mutableCopy] autorelease]; |
---|
90 | [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><("]]; |
---|
91 | urlLeftDelimiters = [tmpSet copy]; // make immutable again - for efficiency |
---|
92 | |
---|
93 | tmpSet = [[set mutableCopy] autorelease]; |
---|
94 | [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"><)"]]; |
---|
95 | urlRightDelimiters = [tmpSet copy]; // make immutable again - for efficiency |
---|
96 | } |
---|
97 | |
---|
98 | *leftPtr = urlLeftDelimiters; *rightPtr = urlRightDelimiters; |
---|
99 | } |
---|
100 | |
---|
101 | static ICInstance ICCF_icInst = NULL; |
---|
102 | |
---|
103 | void ICCF_StartIC() { |
---|
104 | OSStatus err; |
---|
105 | |
---|
106 | if (ICCF_icInst != NULL) { |
---|
107 | ICLog(@"ICCF_StartIC: Internet Config is already running!"); |
---|
108 | ICCF_StopIC(); |
---|
109 | } |
---|
110 | err = ICStart(&ICCF_icInst, kICCFCreator); |
---|
111 | NSCAssert1(err == noErr, ICCF_LocalizedString(@"Unable to start Internet Config (error %d)"), err); |
---|
112 | } |
---|
113 | |
---|
114 | void ICCF_StopIC() { |
---|
115 | if (ICCF_icInst == NULL) { |
---|
116 | ICLog(@"ICCF_StopIC: Internet Config is not running!"); |
---|
117 | } else { |
---|
118 | ICStop(ICCF_icInst); |
---|
119 | ICCF_icInst = NULL; |
---|
120 | } |
---|
121 | } |
---|
122 | |
---|
123 | ICInstance ICCF_GetInst() { |
---|
124 | NSCAssert(ICCF_icInst != NULL, @"Internal error: Called ICCF_GetInst without ICCF_StartIC"); |
---|
125 | return ICCF_icInst; |
---|
126 | } |
---|
127 | |
---|
128 | ConstStringPtr ICCF_GetHint(ICInstance inst, const char *urlData, Size length, long *selStart, long *selEnd, Boolean *needsSlashes) { |
---|
129 | Handle h = NewHandle(0); |
---|
130 | OSStatus err; |
---|
131 | |
---|
132 | if (h == NULL) return NULL; |
---|
133 | |
---|
134 | // parse the URL providing a bogus protocol, to get rid of escaped forms |
---|
135 | err = ICParseURL(inst, "\p*", urlData, length, selStart, selEnd, h); |
---|
136 | if (err != noErr) return NULL; |
---|
137 | |
---|
138 | // scan through the parsed URL looking for characters not found in email addresses |
---|
139 | Size hSize = GetHandleSize(h); |
---|
140 | if (hSize == 0) return NULL; |
---|
141 | |
---|
142 | const char *urlParsed = *h; |
---|
143 | long i = 0; |
---|
144 | Boolean sawAt = false; |
---|
145 | if (hSize >= 2 && urlParsed[0] == '*' && urlParsed[1] == ':') { |
---|
146 | // this is an IC-inserted protocol; skip over it |
---|
147 | i = 2; |
---|
148 | *needsSlashes = (hSize < i + 2 || urlParsed[i] != '/' || urlParsed[i + 1] != '/'); |
---|
149 | } else *needsSlashes = false; |
---|
150 | for ( ; i < hSize ; i++) { |
---|
151 | char c = urlParsed[i]; |
---|
152 | if (c == '@') { |
---|
153 | sawAt = true; |
---|
154 | } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || |
---|
155 | (c == '+' || c == '-' || c == '_' || c == '!' || c == '.'))) { |
---|
156 | DisposeHandle(h); |
---|
157 | return "\phttp"; |
---|
158 | } |
---|
159 | } |
---|
160 | DisposeHandle(h); |
---|
161 | if (sawAt) { |
---|
162 | *needsSlashes = false; |
---|
163 | return "\pmailto"; |
---|
164 | } |
---|
165 | return "\phttp"; |
---|
166 | } |
---|
167 | |
---|
168 | static const char *kICSlashes = "//"; |
---|
169 | |
---|
170 | void ICCF_AddSlashes(Handle h, ConstStringPtr hint) { |
---|
171 | Size sizeBefore = GetHandleSize(h); |
---|
172 | unsigned char hintLength = StrLength(hint); |
---|
173 | char *copy = (char *)malloc(sizeBefore); |
---|
174 | memcpy(copy, *h, sizeBefore); |
---|
175 | ICLog(@"ICCF_AddSlashes before: |%s|\n", *h); |
---|
176 | ReallocateHandle(h, sizeBefore + 2); |
---|
177 | |
---|
178 | // if *h begins with '<hint>:', then copy the slashes after it |
---|
179 | if (sizeBefore > hintLength + 1 && strncmp((const char *)&hint[1], copy, hintLength) == 0 && copy[hintLength] == ':') { |
---|
180 | memcpy(*h, copy, hintLength + 1); |
---|
181 | memcpy(*h + hintLength + 1, kICSlashes, 2); |
---|
182 | memcpy(*h + hintLength + 3, ©[hintLength + 1], sizeBefore - hintLength - 1); |
---|
183 | } else { |
---|
184 | memcpy(*h, kICSlashes, 2); |
---|
185 | memcpy(*h + 2, copy, sizeBefore); |
---|
186 | } |
---|
187 | |
---|
188 | free(copy); |
---|
189 | ICLog(@"ICCF_AddSlashes after: |%s|\n", *h); |
---|
190 | } |
---|
191 | |
---|
192 | // input/output 'range' is the range of source document which contains 'string' |
---|
193 | void ICCF_ParseURL(NSString *string, NSRange *range) { |
---|
194 | OSStatus err; |
---|
195 | Handle h; |
---|
196 | long selStart = 0, selEnd = range->length; // local offsets within 'string' |
---|
197 | char *urlData = NULL; |
---|
198 | |
---|
199 | NSCAssert(selEnd == [string length], @"Internal error: URL string is wrong length"); |
---|
200 | |
---|
201 | NS_DURING |
---|
202 | if ([[NSCharacterSet characterSetWithCharactersInString: @";,."] characterIsMember: |
---|
203 | [string characterAtIndex: selEnd - 1]]) { |
---|
204 | selEnd--; |
---|
205 | } |
---|
206 | NSCharacterSet *alphanumericCharacterSet = [NSCharacterSet alphanumericCharacterSet]; |
---|
207 | while (![alphanumericCharacterSet characterIsMember: [string characterAtIndex: selStart]]) { |
---|
208 | selStart++; |
---|
209 | NSCAssert(selStart < selEnd, @"No URL is selected"); |
---|
210 | } |
---|
211 | |
---|
212 | string = [string substringWithRange: NSMakeRange(selStart, selEnd - selStart)]; |
---|
213 | |
---|
214 | ICLog(@"Parsing URL |%@|", string); |
---|
215 | |
---|
216 | NSCAssert([string canBeConvertedToEncoding: NSASCIIStringEncoding], @"No URL is selected"); |
---|
217 | |
---|
218 | urlData = (char *)malloc( (range->length + 1) * sizeof(char)); |
---|
219 | NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string"); |
---|
220 | |
---|
221 | // 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. |
---|
222 | [string getCString: urlData]; |
---|
223 | |
---|
224 | h = NewHandle(0); |
---|
225 | NSCAssert(h != NULL, @"Internal error: can't allocate URL handle"); |
---|
226 | |
---|
227 | err = ICParseURL(ICCF_GetInst(), "\pmailto", urlData, range->length, &selStart, &selEnd, h); |
---|
228 | DisposeHandle(h); |
---|
229 | |
---|
230 | ICCF_OSErrCAssert(err, @"ICParseURL"); |
---|
231 | |
---|
232 | range->length = selEnd - selStart; |
---|
233 | range->location += selStart; |
---|
234 | NS_HANDLER |
---|
235 | free(urlData); |
---|
236 | [localException raise]; |
---|
237 | NS_ENDHANDLER |
---|
238 | |
---|
239 | free(urlData); |
---|
240 | } |
---|
241 | |
---|
242 | BOOL ICCF_LaunchURL(NSString *string, iccfURLAction action) { |
---|
243 | OSStatus err = noErr; |
---|
244 | long selStart, selEnd; |
---|
245 | unsigned len = [string length]; |
---|
246 | |
---|
247 | Handle h = NULL; |
---|
248 | |
---|
249 | NS_DURING |
---|
250 | h = NewHandle(len); |
---|
251 | if (h == NULL) |
---|
252 | ICCF_OSErrCAssert(MemError(), @"NewHandle"); |
---|
253 | |
---|
254 | if (CFStringGetBytes((CFStringRef)string, CFRangeMake(0, len), kCFStringEncodingASCII, '\0', false, (UInt8 *)*h, len, NULL) != len) |
---|
255 | ICCF_OSErrCAssert(kTECNoConversionPathErr, @"CFStringGetBytes"); |
---|
256 | |
---|
257 | selStart = 0; selEnd = len; |
---|
258 | |
---|
259 | Boolean needsSlashes; |
---|
260 | ConstStringPtr hint = ICCF_GetHint(ICCF_GetInst(), *h, len, &selStart, &selEnd, &needsSlashes); |
---|
261 | NSCAssert(hint != NULL, @"Internal error: can't get protocol hint for URL"); |
---|
262 | |
---|
263 | if (needsSlashes) { |
---|
264 | ICCF_AddSlashes(h, hint); |
---|
265 | len = selEnd = GetHandleSize(h); |
---|
266 | } |
---|
267 | |
---|
268 | err = ICCF_DoURLAction(ICCF_GetInst(), hint, *h, selStart, selEnd, action); |
---|
269 | ICCF_OSErrCAssert(err, @"ICCF_DoURLAction"); |
---|
270 | |
---|
271 | NS_HANDLER |
---|
272 | DisposeHandle(h); |
---|
273 | [localException raise]; |
---|
274 | NS_ENDHANDLER |
---|
275 | |
---|
276 | DisposeHandle(h); |
---|
277 | |
---|
278 | return (err == noErr); |
---|
279 | } |
---|
280 | |
---|
281 | // XXX not sure what to do if there's already a selection; BBEdit and MLTE extend it, Tex-Edit Plus doesn't. |
---|
282 | // RFC-ordained max URL length, just to avoid passing IC/LS multi-megabyte documents |
---|
283 | #if ICCF_DEBUG |
---|
284 | const long ICCF_MAX_URL_LEN = 60; // XXX change later |
---|
285 | #else |
---|
286 | const long ICCF_MAX_URL_LEN = 1024; |
---|
287 | #endif |
---|
288 | |
---|
289 | Boolean ICCF_enabled = true; |
---|
290 | |
---|
291 | BOOL ICCF_HandleException(NSException *e) { |
---|
292 | if ([e reason] == nil || [[e reason] length] == 0) |
---|
293 | return NO; |
---|
294 | |
---|
295 | if (ICCF_prefs.errorSoundEnabled) NSBeep(); |
---|
296 | if (!ICCF_prefs.errorDialogEnabled) return YES; |
---|
297 | |
---|
298 | int result = NSRunAlertPanel(ICCF_LocalizedString(@"AlertTitle"), ICCF_LocalizedString(@"AlertMessage%@"), nil, nil, ICCF_LocalizedString(@"AlertDisableButton"), e); |
---|
299 | if (result != NSAlertDefaultReturn) { |
---|
300 | result = NSRunAlertPanel(ICCF_LocalizedString(@"DisableAlertTitle"), ICCF_LocalizedString(@"DisableAlertMessage%@"), ICCF_LocalizedString(@"DisableAlertDisableButton"), ICCF_LocalizedString(@"DisableAlertDontDisableButton"), nil, |
---|
301 | [(NSString *)ICCF_CopyAppName() autorelease]); |
---|
302 | if (result == NSAlertDefaultReturn) |
---|
303 | ICCF_enabled = NO; |
---|
304 | } |
---|
305 | return YES; |
---|
306 | } |
---|
307 | |
---|
308 | void ICCF_LaunchURLFromTextView(NSTextView *self, NSEvent *triggeringEvent) { |
---|
309 | NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil; |
---|
310 | NSRange range = [self selectedRange], delimiterRange; |
---|
311 | NSColor *insertionPointColor = [self insertionPointColor]; |
---|
312 | NSString *s = [[self textStorage] string]; // according to the class documentation, sending 'string' is guaranteed to be O(1) |
---|
313 | unsigned extraLen; |
---|
314 | int i; |
---|
315 | |
---|
316 | NS_DURING |
---|
317 | |
---|
318 | NSCAssert(range.location != NSNotFound, ICCF_LocalizedString(@"There is no insertion point or selection in the text field where you clicked")); |
---|
319 | NSCAssert(s != nil, ICCF_LocalizedString(@"Sorry, ICeCoffEE is unable to locate the insertion point or selection")); |
---|
320 | |
---|
321 | ICCF_StartIC(); |
---|
322 | |
---|
323 | NSCAssert([s length] != 0, ICCF_LocalizedString(@"No text was found")); |
---|
324 | |
---|
325 | if (range.location == [s length]) range.location--; // work around bug in selectionRangeForProposedRange (r. 2845418) |
---|
326 | |
---|
327 | range = [self selectionRangeForProposedRange: range granularity: NSSelectByWord]; |
---|
328 | |
---|
329 | // However, NSSelectByWord does not capture even the approximate boundaries of a URL |
---|
330 | // (text to a space/line ending character); it'll stop at a period in the middle of a hostname. |
---|
331 | // So, we expand it as follows: |
---|
332 | |
---|
333 | ICCF_CheckRange(range); |
---|
334 | |
---|
335 | ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters); |
---|
336 | |
---|
337 | // XXX instead of 0, make this stop at the max URL length to prevent protracted searches |
---|
338 | // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...) |
---|
339 | delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters |
---|
340 | options: NSLiteralSearch | NSBackwardsSearch |
---|
341 | range: NSMakeRange(0, range.location + (range.location != [s length]))]; |
---|
342 | if (delimiterRange.location == NSNotFound) { |
---|
343 | // extend to beginning of string |
---|
344 | range.length += range.location; |
---|
345 | range.location = 0; |
---|
346 | } else { |
---|
347 | NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1"); |
---|
348 | range.length += range.location - delimiterRange.location - 1; |
---|
349 | range.location = delimiterRange.location + 1; |
---|
350 | } |
---|
351 | |
---|
352 | ICCF_CheckRange(range); |
---|
353 | |
---|
354 | // XXX instead of length of string, make this stop at the max URL length to prevent protracted searches |
---|
355 | // add 1 to range to trap delimiters that are on the edge of the selection (i.e., ...>) |
---|
356 | extraLen = [s length] - range.location - range.length; |
---|
357 | delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters |
---|
358 | options: NSLiteralSearch |
---|
359 | range: NSMakeRange(range.location + range.length - (range.length != 0), |
---|
360 | extraLen + (range.length != 0))]; |
---|
361 | if (delimiterRange.location == NSNotFound) { |
---|
362 | // extend to end of string |
---|
363 | range.length += extraLen; |
---|
364 | } else { |
---|
365 | NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1"); |
---|
366 | range.length += delimiterRange.location - range.location - range.length; |
---|
367 | } |
---|
368 | |
---|
369 | ICCF_CheckRange(range); |
---|
370 | |
---|
371 | ICCF_ParseURL([s substringWithRange: range], &range); |
---|
372 | |
---|
373 | [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: NO]; |
---|
374 | [self display]; |
---|
375 | |
---|
376 | if (ICCF_LaunchURL([s substringWithRange: range], ICCF_KeyboardAction(triggeringEvent)) && ICCF_prefs.textBlinkEnabled) { |
---|
377 | for (i = 0 ; i < ICCF_prefs.textBlinkCount ; i++) { |
---|
378 | NSRange emptyRange = {range.location, 0}; |
---|
379 | [self setSelectedRange: emptyRange affinity: NSSelectionAffinityDownstream stillSelecting: YES]; |
---|
380 | [self display]; |
---|
381 | usleep(kICBlinkDelayUsecs); |
---|
382 | [self setInsertionPointColor: [self backgroundColor]]; |
---|
383 | [self setSelectedRange: range affinity: NSSelectionAffinityDownstream stillSelecting: YES]; |
---|
384 | [self display]; |
---|
385 | usleep(kICBlinkDelayUsecs); |
---|
386 | } |
---|
387 | } |
---|
388 | |
---|
389 | NS_HANDLER |
---|
390 | ICCF_HandleException(localException); |
---|
391 | NS_ENDHANDLER |
---|
392 | |
---|
393 | ICCF_StopIC(); |
---|
394 | [self setInsertionPointColor: insertionPointColor]; |
---|
395 | } |
---|
396 | |
---|
397 | NSString * const ICCF_SERVICES_ITEM = @"ICeCoffEE Services Item"; |
---|
398 | |
---|
399 | NSMenuItem *ICCF_ServicesMenuItem() { |
---|
400 | NSMenuItem *servicesItem; |
---|
401 | NSString *servicesTitle = nil; |
---|
402 | NSMenu *servicesMenu = [NSApp servicesMenu]; |
---|
403 | |
---|
404 | if (servicesMenu != nil) { |
---|
405 | servicesTitle = [servicesMenu title]; |
---|
406 | if (servicesTitle == nil) { |
---|
407 | ICLog(@"Can't get service menu title"); |
---|
408 | servicesTitle = @"Services"; |
---|
409 | } |
---|
410 | } else { |
---|
411 | servicesTitle = [[NSBundle bundleWithIdentifier: @"com.apple.AppKit"] localizedStringForKey: @"Services" value: nil table: @"ServicesMenu"]; |
---|
412 | if (servicesTitle == nil) { |
---|
413 | ICLog(@"Can't get localized text for 'Services' in AppKit.framework"); |
---|
414 | servicesTitle = @"Services"; |
---|
415 | } |
---|
416 | } |
---|
417 | servicesMenu = [[NSMenu alloc] initWithTitle: servicesTitle]; |
---|
418 | servicesItem = [[NSMenuItem alloc] initWithTitle: servicesTitle action:nil keyEquivalent:@""]; |
---|
419 | ICCF_SetServicesMenu(servicesMenu); |
---|
420 | [servicesItem setSubmenu: servicesMenu]; |
---|
421 | [servicesItem setRepresentedObject: ICCF_SERVICES_ITEM]; |
---|
422 | [servicesMenu release]; |
---|
423 | return [servicesItem autorelease]; |
---|
424 | } |
---|
425 | |
---|
426 | static const unichar UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE = 0x25b8; |
---|
427 | |
---|
428 | // returns YES if menu contains useful items, NO otherwise |
---|
429 | BOOL ICCF_ConsolidateServicesMenu(NSMenu *menu, NSDictionary *serviceOptions) { |
---|
430 | [menu update]; // doesn't propagate to submenus, so we need to do this first |
---|
431 | NSEnumerator *enumerator = [[menu itemArray] objectEnumerator]; |
---|
432 | NSMenuItem *menuItem; |
---|
433 | NSMenu *submenu; |
---|
434 | NSDictionary *itemOptions = nil; |
---|
435 | BOOL shouldKeepItem = NO, shouldKeepMenu = NO; |
---|
436 | |
---|
437 | while ( (menuItem = [enumerator nextObject]) != nil) { |
---|
438 | if (serviceOptions != nil) |
---|
439 | itemOptions = [serviceOptions objectForKey: [menuItem title]]; |
---|
440 | if ([[itemOptions objectForKey: (NSString *)kICServiceHidden] boolValue]) { |
---|
441 | shouldKeepItem = NO; |
---|
442 | } else if ( (submenu = [menuItem submenu]) != nil) { |
---|
443 | shouldKeepItem = ICCF_ConsolidateServicesMenu(submenu, [itemOptions objectForKey: (NSString *)kICServiceSubmenu]); |
---|
444 | if (shouldKeepItem && [submenu numberOfItems] == 1) { // consolidate |
---|
445 | NSMenuItem *serviceItem = [[submenu itemAtIndex: 0] retain]; |
---|
446 | [serviceItem setTitle: |
---|
447 | [NSString stringWithFormat: @"%@ %@ %@", [menuItem title], [NSString stringWithCharacters: &UNICHAR_BLACK_RIGHT_POINTING_SMALL_TRIANGLE length: 1], [serviceItem title]]]; |
---|
448 | |
---|
449 | int serviceIndex = [menu indexOfItem: menuItem]; |
---|
450 | [submenu removeItemAtIndex: 0]; // can't have item in two menus |
---|
451 | [menu removeItemAtIndex: serviceIndex]; |
---|
452 | [menu insertItem: serviceItem atIndex: serviceIndex]; |
---|
453 | [serviceItem release]; |
---|
454 | } |
---|
455 | } else { |
---|
456 | [menuItem setKeyEquivalent: @""]; |
---|
457 | shouldKeepItem = [menuItem isEnabled]; |
---|
458 | } |
---|
459 | if (shouldKeepItem) { |
---|
460 | shouldKeepMenu = YES; |
---|
461 | } else { |
---|
462 | [menu removeItem: menuItem]; |
---|
463 | } |
---|
464 | } |
---|
465 | |
---|
466 | return shouldKeepMenu; |
---|
467 | } |
---|
468 | |
---|
469 | NSMenuItem *ICCF_ContextualServicesMenuItem() { |
---|
470 | NSMenuItem *servicesItem = ICCF_ServicesMenuItem(); |
---|
471 | if (ICCF_ConsolidateServicesMenu([servicesItem submenu], (NSDictionary *)ICCF_prefs.serviceOptions)) |
---|
472 | return servicesItem; |
---|
473 | else |
---|
474 | return nil; |
---|
475 | } |
---|
476 | |
---|
477 | void ICCF_AddRemoveServicesMenu() { |
---|
478 | // needed because: |
---|
479 | // (a) we get called before the runloop has properly started and will crash if we don't delay on app startup |
---|
480 | // (b) the APE message handler calls us from another thread and nothing happens if we try to add a menu on it |
---|
481 | [ICeCoffEE performSelectorOnMainThread: @selector(IC_addRemoveServicesMenu) withObject: nil waitUntilDone: NO]; |
---|
482 | } |
---|
483 | |
---|
484 | NSMenu *ICCF_MenuForEvent(NSView *self, NSMenu *contextMenu, NSEvent *e) { |
---|
485 | if (contextMenu != nil && [e type] == NSRightMouseDown || ([e type] == NSLeftMouseDown && [e modifierFlags] & NSControlKeyMask)) { |
---|
486 | int servicesItemIndex = [contextMenu indexOfItemWithRepresentedObject: ICCF_SERVICES_ITEM]; |
---|
487 | // always regenerate: make sure menu reflects context |
---|
488 | if (servicesItemIndex != -1) { |
---|
489 | [contextMenu removeItemAtIndex: servicesItemIndex]; |
---|
490 | [contextMenu removeItemAtIndex: servicesItemIndex - 1]; |
---|
491 | } |
---|
492 | if (ICCF_prefs.servicesInContextualMenu) { |
---|
493 | NSMenuItem *contextualServicesItem = ICCF_ContextualServicesMenuItem(); |
---|
494 | if (contextualServicesItem != nil) { |
---|
495 | [contextMenu addItem: [NSMenuItem separatorItem]]; |
---|
496 | [contextMenu addItem: contextualServicesItem]; |
---|
497 | } |
---|
498 | } |
---|
499 | } |
---|
500 | return contextMenu; |
---|
501 | } |
---|
502 | |
---|
503 | static NSEvent *ICCF_MouseDownEventWithModifierFlags(NSEvent *e, BOOL inheritModifierFlags) { |
---|
504 | return [NSEvent mouseEventWithType: NSLeftMouseDown |
---|
505 | location: [e locationInWindow] |
---|
506 | modifierFlags: (inheritModifierFlags ? [e modifierFlags] : 0) |
---|
507 | timestamp: [e timestamp] |
---|
508 | windowNumber: [e windowNumber] |
---|
509 | context: [e context] |
---|
510 | eventNumber: [e eventNumber] |
---|
511 | clickCount: 1 |
---|
512 | pressure: 0]; |
---|
513 | } |
---|
514 | |
---|
515 | |
---|
516 | @interface NSTextView (IC_NSSharing) |
---|
517 | // only in Mac OS X 10.4 and later |
---|
518 | - (NSArray *)selectedRanges; |
---|
519 | @end |
---|
520 | |
---|
521 | @implementation ICeCoffEE |
---|
522 | |
---|
523 | + (void)IC_addRemoveServicesMenu; |
---|
524 | { |
---|
525 | NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu]; |
---|
526 | static NSMenuItem *servicesItem = nil; |
---|
527 | |
---|
528 | if (servicesItem == nil && ICCF_prefs.servicesInMenuBar) { |
---|
529 | servicesItem = [ICCF_ServicesMenuItem() retain]; |
---|
530 | |
---|
531 | int insertLoc = [mainMenu indexOfItemWithSubmenu: [NSApp windowsMenu]]; |
---|
532 | if (insertLoc == -1) |
---|
533 | insertLoc = [mainMenu numberOfItems]; |
---|
534 | |
---|
535 | [mainMenu insertItem: servicesItem atIndex: insertLoc]; |
---|
536 | } else if (servicesItem != nil && !ICCF_prefs.servicesInMenuBar) { |
---|
537 | [mainMenu removeItem: servicesItem]; |
---|
538 | [servicesItem release]; |
---|
539 | servicesItem = nil; |
---|
540 | } |
---|
541 | if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_3) { |
---|
542 | [[NSApp servicesMenu] update]; // enable keyboard equivalents in Mac OS X 10.3 |
---|
543 | } |
---|
544 | } |
---|
545 | |
---|
546 | // XXX localization? |
---|
547 | - (NSMenu *)menuForEvent:(NSEvent *)e; |
---|
548 | { |
---|
549 | NSMenu *myMenu = [super menuForEvent: e]; |
---|
550 | return ICCF_MenuForEvent(self, myMenu, e); |
---|
551 | } |
---|
552 | |
---|
553 | - (void)mouseDown:(NSEvent *)e; |
---|
554 | { |
---|
555 | #if ICCF_DEBUG |
---|
556 | static BOOL down = NO; |
---|
557 | if (down) { |
---|
558 | ICLog(@"recursive invocation!"); |
---|
559 | return; |
---|
560 | } |
---|
561 | down = YES; |
---|
562 | ICLog(@"ICeCoffEE down: %@", e); |
---|
563 | #endif |
---|
564 | if (ICCF_sharedTrigger != nil) { |
---|
565 | ICLog(@"%@ cancelling", ICCF_sharedTrigger); |
---|
566 | [ICCF_sharedTrigger cancel]; |
---|
567 | } |
---|
568 | if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(e)) { |
---|
569 | BOOL inheritModifierFlags; |
---|
570 | if ([self respondsToSelector: @selector(selectedRanges)]) { |
---|
571 | // Command-multiple-click or -drag for discontiguous selection, Mac OS X 10.4 or later |
---|
572 | inheritModifierFlags = YES; |
---|
573 | } else { |
---|
574 | // don't want to trigger selection extension or anything else; pass through as a plain click |
---|
575 | // (on Mac OS X 10.3, command does not modify behavior) |
---|
576 | inheritModifierFlags = NO; |
---|
577 | } |
---|
578 | [super mouseDown: ICCF_MouseDownEventWithModifierFlags(e, inheritModifierFlags)]; |
---|
579 | // we don't actually get a mouseUp event, just wait for mouseDown to return |
---|
580 | NSEvent *upEvent = [[self window] currentEvent]; |
---|
581 | NSPoint downPt = [e locationInWindow]; |
---|
582 | NSPoint upPt = [upEvent locationInWindow]; |
---|
583 | ICLog(@"next: %@", upEvent); |
---|
584 | NSAssert([upEvent type] == NSLeftMouseUp, @"NSTextView mouseDown: did not return with current event as mouse up!"); |
---|
585 | if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) { |
---|
586 | if (inheritModifierFlags) { |
---|
587 | // Mac OS X 10.4 and later: make sure we don't have a command-double-click |
---|
588 | [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 |
---|
589 | ICLog(@"%@ set", ICCF_sharedTrigger); |
---|
590 | } else { |
---|
591 | // Mac OS X 10.3 |
---|
592 | ICCF_LaunchURLFromTextView(self, e); |
---|
593 | } |
---|
594 | } |
---|
595 | } else { |
---|
596 | [super mouseDown: e]; |
---|
597 | } |
---|
598 | #if ICCF_DEBUG |
---|
599 | down = NO; |
---|
600 | #endif |
---|
601 | } |
---|
602 | |
---|
603 | @end |
---|