/* EditorTextView.m Implementation of the EditorTextView class for the ProjectManager application. Copyright (C) 2005, 2006 Saso Kiselkov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #import "EditorTextView.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "SourceEditorDocument.h" // the maximum number of highlighted characters at the same time #define MAX_HIGHLIGHTED_INDEXES 2 static inline float my_abs(float aValue) { if (aValue >= 0) { return aValue; } else { return -aValue; } } /** * Computes the indenting offset of the last line before the passed * start offset containg text in the passed string, e.g. * * ComputeIndentingOffset(@" Hello World", 12) = 2 * ComputeIndentingOffset(@" Try this one out\n" * @" ", 27) = 4 * * @argument string The string in which to do the computation. * @argument start The start offset from which to start looking backwards. * @return The ammount of spaces the last line containing text is offset * from it's start. */ static int ComputeIndentingOffset(NSString * string, unsigned int start) { SEL sel = @selector(characterAtIndex:); unichar (* charAtIndex)(NSString *, SEL, unsigned int) = (unichar (*)(NSString *, SEL, unsigned int)) [string methodForSelector: sel]; unichar c; int firstCharOffset = -1; int offset; int startOffsetFromLineStart = -1; for (offset = start - 1; offset >= 0; offset--) { c = charAtIndex(string, sel, offset); if (c == '\n') { if (startOffsetFromLineStart < 0) { startOffsetFromLineStart = start - offset - 1; } if (firstCharOffset >= 0) { firstCharOffset = firstCharOffset - offset - 1; break; } } else if (!isspace(c)) { firstCharOffset = offset; } } if (firstCharOffset >= 0) { // if the indenting of the current line is lower than the indenting // of the previous actual line, we return the lower indenting if (startOffsetFromLineStart >= 0 && startOffsetFromLineStart < firstCharOffset) { return startOffsetFromLineStart; } // otherwise we return the actual indenting, so that any excess // space is trimmed and the lines are aligned according the last // indenting level else { return firstCharOffset; } } else { return 0; } } @interface EditorTextView (Private) - (void) insertSpaceFillAlignedAtTabsOfSize: (unsigned int) tabSize; - (void) updateDisplayOnInsertionPointCrosshairs; - (void) updateDisplayOnGuide: (EditorGuide *) aGuide; - (EditorGuide *) guideHitByPoint: (NSPoint) p; @end @implementation EditorTextView (Private) /** * Makes the receiver insert as many spaces at the current insertion * location as are required to reach the nearest tab-character boundary. * * @argument tabSize Specifies how many spaces represent one tab. */ - (void) insertSpaceFillAlignedAtTabsOfSize: (unsigned int) tabSize { char buf[tabSize]; NSString * string = [self string]; unsigned int lineLength; SEL sel = @selector(characterAtIndex:); unichar (* charAtIndex)(NSString*, SEL, unsigned int) = (unichar (*)(NSString*, SEL, unsigned int)) [string methodForSelector: sel]; int i; int skip; // computes the length of the current line for (i = [self selectedRange].location - 1, lineLength = 0; i >= 0; i--, lineLength++) { if (charAtIndex(string, sel, i) == '\n') { break; } } skip = tabSize - (lineLength % tabSize); if (skip == 0) { skip = tabSize; } memset(buf, ' ', skip); [super insertText: [NSString stringWithCString: buf length: skip]]; } /** * Makes us perform -setNeedsDisplayInRect: on a minimal space that * only includes the insertion point crosshairs. */ - (void) updateDisplayOnInsertionPointCrosshairs { NSRect visible = [self visibleRect]; if (drawsColumnIndicator) { [self setNeedsDisplayInRect: NSMakeRect(NSMinX(_insertionPointRect), NSMinY(visible), 1, visible.size.height)]; } [self setNeedsDisplayInRect: NSMakeRect(NSMinX(visible), NSMaxY(_insertionPointRect) + 1, visible.size.width, 1)]; } /** * Makes us perform -setNeedsDisplayInRect: on a minimal space that * only includes the guide `aGuide'. */ - (void) updateDisplayOnGuide: (EditorGuide *) aGuide { NSRect visible = [self visibleRect]; float offset = [aGuide offset]; if ([aGuide isHorizontal]) { if (offset >= NSMinY(visible) && offset <= NSMaxY(visible)) { [self setNeedsDisplayInRect: NSMakeRect(visible.origin.x, offset, visible.size.width, 1)]; } } else { if (offset >= NSMinX(visible) && offset <= NSMaxX(visible)) { [self setNeedsDisplayInRect: NSMakeRect(offset, visible.origin.y, 1, visible.size.height)]; } } } /** * Returns the guide hit by point `p'. If no guide is hit, `nil' is * returned instead. */ - (EditorGuide *) guideHitByPoint: (NSPoint) p { NSEnumerator * e = [guides objectEnumerator]; EditorGuide * guide; while ((guide = [e nextObject]) != nil) { float offset1 = [guide offset], offset2 = [guide isHorizontal] ? p.y : p.x; // we accept a vicinity of 2 points around the line if (my_abs(offset1 - offset2) <= 2) { return guide; } } return nil; } @end @implementation EditorTextView - (void) dealloc { TEST_RELEASE(crosshairColor); TEST_RELEASE(highlighter); TEST_RELEASE(guides); [super dealloc]; } - (void) awakeFromNib { NSData * data; NSUserDefaults * df = [NSUserDefaults standardUserDefaults]; drawCrosshairs = [df boolForKey: @"DrawCrosshairs"]; if (drawCrosshairs) { if ((data = [df dataForKey: @"CrosshairColor"]) == nil || (crosshairColor = [NSKeyedUnarchiver unarchiveObjectWithData: data]) == nil) { crosshairColor = [NSColor lightGrayColor]; } [crosshairColor retain]; } guides = [NSMutableArray new]; } - (void) drawRect: (NSRect) r { NSLayoutManager * layoutManager; NSTextContainer * textContainer; NSEnumerator * e; EditorGuide * guide; NSRange drawnRange; unsigned int highlightIndexes[MAX_HIGHLIGHTED_INDEXES]; unsigned int i, nIndexes; if (drawingRecursionProtectionActive) { return; } drawingRecursionProtectionActive = YES; layoutManager = [self layoutManager]; textContainer = [self textContainer]; drawnRange = [layoutManager glyphRangeForBoundingRect: r inTextContainer: textContainer]; [highlighter highlightRange: drawnRange]; [super drawRect: r]; // draw any guidelines e = [guides objectEnumerator]; while ((guide = [e nextObject]) != nil) { [guide drawRect: r]; } // then draw the crosshairs if (drawCrosshairs && !NSEqualRects(_insertionPointRect, NSZeroRect)) { NSRect visible = [self visibleRect]; [crosshairColor set]; // reset dashing, as that might have been changed by the guidelines PSsetdash(NULL, 0, 0.0); if (drawsColumnIndicator) { PSmoveto(NSMinX(_insertionPointRect), NSMinY(visible)); PSrlineto(0, NSMinY(_insertionPointRect) - NSMinY(visible)); PSstroke(); PSmoveto(NSMinX(_insertionPointRect), NSMaxY(_insertionPointRect)); PSrlineto(0, NSMaxY(visible) - NSMaxY(_insertionPointRect)); PSstroke(); } PSmoveto(NSMinX(visible), NSMaxY(_insertionPointRect) + 1); PSrlineto(visible.size.width, 0); PSstroke(); } // now draw the highlighted indexes nIndexes = [[editorDocument highlightedCharacterIndexes] getIndexes: highlightIndexes maxCount: MAX_HIGHLIGHTED_INDEXES inIndexRange: NULL]; [[editorDocument highlightColor] set]; if (nIndexes > 0) { NSRange r; NSRect boundingRect; r = NSMakeRange (highlightIndexes[0], 1); boundingRect = [layoutManager boundingRectForGlyphRange: r inTextContainer: textContainer]; // widen the line PSsetlinewidth (2.0); PSmoveto (NSMidX (boundingRect), NSMinY (boundingRect)); PSrlineto (-NSWidth (boundingRect) / 2, 0); PSrlineto (0, NSHeight (boundingRect)); PSrlineto (NSWidth (boundingRect) / 2, 0); PSstroke (); } if (nIndexes > 1) { NSRange r; NSRect boundingRect; r = NSMakeRange (highlightIndexes[1], 1); boundingRect = [layoutManager boundingRectForGlyphRange: r inTextContainer: textContainer]; PSmoveto (NSMidX (boundingRect), NSMinY (boundingRect)); PSrlineto (NSWidth (boundingRect) / 2, 0); PSrlineto (0, NSHeight (boundingRect)); PSrlineto (-NSWidth (boundingRect) / 2, 0); PSstroke (); } drawingRecursionProtectionActive = NO; } - (void) createSyntaxHighlighterForFileType: (NSString *) fileType { ASSIGN (highlighter, [[[HKSyntaxHighlighter alloc] initWithHighlighterType: fileType textStorage: [self textStorage] defaultTextColor: [editorDocument textColor]] autorelease]); } - (void) insertText: text { if ([text isKindOfClass: [NSString class]]) { NSString * string = text; if ([string isEqualToString: @"\n"]) { if ([[NSUserDefaults standardUserDefaults] boolForKey: @"ReturnDoesAutoindent"]) { int offset = ComputeIndentingOffset([self string], [self selectedRange].location); char * buf; buf = (char *) malloc((offset + 2) * sizeof(unichar)); buf[0] = '\n'; memset(&buf[1], ' ', offset); buf[offset+1] = '\0'; [super insertText: [NSString stringWithCString: buf]]; free(buf); } else { [super insertText: text]; } } else if ([string isEqualToString: @"\t"]) { switch ([[NSUserDefaults standardUserDefaults] integerForKey: @"TabConversion"]) { case 0: // no conversion [super insertText: text]; break; case 1: // 2 spaces [super insertText: @" "]; break; case 2: // 4 spaces [super insertText: @" "]; break; case 3: // 8 spaces [super insertText: @" "]; break; case 4: // aligned to tab boundaries of 2 spaces long tabs [self insertSpaceFillAlignedAtTabsOfSize: 2]; break; case 5: // aligned to tab boundaries of 4 spaces long tabs [self insertSpaceFillAlignedAtTabsOfSize: 4]; break; case 6: // aligned to tab boundaries of 8 spaces long tabs [self insertSpaceFillAlignedAtTabsOfSize: 8]; break; } } else { [super insertText: text]; } } else { [super insertText: text]; } } /* This extra change tracking is required in order to inform the document * that the text is changing _before_ it actually changes. This is required * so that the document can un-highlight any highlit characters before the * change occurs and after the change recompute any new highlighting. */ - (void) keyDown: (NSEvent *) ev { if (drawCrosshairs) [self updateDisplayOnInsertionPointCrosshairs]; [editorDocument editorTextViewWillPressKey: self]; // allow the user to insert the tab character directly when holding // down the Shift key if ([[ev characters] isEqualToString: @"\t"] && [ev modifierFlags] & NSShiftKeyMask) { [super insertText: @"\t"]; } // otherwise take the standard path else { [super keyDown: ev]; } [editorDocument editorTextViewDidPressKey: self]; if (drawCrosshairs) [self updateDisplayOnInsertionPointCrosshairs]; } - (void) paste: sender { if (drawCrosshairs) [self updateDisplayOnInsertionPointCrosshairs]; [editorDocument editorTextViewWillPressKey: self]; [super paste: sender]; [editorDocument editorTextViewDidPressKey: self]; if (drawCrosshairs) [self updateDisplayOnInsertionPointCrosshairs]; } - (void) mouseDown: (NSEvent *) ev { EditorGuide * guide; guide = [self guideHitByPoint: [self convertPoint: [ev locationInWindow] fromView: nil]]; if (guide != nil) { [self beginDraggingGuide: guide]; } else { if (drawCrosshairs) [self updateDisplayOnInsertionPointCrosshairs]; [super mouseDown: ev]; if (drawCrosshairs) [self updateDisplayOnInsertionPointCrosshairs]; } } - (void) setDrawsColumnIndicationGuideline: (BOOL) flag { drawsColumnIndicator = flag; [self updateDisplayOnInsertionPointCrosshairs]; } - (BOOL) drawsColumnIndicationGuideline { return drawsColumnIndicator; } - (NSRect) selectionRect { return _insertionPointRect; } /** * Creates a new guide and start dragging it. If `aStyle' isn't * -1, the guide's style is set to aStyle. */ - (void) createAndBeginDraggingNewGuide: (BOOL) isHorizontal withStyle: (EditorGuideStyle) aStyle { EditorGuide * guide; guide = [[EditorGuide new] autorelease]; [guide setHorizontal: isHorizontal]; if (aStyle != AnyGuideStyle) { [guide setGuideStyle: aStyle]; } [guides addObject: guide]; [self beginDraggingGuide: guide]; } /** * Invoked when a guide is to be dragged. */ - (void) beginDraggingGuide: (EditorGuide *) aGuide { NSEvent * ev; BOOL isHorizontal = [aGuide isHorizontal]; NSCursor * cursor; // change the cursor if (isHorizontal) { cursor = [NSCursor resizeUpDownCursor]; } else { cursor = [NSCursor resizeLeftRightCursor]; } [cursor push]; [aGuide setSelected: YES]; [self updateDisplayOnGuide: aGuide]; // start tracking the mouse while ([(ev = [[self window] nextEventMatchingMask: NSAnyEventMask]) type] != NSLeftMouseUp) { if ([ev type] == NSLeftMouseDragged) { NSPoint p = [self convertPoint: [ev locationInWindow] fromView: nil]; // now update the guide's position if (isHorizontal) { [self updateDisplayOnGuide: aGuide]; [aGuide setOffset: p.y]; [self updateDisplayOnGuide: aGuide]; } else { [self updateDisplayOnGuide: aGuide]; [aGuide setOffset: p.x]; [self updateDisplayOnGuide: aGuide]; } } } [cursor pop]; [aGuide setSelected: NO]; [self updateDisplayOnGuide: aGuide]; { NSRect visible = [self visibleRect]; float offset = [aGuide offset]; if ([aGuide isHorizontal]) { if (offset < NSMinY(visible) || offset > NSMaxY(visible)) { [guides removeObject: aGuide]; } } else { if (offset < NSMinX(visible) || offset > NSMaxX(visible)) { [guides removeObject: aGuide]; } } } } /** * Makes the text view mark it's highlighted character positions as * needing redisplay. */ - (void) setNeedsDisplayInHighlightedCharacters: (BOOL) flag { if (flag) { unsigned int highlightIndexes[MAX_HIGHLIGHTED_INDEXES]; unsigned int i, nIndexes; NSLayoutManager * layoutManager = [self layoutManager]; NSTextContainer * textContainer = [self textContainer]; nIndexes = [[editorDocument highlightedCharacterIndexes] getIndexes: highlightIndexes maxCount: MAX_HIGHLIGHTED_INDEXES inIndexRange: NULL]; for (i = 0; i < nIndexes; i++) { NSRange r = NSMakeRange (highlightIndexes[i], 1); NSRect boundingRect = NSInsetRect ([layoutManager boundingRectForGlyphRange: r inTextContainer: textContainer], -1, -1); [self setNeedsDisplayInRect: boundingRect]; } } } @end