source: trunk/ICeCoffEE/ICeCoffEE/ICeCoffEETerminal.m@ 319

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

VERSION: Starting with 1.5d1.

ICeCoffEEKeyEquivalents.m: Support "collision font" for displaying key
equivalent conflicts.

ICeCoffEE.m: Increase debug ICCF_MAX_URL_LEN to 120 for testing. Set
icons in ICCF_ConsolidateServicesMenu (needs better caching).

ICeCoffEEServicePrefController.m: Display icons, proper key
equivalents (instead of #, what was I thinking?!) and conflicts. Fix
a dumb bug in ICCF_PropagateServiceStateChange. Ellipsize long menu
items rather than chopping them off. Fix key equivalent column
getting moved when expanding disclosure triangles.

ICeCoffEELabeledIconCell.[hm]: An IconRef-displaying text cell.

Info-APE Module.plist: Update version to 1.5d1.

ICeCoffEE.xcodeproj: Added files, no significant changes.

English.lproj/InfoPlist.strings: Update version to 1.5d1.

English.lproj/APEInfo.rtfd/TXT.rtf: Some overdue documentation
updates.

ICeCoffEEShared.[hm]: Enable debugging; we're now using
kICServiceShortcut (though not yet for customizable shortcuts) so
define its data type.

ICeCoffEETerminal.m: Remove some useless code to "extend to beginning
of string" which seems to have been stolen from the NSTextView
implementation and not well understood. Handle common uses of
parentheses in URLs; still need to do this for NSTextView.

ICeCoffEESetServicesMenu.[hm]: Needs renaming; now with icon
extraction functionality and semi-working code to create a service
info dictionary.

Info-APEManagerPrefPane.plist: Update version to 1.5d1.

File size: 17.1 KB
Line 
1//
2// ICeCoffEETerminal.m
3// ICeCoffEE
4//
5// Created by Nicholas Riley on Mon Jan 28 2002.
6// Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9#import "ICeCoffEETerminal.h"
10#import "ICeCoffEEScanner.h"
11#import "ICeCoffEE.h"
12#import <Carbon/Carbon.h>
13#include <unistd.h>
14
15static NSRange ICCF_zeroRange = { NSNotFound, 5 };
16
17#define min(a,b) (a < b ? a : b)
18#define max(a,b) (a > b ? a : b)
19
20@interface TermStorage:NSObject
21- (struct _FSelPt)selPt0;
22- (struct _FSelPt)selPt1;
23- (BOOL)hasSelection;
24- (void)startSelectionAtLine:(unsigned int)row column:(unsigned short)column;
25- (void)startSelectionAtLine:(unsigned int)row offset:(unsigned short)offset;
26- (void)endSelectionAtLine:(unsigned int)row offset:(unsigned short)offset;
27- (void)selectWhitespaceDelimitedTextAtLine:(unsigned int)line offset:(unsigned short)offset;
28- (void)selectWordAtLine:(unsigned int)line offset:(unsigned short)offset;
29- (NSString *)selectedString;
30- (unsigned int)numberOfLines;
31- (unsigned int)effectiveColumnsForLine:(unsigned int)line;
32- (void)clearSelection;
33- (BOOL)isSelected:(unsigned int)line :(unsigned int)column;
34@end
35
36@interface TermController:NSObject
37- (TermStorage *)storage;
38@end
39
40@implementation ICECoffEETermSubviewSuper
41// NSTextInput implementation
42- (void)insertText:(id)aString {}
43- (void)doCommandBySelector:(SEL)aSelector {}
44- (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {}
45- (void)unmarkText {}
46- (BOOL)hasMarkedText { return NO; }
47- (long)conversationIdentifier { return 0; }
48- (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange { return nil; }
49- (NSRange)markedRange { return ICCF_zeroRange; }
50- (NSRange)selectedRange { return ICCF_zeroRange; }
51- (NSRect)firstRectForCharacterRange:(NSRange)theRange { return NSZeroRect; }
52- (unsigned int)characterIndexForPoint:(NSPoint)thePoint { return 0; }
53- (NSArray*)validAttributesForMarkedText { return nil; }
54// NSDraggingDestination
55- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender { return NSDragOperationNone; }
56// misc. other stuff
57- (void)_optionClickEvent:(NSEvent *)event:(unsigned int)row:(unsigned short)column {}
58- (void)setNeedsDisplay {}
59// NSView methods (needed to avoid crash in super invocation)
60- (void)mouseUp:(NSEvent *)e { [super mouseUp: e]; }
61- (void)mouseDown:(NSEvent *)e { [super mouseDown: e]; }
62@end
63
64@interface ICeCoffEETerminalRange : NSObject
65{
66 TermStorage *storage;
67 struct _FSelPt pt0;
68 struct _FSelPt pt1;
69 unsigned int width;
70 unsigned int height;
71}
72
73+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal;
74+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
75
76- (id)initWithTerminal:(ICeCoffEETerminal *)terminal;
77- (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
78
79- (struct _FSelPt)pt0;
80- (struct _FSelPt)pt1;
81
82- (NSString *)stringFromRange;
83
84- (void)growBackwardByLength:(unsigned long)length;
85- (void)growForwardByLength:(unsigned long)length;
86
87- (void)shrinkBackByLength:(unsigned long)length;
88- (void)shrinkFrontByLength:(unsigned long)length;
89
90- (BOOL)rangeIsEmpty;
91
92- (void)select;
93
94@end
95
96@implementation ICeCoffEETerminalRange
97
98+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal;
99{
100 return [[[self alloc] initWithTerminal: terminal] autorelease];
101}
102
103+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
104{
105 return [[[self alloc] initWithTerminal: terminal pt0: selPt0 pt1: selPt1] autorelease];
106}
107
108- (id)initWithTerminal:(ICeCoffEETerminal *)terminal;
109{
110 if ( (self = [self init]) != nil) {
111 storage = [(TermController *)[terminal valueForKey: @"controller"] storage];
112 pt0 = [storage selPt0];
113 pt1 = [storage selPt1];
114 width = [storage effectiveColumnsForLine: pt0.line];
115 height = [storage numberOfLines];
116 }
117 return self;
118}
119
120- (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
121{
122 if ( (self = [self initWithTerminal: terminal]) != nil) {
123 pt0 = selPt0;
124 pt1 = selPt1;
125 }
126 return self;
127}
128
129- (struct _FSelPt)pt0;
130{
131 return pt0;
132}
133
134- (struct _FSelPt)pt1;
135{
136 return pt1;
137}
138
139- (NSString *)stringFromRange;
140{
141 struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1];
142 NSString *str;
143
144 [storage startSelectionAtLine: pt0.line offset: pt0.col];
145 [storage endSelectionAtLine: pt1.line offset: pt1.col];
146
147 str = [storage selectedString];
148
149 [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col];
150 [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col];
151
152 return str;
153}
154
155- (struct _FSelPt)_moveBack:(struct _FSelPt)pt byLength:(unsigned long)length;
156{
157 unsigned int extraLines = length / width;
158 if ((long)(pt.line - extraLines) < 0) {
159 pt.line = 0;
160 pt.col = 0;
161 } else if (pt.line - extraLines == 0) {
162 pt.line = 0;
163 pt.col += width - (length % height);
164 if (pt.col < width) pt.col = 0;
165 else pt.col -= width;
166 } else {
167 pt.line -= extraLines;
168 pt.col += width - (length % height);
169 if (pt.col < width) pt.line--;
170 else pt.col -= width;
171 }
172 return pt;
173}
174
175- (struct _FSelPt)_moveForward:(struct _FSelPt)pt byLength:(unsigned long)length;
176{
177 unsigned int extraLines = length / width;
178 if (pt.line + extraLines >= height) {
179 pt.line = height - 1;
180 pt.col = width - 1;
181 } else if (pt.line + extraLines == height - 1) {
182 pt.line = height - 1;
183 pt.col += length % height;
184 if (pt.col >= width) pt.col = width - 1;
185 } else {
186 pt.line += extraLines;
187 pt.col = pt.col + (length % height);
188 if (pt.col > width) {
189 pt.line++;
190 pt.col -= width;
191 }
192 }
193 return pt;
194}
195
196- (BOOL)rangeIsEmpty;
197{
198 return (pt1.line < pt0.line) || (pt1.line == pt0.line && pt1.col <= pt0.col);
199}
200
201- (void)growBackwardByLength:(unsigned long)length;
202{
203 pt0 = [self _moveBack: pt0 byLength: length];
204}
205
206- (void)growForwardByLength:(unsigned long)length;
207{
208 pt1 = [self _moveForward: pt1 byLength: length];
209}
210
211- (void)shrinkFrontByLength:(unsigned long)length;
212{
213 pt0 = [self _moveForward: pt0 byLength: length];
214}
215
216- (void)shrinkBackByLength:(unsigned long)length;
217{
218 pt1 = [self _moveBack: pt1 byLength: length];
219}
220
221- (void)select;
222{
223 [storage startSelectionAtLine: pt0.line offset: pt0.col];
224 [storage endSelectionAtLine: pt1.line offset: pt1.col];
225}
226
227- (NSString *)description;
228{
229 return [NSString stringWithFormat: @"L%uC%hu - L%uC%hu: |%@|", pt0.line, pt0.col, pt1.line, pt1.col, [[self stringFromRange] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
230}
231
232@end
233
234// TermSubview's NSTextInput implementation is essentially a noop: make one that does something
235@implementation ICeCoffEETerminal // XXX normally in category (NSTextInput), but APE doesnÕt work if I do that
236
237- (NSRange)selectedRange {
238 TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
239 struct _FSelPt selPt0 = [storage selPt0], selPt1 = [storage selPt1];
240 unsigned width = [storage effectiveColumnsForLine: selPt0.line];
241 unsigned pt0 = selPt0.line * width + selPt0.col;
242 ICLog(@"selPt0 %d x %d selPt1 %d x %d", selPt0.line, selPt0.col, selPt1.line, selPt1.col);
243 return NSMakeRange(pt0, selPt1.line * width + selPt1.col - pt0);
244}
245
246// XXX string isn't padded: what we need is something like the ICeCoffEE 1.2.x method, adapted to Terminal 1.2, but we've chosen another way
247- (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange;
248{
249 TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
250 struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1], realPt0, realPt1;
251 unsigned pt1 = theRange.location + theRange.length;
252 unsigned width = [storage effectiveColumnsForLine: 0];
253 NSAttributedString *str;
254
255 realPt0.line = theRange.location / width;
256 realPt0.col = theRange.location % width;
257 realPt1.line = pt1 / width;
258 realPt1.col = pt1 % width;
259
260 [storage startSelectionAtLine: realPt0.line offset: realPt0.col];
261 [storage endSelectionAtLine: realPt1.line offset: realPt1.col];
262
263 str = [[NSAttributedString alloc] initWithString: [storage selectedString]];
264
265 NSAssert2([str length] == theRange.length, @"Substring has length %lu when we expected %lu", [str length], theRange.length);
266
267 [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col];
268 [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col];
269
270 return [str autorelease];
271}
272
273static NSEvent *ICCF_downEvent;
274static unsigned int ICCF_line;
275static unsigned short ICCF_col;
276static BOOL ICCF_optionClickRegistered;
277
278- (void)setSelectedRange:(NSRange)charRange affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag;
279{
280 TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
281 unsigned width = [storage effectiveColumnsForLine: 0];
282 unsigned pt1 = charRange.location + charRange.length;
283 [storage startSelectionAtLine: charRange.location / width offset: charRange.location % width];
284 [storage endSelectionAtLine: pt1 / width offset: pt1 % width];
285 // [self refresh];
286}
287
288void ICCF_LaunchURLFromTerminal(ICeCoffEETerminal *self) {
289 NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
290
291 ICeCoffEETerminalRange *termRange = nil, *selRange = nil;
292 NSString *s;
293 NSRange range, delimiterRange;
294
295 NS_DURING
296
297 TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
298
299 if ([storage hasSelection] && [storage isSelected: ICCF_line : ICCF_col]) {
300 selRange = [ICeCoffEETerminalRange rangeWithTerminal: self];
301 } else { // select something
302 // XXX test this next line, it may be what's causing a Terminal bug to exhibit itself
303 [storage selectWordAtLine: ICCF_line offset: ICCF_col];
304 selRange = [ICeCoffEETerminalRange rangeWithTerminal: self];
305 NSCAssert(![selRange rangeIsEmpty], ICCF_LocalizedString(@"Sorry, ICeCoffEE was unable to find anything to select"));
306 }
307
308 // However, word selection does not capture even the approximate boundaries of a URL
309 // (text to a space/line ending character); it'll stop at a period in the middle of a hostname.
310 // So, we expand it as follows:
311
312 ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);
313
314 termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt0] pt1: [selRange pt0]];
315 [termRange growBackwardByLength: ICCF_MAX_URL_LEN]; // potentially too big
316
317expandFront:
318 s = [termRange stringFromRange];
319 ICLog(@"front %@", termRange);
320 delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
321 options: NSLiteralSearch | NSBackwardsSearch];
322 if (delimiterRange.location == NSNotFound) {
323 // extend to beginning of string (as much as possible)
324 [selRange growBackwardByLength: [s length]];
325 } else {
326 NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
327 [selRange growBackwardByLength: [s length] - delimiterRange.location - 1];
328 // in https://www-s.acm.uiuc.edu/wiki/space/(user)njriley - handle clicking inside or after (user).
329 if ([s characterAtIndex: delimiterRange.location] == '(') {
330 s = [selRange stringFromRange];
331 if ([s rangeOfString: @")"].location != NSNotFound ||
332 [s rangeOfCharacterFromSet: [NSCharacterSet characterSetWithCharactersInString: @"/."]].location == NSNotFound) {
333 [selRange growBackwardByLength: 1];
334 ICLog(@"expanding past (, now %@", selRange);
335 [termRange shrinkBackByLength: [[termRange stringFromRange] length] - delimiterRange.location];
336 goto expandFront;
337 }
338 }
339 }
340
341 ICLog(@"parsed front %@", selRange);
342
343 termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt1] pt1: [selRange pt1]];
344 [termRange growForwardByLength: ICCF_MAX_URL_LEN]; // potentially too big
345
346expandBack:
347 s = [termRange stringFromRange];
348 ICLog(@"back %@", termRange);
349 delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
350 options: NSLiteralSearch];
351 if (delimiterRange.location == NSNotFound) {
352 // extend to end of string
353 [selRange growForwardByLength: [s length]];
354 } else {
355 NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
356 [selRange growForwardByLength: delimiterRange.location];
357 // URL may look like "https://www-s.acm.uiuc.edu/wiki/space/(user" now; expand if so
358 if ([s characterAtIndex: delimiterRange.location] == ')' &&
359 [[selRange stringFromRange] rangeOfString: @"("].location != NSNotFound) {
360 [selRange growForwardByLength: 1];
361 ICLog(@"expanding past ), now %@", selRange);
362 [termRange shrinkFrontByLength: delimiterRange.location + 1];
363 goto expandBack;
364 }
365 }
366
367 ICCF_StartIC();
368
369 s = [selRange stringFromRange];
370
371 range = NSMakeRange(0, [s length]);
372
373 ICCF_CheckRange(range);
374
375 ICLog(@"parsed back %@", selRange);
376 ICLog(@"range of string %@", NSStringFromRange(range));
377 ICCF_ParseURL(s, &range);
378 ICLog(@"parsed range %@ |%@|", NSStringFromRange(range), [s substringWithRange: range]);
379
380 [selRange shrinkFrontByLength: range.location];
381 [selRange shrinkBackByLength: [s length] - range.length - range.location];
382
383 s = [selRange stringFromRange];
384 ICLog(@"reconstituted URL %@", selRange);
385
386 [selRange select];
387 [self setNeedsDisplay];
388 [[self superview] display];
389
390 if (ICCF_LaunchURL(s, ICCF_KeyboardAction([NSApp currentEvent])) && ICCF_prefs.textBlinkEnabled) {
391 int i;
392 // Terminal flashes the selection one more time, so blink one fewer
393 for (i = 1 ; i < ICCF_prefs.textBlinkCount ; i++) {
394 [storage clearSelection];
395 [self setNeedsDisplay];
396 [[self superview] display];
397 usleep(kICBlinkDelayUsecs);
398 [selRange select];
399 [self setNeedsDisplay];
400 [[self superview] display];
401 usleep(kICBlinkDelayUsecs);
402 }
403 }
404 NS_HANDLER
405 ICCF_HandleException(localException);
406 NS_ENDHANDLER
407
408 ICCF_StopIC();
409}
410
411- (void)_optionClickEvent:(NSEvent *)event:(unsigned int)row:(unsigned short)column;
412{
413 if (ICCF_downEvent != nil) {
414 ICCF_line = row; // XXX are these lines or rows? check with wrapping
415 ICCF_col = column;
416 ICCF_optionClickRegistered = YES;
417 } else {
418 [super _optionClickEvent: event :row :column];
419 }
420}
421
422- (void)mouseUp:(NSEvent *)upEvent;
423{
424 ICLog(@"ICeCoffEE Terminal up: %@", upEvent);
425 [super mouseUp: upEvent];
426 if (ICCF_downEvent != nil) {
427 NSPoint downPt = [ICCF_downEvent locationInWindow];
428 NSPoint upPt = [upEvent locationInWindow];
429 [ICCF_downEvent release];
430 ICCF_downEvent = nil;
431 if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
432 if (ICCF_optionClickRegistered) {
433 ICCF_optionClickRegistered = NO;
434 ICLog(@"========= launching... %d x %d", ICCF_line, ICCF_col);
435 ICCF_LaunchURLFromTerminal(self);
436 }
437 }
438 }
439}
440
441- (void)mouseDown:(NSEvent *)downEvent;
442{
443 if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(downEvent)) {
444 NSEvent *optionClickEvent = [NSEvent mouseEventWithType: NSLeftMouseDown location: [downEvent locationInWindow] modifierFlags: NSAlternateKeyMask timestamp: [downEvent timestamp] windowNumber: [downEvent windowNumber] context: [downEvent context] eventNumber: [downEvent eventNumber] clickCount: 1 pressure: 0];
445 [ICCF_downEvent release];
446 ICCF_downEvent = [downEvent retain];
447 ICLog(@"ICeCoffEE Terminal constructed: %@", optionClickEvent);
448 ICCF_optionClickRegistered = NO;
449 [super mouseDown: optionClickEvent];
450 } else {
451 [super mouseDown: downEvent];
452 }
453}
454
455// NSDraggingDestination
456// -[TermSubview draggingUpdated:] just invokes draggingEntered...
457// XXX Crashing on repeated drag snap-back can happen even without ICeCoffEE installed; don't bother to try to fix
458- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
459{
460 if (!ICCF_prefs.terminalRequireOptionForSelfDrag || [sender draggingSource] != self || ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)) {
461 [super draggingEntered: sender];
462 // When doing non-self drags, this works around one bug in Terminal wherein the option key acts as a toggle, and it shouldn't (see Aqua HIG). Unfortunately, this messes up drag feedback for self drags, but I don't know of any way to fix it. Not that most Cocoa apps get it remotely right, anyway.
463 return NSDragOperationCopy;
464 }
465 return NSDragOperationNone;
466}
467
468@end
Note: See TracBrowser for help on using the repository browser.