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

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

ICeCoffEE 1.4a1

File size: 16.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@end
60
61@interface ICeCoffEETerminalRange : NSObject
62{
63    TermStorage *storage;
64    struct _FSelPt pt0;
65    struct _FSelPt pt1;
66    unsigned int width;
67    unsigned int height;
68}
69
70+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal;
71+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
72
73- (id)initWithTerminal:(ICeCoffEETerminal *)terminal;
74- (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
75
76- (struct _FSelPt)pt0;
77- (struct _FSelPt)pt1;
78
79- (NSString *)stringFromRange;
80
81- (void)growBackwardByLength:(unsigned long)length;
82- (void)growForwardByLength:(unsigned long)length;
83
84- (void)shrinkBackByLength:(unsigned long)length;
85- (void)shrinkFrontByLength:(unsigned long)length;
86
87- (BOOL)rangeIsEmpty;
88
89- (void)select;
90
91@end
92
93@implementation ICeCoffEETerminalRange
94
95+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal;
96{
97    return [[[self alloc] initWithTerminal: terminal] autorelease];
98}
99
100+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
101{
102    return [[[self alloc] initWithTerminal: terminal pt0: selPt0 pt1: selPt1] autorelease];
103}
104
105- (id)initWithTerminal:(ICeCoffEETerminal *)terminal;
106{
107    if ( (self = [self init]) != nil) {
108        storage = [(TermController *)[terminal valueForKey: @"controller"] storage];
109        pt0 = [storage selPt0];
110        pt1 = [storage selPt1];
111        width = [storage effectiveColumnsForLine: pt0.line];
112        height = [storage numberOfLines];
113    }
114    return self;
115}
116
117- (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
118{
119    if ( (self = [self initWithTerminal: terminal]) != nil) {
120        pt0 = selPt0;
121        pt1 = selPt1;
122    }
123    return self;
124}
125
126- (struct _FSelPt)pt0;
127{
128    return pt0;
129}
130
131- (struct _FSelPt)pt1;
132{
133    return pt1;
134}
135
136- (NSString *)stringFromRange;
137{
138    struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1];
139    NSString *str;
140
141    [storage startSelectionAtLine: pt0.line offset: pt0.col];
142    [storage endSelectionAtLine: pt1.line offset: pt1.col];
143
144    str = [storage selectedString];
145
146    [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col];
147    [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col];
148
149    return str;
150}
151
152- (struct _FSelPt)_moveBack:(struct _FSelPt)pt byLength:(unsigned long)length;
153{
154    unsigned int extraLines = length / width;
155    if ((long)(pt.line - extraLines) < 0) {
156        pt.line = 0;
157        pt.col = 0;
158    } else if (pt.line - extraLines == 0) {
159        pt.line = 0;
160        pt.col += width - (length % height);
161        if (pt.col < width) pt.col = 0;
162        else pt.col -= width;
163    } else {
164        pt.line -= extraLines;
165        pt.col += width - (length % height);
166        if (pt.col < width) pt.line--;
167        else pt.col -= width;
168    }
169    return pt;
170}
171
172- (struct _FSelPt)_moveForward:(struct _FSelPt)pt byLength:(unsigned long)length;
173{
174    unsigned int extraLines = length / width;
175    if (pt.line + extraLines >= height) {
176        pt.line = height - 1;
177        pt.col = width - 1;
178    } else if (pt.line + extraLines == height - 1) {
179        pt.line = height - 1;
180        pt.col += length % height;
181        if (pt.col >= width) pt.col = width - 1;
182    } else {
183        pt.line += extraLines;
184        pt.col = pt.col + (length % height);
185        if (pt.col > width) {
186            pt.line++;
187            pt.col -= width;
188        }
189    }
190    return pt;
191}
192
193- (BOOL)rangeIsEmpty;
194{
195    return (pt1.line < pt0.line) || (pt1.line == pt0.line && pt1.col <= pt0.col);
196}
197
198- (void)growBackwardByLength:(unsigned long)length;
199{
200    pt0 = [self _moveBack: pt0 byLength: length];
201}
202
203- (void)growForwardByLength:(unsigned long)length;
204{
205    pt1 = [self _moveForward: pt1 byLength: length];
206}
207
208- (void)shrinkFrontByLength:(unsigned long)length;
209{
210    pt0 = [self _moveForward: pt0 byLength: length];
211}
212
213- (void)shrinkBackByLength:(unsigned long)length;
214{
215    pt1 = [self _moveBack: pt1 byLength: length];
216}
217
218- (void)select;
219{
220    [storage startSelectionAtLine: pt0.line offset: pt0.col];
221    [storage endSelectionAtLine: pt1.line offset: pt1.col];
222}
223
224- (NSString *)description;
225{
226    return [NSString stringWithFormat: @"L%uC%hu - L%uC%hu: |%@|", pt0.line, pt0.col, pt1.line, pt1.col, [[self stringFromRange] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
227}
228
229@end
230
231// TermSubview's NSTextInput implementation is essentially a noop: make one that does something
232@implementation ICeCoffEETerminal // XXX normally in category (NSTextInput), but APE doesnÕt work if I do that
233
234- (NSRange)selectedRange {
235    TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
236    struct _FSelPt selPt0 = [storage selPt0], selPt1 = [storage selPt1];
237    unsigned width = [storage effectiveColumnsForLine: selPt0.line];
238    unsigned pt0 = selPt0.line * width + selPt0.col;
239    ICLog(@"selPt0 %d x %d selPt1 %d x %d", selPt0.line, selPt0.col, selPt1.line, selPt1.col);
240    return NSMakeRange(pt0, selPt1.line * width + selPt1.col - pt0);
241}
242
243// 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
244- (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange;
245{
246    TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
247    struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1], realPt0, realPt1;
248    unsigned pt1 = theRange.location + theRange.length;
249    unsigned width = [storage effectiveColumnsForLine: 0];
250    NSAttributedString *str;
251
252    realPt0.line = theRange.location / width;
253    realPt0.col = theRange.location % width;
254    realPt1.line = pt1 / width;
255    realPt1.col = pt1 % width;
256
257    [storage startSelectionAtLine: realPt0.line offset: realPt0.col];
258    [storage endSelectionAtLine: realPt1.line offset: realPt1.col];
259
260    str = [[NSAttributedString alloc] initWithString: [storage selectedString]];
261
262    NSAssert2([str length] == theRange.length, @"Substring has length %lu when we expected %lu", [str length], theRange.length);
263
264    [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col];
265    [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col];
266   
267    return [str autorelease];
268}
269
270static NSEvent *ICCF_downEvent;
271static unsigned int ICCF_line;
272static unsigned short ICCF_col;
273static BOOL ICCF_optionClickRegistered;
274
275- (void)setSelectedRange:(NSRange)charRange affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag;
276{
277    TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
278    unsigned width = [storage effectiveColumnsForLine: 0];
279    unsigned pt1 = charRange.location + charRange.length;
280    [storage startSelectionAtLine: charRange.location / width offset: charRange.location % width];
281    [storage endSelectionAtLine: pt1 / width offset: pt1 % width];
282    // [self refresh];
283}
284
285void ICCF_LaunchURLFromTerminal(ICeCoffEETerminal *self) {
286    NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
287
288    ICeCoffEETerminalRange *termRange = nil, *selRange = nil;
289    NSString *s;
290    NSRange range, delimiterRange;
291
292    NS_DURING
293
294        TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
295
296        if ([storage hasSelection] && [storage isSelected: ICCF_line : ICCF_col]) {
297            selRange = [ICeCoffEETerminalRange rangeWithTerminal: self];
298        } else { // select something
299            [storage selectWordAtLine: ICCF_line offset: ICCF_col];
300            selRange = [ICeCoffEETerminalRange rangeWithTerminal: self];
301            NSCAssert(![selRange rangeIsEmpty], ICCF_LocalizedString(@"Sorry, ICeCoffEE was unable to find anything to select"));
302        }
303
304        // However, word selection does not capture even the approximate boundaries of a URL
305        // (text to a space/line ending character); it'll stop at a period in the middle of a hostname.
306        // So, we expand it as follows:
307
308        ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);
309
310        termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt0] pt1: [selRange pt0]];
311        // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...)
312        [termRange growForwardByLength: 1];
313        [termRange growBackwardByLength: ICCF_MAX_URL_LEN]; // potentially too big
314        s = [termRange stringFromRange];
315        ICLog(@"front %@", termRange);
316        delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
317                                            options: NSLiteralSearch | NSBackwardsSearch];
318        if (delimiterRange.location == NSNotFound) {
319            // extend to beginning of string (as much as possible)
320            [selRange growBackwardByLength: [s length] - 1];
321        } else {
322            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
323            [selRange growBackwardByLength: [s length] - delimiterRange.location - 2];
324        }
325
326        ICLog(@"parsed front %@", selRange);
327
328        termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt1] pt1: [selRange pt1]];
329        // subtract 1 from range to trap delimiters that are on the edge of the selection (i.e., ...>)
330        [termRange growBackwardByLength: 1];
331        [termRange growForwardByLength: ICCF_MAX_URL_LEN]; // potentially too big
332        s = [termRange stringFromRange];
333        ICLog(@"back %@", termRange);
334        delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
335                                            options: NSLiteralSearch];
336        if (delimiterRange.location == NSNotFound) {
337            // extend to end of string
338            [selRange growForwardByLength: [s length] - 1];
339        } else {
340            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
341            [selRange growForwardByLength: delimiterRange.location - 1];
342        }
343
344        ICCF_StartIC();
345
346        s = [selRange stringFromRange];
347
348        range = NSMakeRange(0, [s length]);
349
350        ICCF_CheckRange(range);
351
352        ICLog(@"parsed back %@", selRange);
353        ICLog(@"range of string %@", NSStringFromRange(range));
354        ICCF_ParseURL(s, &range);
355        ICLog(@"parsed range %@ |%@|", NSStringFromRange(range), [s substringWithRange: range]);
356
357        [selRange shrinkFrontByLength: range.location];
358        [selRange shrinkBackByLength: [s length] - range.length - range.location];
359
360        s = [selRange stringFromRange];
361        ICLog(@"reconstituted URL %@", selRange);
362
363        [selRange select];
364        [self setNeedsDisplay];
365        [[self superview] display];
366
367        ICCF_LaunchURL(s, ICCF_KeyboardAction());
368       
369        if (ICCF_prefs.textBlinkEnabled) {
370            int i;
371            // Terminal flashes the selection one more time, so blink one fewer
372            for (i = 1 ; i < ICCF_prefs.textBlinkCount ; i++) {
373                [storage clearSelection];
374                [self setNeedsDisplay];
375                [[self superview] display];
376                usleep(kICBlinkDelayUsecs);
377                [selRange select];
378                [self setNeedsDisplay];
379                [[self superview] display];
380                usleep(kICBlinkDelayUsecs);
381            }
382        }
383    NS_HANDLER
384        ICCF_HandleException(localException);
385    NS_ENDHANDLER
386
387    ICCF_StopIC();
388}
389
390- (void)_optionClickEvent:(NSEvent *)event:(unsigned int)row:(unsigned short)column;
391{
392    if (ICCF_downEvent != nil) {
393        ICCF_line = row; // XXX are these lines or rows? check with wrapping
394        ICCF_col = column;
395        ICCF_optionClickRegistered = YES;
396    } else {
397        [super _optionClickEvent: event :row :column];
398    }
399}
400
401- (void)mouseUp:(NSEvent *)upEvent;
402{
403    ICLog(@"ICeCoffEE Terminal up: %@", upEvent);
404    [super mouseUp: upEvent];
405    if (ICCF_downEvent != nil) {
406        NSPoint downPt = [ICCF_downEvent locationInWindow];
407        NSPoint upPt = [upEvent locationInWindow];
408        [ICCF_downEvent release];
409        ICCF_downEvent = nil;
410        if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
411            if (ICCF_optionClickRegistered) {
412                ICCF_optionClickRegistered = NO;
413                ICLog(@"========= launching... %d x %d", ICCF_line, ICCF_col);
414                ICCF_LaunchURLFromTerminal(self);
415            }
416        }
417    }
418}
419
420- (void)mouseDown:(NSEvent *)downEvent;
421{
422    if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(downEvent)) {
423        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];
424        [ICCF_downEvent release];
425        ICCF_downEvent = [downEvent retain];
426        ICLog(@"ICeCoffEE Terminal constructed: %@", optionClickEvent);
427        ICCF_optionClickRegistered = NO;
428        [super mouseDown: optionClickEvent];
429    } else {
430        [super mouseDown: downEvent];
431    }
432}
433
434// NSDraggingDestination
435// -[TermSubview draggingUpdated:] just invokes draggingEntered...
436// XXX Crashing on repeated drag snap-back can happen even without ICeCoffEE installed; don't bother to try to fix
437- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
438{
439    if (!ICCF_prefs.terminalRequireOptionForSelfDrag || [sender draggingSource] != self || ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)) {
440        [super draggingEntered: sender];
441        // 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.
442        return NSDragOperationCopy;
443    }
444    return NSDragOperationNone;
445}
446
447@end
Note: See TracBrowser for help on using the repository browser.