Rev 5808 | Rev 6232 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
//// NoodleLineNumberView.m// Line View Test//// Created by Paul Kim on 9/28/08.// Copyright (c) 2008 Noodlesoft, LLC. All rights reserved.//// Permission is hereby granted, free of charge, to any person// obtaining a copy of this software and associated documentation// files (the "Software"), to deal in the Software without// restriction, including without limitation the rights to use,// copy, modify, merge, publish, distribute, sublicense, and/or sell// copies of the Software, and to permit persons to whom the// Software is furnished to do so, subject to the following// conditions://// The above copyright notice and this permission notice shall be// included in all copies or substantial portions of the Software.//// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR// OTHER DEALINGS IN THE SOFTWARE.//// This version of the NoodleLineNumberView was improved by the Sequel Pro team// http://www.sequelpro.com which removes marker// functionality, adds selection by clicking/dragging on the ruler, and// it was optimised for speed.#import "NoodleLineNumberView.h"#import "RScriptEditorTextView.h"#include <tgmath.h>#pragma mark NSCoding methods#define NOODLE_FONT_CODING_KEY @"font"#define NOODLE_TEXT_COLOR_CODING_KEY @"textColor"// #define NOODLE_ALT_TEXT_COLOR_CODING_KEY @"alternateTextColor"// #define NOODLE_BACKGROUND_COLOR_CODING_KEY @"backgroundColor"#pragma mark -#define DEFAULT_THICKNESS 22.0f#define RULER_MARGIN 5.0f#define RULER_MARGIN2 RULER_MARGIN * 2// Cache loop methods for speed#pragma mark -@interface NoodleLineNumberView (Private)- (NSArray *)lineIndices;- (void)invalidateLineIndices;- (void)calculateLines;- (void)updateGutterThicknessConstants;@end@implementation NoodleLineNumberView// @synthesize alternateTextColor;// @synthesize backgroundColor;- (id)initWithScrollView:(NSScrollView *)aScrollView{if ((self = [super initWithScrollView:aScrollView orientation:NSVerticalRuler]) != nil){[self setClientView:[aScrollView documentView]];// [self setAlternateTextColor:[NSColor whiteColor]];lineIndices = nil;textAttributes = [[NSDictionary dictionaryWithObjectsAndKeys:[self font], NSFontAttributeName,[self textColor], NSForegroundColorAttributeName,nil] retain];NSSize s = [[NSString stringWithString:@"8"] sizeWithAttributes:textAttributes];maxWidthOfGlyph = s.width;maxHeightOfGlyph = s.height;[self updateGutterThicknessConstants];currentRuleThickness = 0.0f;// Cache loop methods for speedlineNumberForCharacterIndexSel = @selector(lineNumberForCharacterIndex:);lineNumberForCharacterIndexIMP = [self methodForSelector:lineNumberForCharacterIndexSel];lineRangeForRangeSel = @selector(lineRangeForRange:);addObjectSel = @selector(addObject:);numberWithUnsignedIntegerSel = @selector(numberWithUnsignedInteger:);numberWithUnsignedIntegerIMP = [NSNumber methodForSelector:numberWithUnsignedIntegerSel];rangeOfLineSel = @selector(getLineStart:end:contentsEnd:forRange:);currentNumberOfLines = 1;numberClass = [NSNumber class];}return self;}- (void)awakeFromNib{[self setClientView:[[self scrollView] documentView]];}- (void)dealloc{[[NSNotificationCenter defaultCenter] removeObserver:self];if (lineIndices) [lineIndices release];if (textAttributes) [textAttributes release];if (font) [font release];if (textColor) [textColor release];[super dealloc];}#pragma mark -- (void)setFont:(NSFont *)aFont{if (font != aFont){[font autorelease];font = [aFont retain];if (textAttributes) [textAttributes release];textAttributes = [[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName,[self textColor], NSForegroundColorAttributeName,nil] retain];NSSize s = [[NSString stringWithString:@"8"] sizeWithAttributes:textAttributes];maxWidthOfGlyph = s.width;maxHeightOfGlyph = s.height;[self updateGutterThicknessConstants];}}- (NSFont *)font{if (font == nil)return [NSFont labelFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]];return font;}- (void)setTextColor:(NSColor *)color{if (textColor != color){[textColor autorelease];textColor = [color retain];if (textAttributes) [textAttributes release];textAttributes = [[NSDictionary dictionaryWithObjectsAndKeys:[self font], NSFontAttributeName,textColor, NSForegroundColorAttributeName,nil] retain];NSSize s = [[NSString stringWithString:@"8"] sizeWithAttributes:textAttributes];maxWidthOfGlyph = s.width;maxHeightOfGlyph = s.height;[self updateGutterThicknessConstants];}}- (NSColor *)textColor{if (textColor == nil)return [NSColor colorWithCalibratedWhite:0.42 alpha:1.0];return textColor;}- (void)setClientView:(NSView *)aView{id oldClientView = [self clientView];if ((oldClientView != aView) && [oldClientView isKindOfClass:[NSTextView class]])[[NSNotificationCenter defaultCenter] removeObserver:self name:NSTextStorageDidProcessEditingNotification object:[(NSTextView *)oldClientView textStorage]];[super setClientView:aView];if ((aView != nil) && [aView isKindOfClass:[NSTextView class]]){layoutManager = [(NSTextView *)aView layoutManager];container = [(NSTextView *)aView textContainer];clientView = (NSTextView*)[self clientView];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:NSTextStorageDidProcessEditingNotification object:[clientView textStorage]];[self invalidateLineIndices];}}#pragma mark -- (void)textDidChange:(NSNotification *)notification{if(!clientView) return;if(![(RScriptEditorTextView*)clientView lineNumberingEnabled]) return;// Invalidate the line indices only if text view was changed in length but not if the font was changed.// They will be recalculated and recached on demand.if([[clientView textStorage] editedMask] != 1) {[self invalidateLineIndices];[self setNeedsDisplay:YES];}}- (NSUInteger)lineNumberForLocation:(CGFloat)location{NSUInteger line, count, rectCount;NSRectArray rects;NSRect visibleRect;NSRange nullRange;NSArray *lines;visibleRect = [[[self scrollView] contentView] bounds];lines = [self lineIndices];location += NSMinY(visibleRect);nullRange = NSMakeRange(NSNotFound, 0);count = [lines count];// Find the characters that are currently visibleNSRange range = [layoutManager characterRangeForGlyphRange:[layoutManager glyphRangeForBoundingRect:visibleRect inTextContainer:container] actualGlyphRange:NULL];// Fudge the range a tad in case there is an extra new line at end.// It doesn't show up in the glyphs so would not be accounted for.range.length++;for (line = (NSUInteger)(*lineNumberForCharacterIndexIMP)(self, lineNumberForCharacterIndexSel, range.location); line < count; line++){rects = [layoutManager rectArrayForCharacterRange:NSMakeRange([NSArrayObjectAtIndex(lines, line) unsignedIntegerValue], 0)withinSelectedCharacterRange:nullRangeinTextContainer:containerrectCount:&rectCount];if(!rectCount) return NSNotFound;if ((location >= NSMinY(rects[0])) && (location < NSMaxY(rects[0])))return line + 1;}return NSNotFound;}- (NSUInteger)lineNumberForCharacterIndex:(NSUInteger)index{NSUInteger left, right, mid, lineStart;NSArray *lines;lines = [self lineIndices];// Binary searchleft = 0;right = [lines count];while ((right - left) > 1){mid = (right + left) >> 1;lineStart = [NSArrayObjectAtIndex(lines, mid) unsignedIntegerValue];if (index < lineStart)right = mid;else if (index > lineStart)left = mid;elsereturn mid;}return left;}- (void)drawHashMarksAndLabelsInRect:(NSRect)aRect{NSRect bounds = [self bounds];// if (backgroundColor != nil)// {// [backgroundColor set];// NSRectFill(bounds);//// [[NSColor colorWithCalibratedWhite:0.58 alpha:1.0] set];// [NSBezierPath strokeLineFromPoint:NSMakePoint(NSMaxX(bounds) - 0.5, NSMinY(bounds)) toPoint:NSMakePoint(NSMaxX(bounds) - 0.5, NSMaxY(bounds))];// }NSRect visibleRect;NSRange range, nullRange;NSString *labelText;NSUInteger rectCount, index, line, count;NSRectArray rects;CGFloat yinset;NSArray *lines;nullRange = NSMakeRange(NSNotFound, 0);yinset = [clientView textContainerInset].height;visibleRect = [[[self scrollView] contentView] bounds];lines = [self lineIndices];count = [lines count];if(!count) return;// Find the characters that are currently visiblerange = [layoutManager characterRangeForGlyphRange:[layoutManager glyphRangeForBoundingRect:visibleRect inTextContainer:container] actualGlyphRange:NULL];// Fudge the range a tad in case there is an extra new line at end.// It doesn't show up in the glyphs so would not be accounted for.range.length++;CGFloat boundsRULERMargin2 = NSWidth(bounds) - RULER_MARGIN2;CGFloat boundsWidthRULER = NSWidth(bounds) - RULER_MARGIN;CGFloat yinsetMinY = yinset - NSMinY(visibleRect);CGFloat rectHeight;for (line = (NSUInteger)(*lineNumberForCharacterIndexIMP)(self, lineNumberForCharacterIndexSel, range.location); line < count; line++){index = [NSArrayObjectAtIndex(lines, line) unsignedIntegerValue];if (NSLocationInRange(index, range)){rects = [layoutManager rectArrayForCharacterRange:NSMakeRange(index, 0)withinSelectedCharacterRange:nullRangeinTextContainer:containerrectCount:&rectCount];if (rectCount > 0){// Note that the ruler view is only as tall as the visible// portion. Need to compensate for the clipview's coordinates.// Line numbers are internally stored starting at 0labelText = [NSString stringWithFormat:@"%lu", (NSUInteger)(line + 1)];// How many digits has the current line number?NSUInteger idx = line + 1;NSInteger numOfDigits = 0;while(idx) { numOfDigits++; idx/=10; }rectHeight = NSHeight(rects[0]);// Draw string flush right, centered vertically within the line[labelText drawInRect:NSMakeRect(boundsWidthRULER - (maxWidthOfGlyph * numOfDigits),yinsetMinY + NSMinY(rects[0]) + ((NSInteger)(rectHeight - maxHeightOfGlyph) >> 1),boundsRULERMargin2, rectHeight)withAttributes:textAttributes];}}if (index > NSMaxRange(range))break;}}- (void)mouseDown:(NSEvent *)theEvent{NSUInteger line;NSTextView *view;if (![[self clientView] isKindOfClass:[NSTextView class]]) return;view = (NSTextView *)[self clientView];line = [self lineNumberForLocation:[self convertPoint:[theEvent locationInWindow] fromView:nil].y];dragSelectionStartLine = line;if (line != NSNotFound){NSUInteger selectionStart, selectionEnd;NSArray *lines = [self lineIndices];selectionStart = [NSArrayObjectAtIndex(lines, (line - 1)) unsignedIntegerValue];if (line < [lines count]) {selectionEnd = [NSArrayObjectAtIndex(lines, line) unsignedIntegerValue];} else {selectionEnd = [[view string] length];}[view setSelectedRange:NSMakeRange(selectionStart, selectionEnd - selectionStart)];}}- (void)mouseDragged:(NSEvent *)theEvent{NSUInteger line, startLine, endLine;NSTextView *view;if (![[self clientView] isKindOfClass:[NSTextView class]] || dragSelectionStartLine == NSNotFound) return;view = (NSTextView *)[self clientView];line = [self lineNumberForLocation:[self convertPoint:[theEvent locationInWindow] fromView:nil].y];if (line != NSNotFound){NSUInteger selectionStart, selectionEnd;NSArray *lines = [self lineIndices];if (line >= dragSelectionStartLine) {startLine = dragSelectionStartLine;endLine = line;} else {startLine = line;endLine = dragSelectionStartLine;}selectionStart = [NSArrayObjectAtIndex(lines, (startLine - 1)) unsignedIntegerValue];if (endLine < [lines count]) {selectionEnd = [NSArrayObjectAtIndex(lines, endLine) unsignedIntegerValue];} else {selectionEnd = [[view string] length];}[view setSelectedRange:NSMakeRange(selectionStart, selectionEnd - selectionStart)];}[view autoscroll:theEvent];}#pragma mark -- (id)initWithCoder:(NSCoder *)decoder{if ((self = [super initWithCoder:decoder]) != nil){if ([decoder allowsKeyedCoding]){font = [[decoder decodeObjectForKey:NOODLE_FONT_CODING_KEY] retain];textColor = [[decoder decodeObjectForKey:NOODLE_TEXT_COLOR_CODING_KEY] retain];// alternateTextColor = [[decoder decodeObjectForKey:NOODLE_ALT_TEXT_COLOR_CODING_KEY] retain];// backgroundColor = [[decoder decodeObjectForKey:NOODLE_BACKGROUND_COLOR_CODING_KEY] retain];}else{font = [[decoder decodeObject] retain];textColor = [[decoder decodeObject] retain];// alternateTextColor = [[decoder decodeObject] retain];// backgroundColor = [[decoder decodeObject] retain];}}return self;}- (void)encodeWithCoder:(NSCoder *)encoder{[super encodeWithCoder:encoder];if ([encoder allowsKeyedCoding]){[encoder encodeObject:font forKey:NOODLE_FONT_CODING_KEY];[encoder encodeObject:textColor forKey:NOODLE_TEXT_COLOR_CODING_KEY];// [encoder encodeObject:alternateTextColor forKey:NOODLE_ALT_TEXT_COLOR_CODING_KEY];// [encoder encodeObject:backgroundColor forKey:NOODLE_BACKGROUND_COLOR_CODING_KEY];}else{[encoder encodeObject:font];[encoder encodeObject:textColor];// [encoder encodeObject:alternateTextColor];// [encoder encodeObject:backgroundColor];}}#pragma mark -#pragma mark PrivateAPI- (NSArray *)lineIndices{if (lineIndices == nil)[self calculateLines];return lineIndices;}- (void)invalidateLineIndices{if (lineIndices) [lineIndices release], lineIndices = nil;}- (void)calculateLines{NSUInteger index, stringLength, lineEnd, contentEnd;NSString *textString;CGFloat newThickness;textString = [clientView string];stringLength = [textString length];// Switch off line numbering if text larger than 6MB// for performance reasons.// TODO improve performance maybe via threadingif(stringLength>3000000)return;lineIndices = [[NSMutableArray alloc] initWithCapacity:currentNumberOfLines];index = 0;// Cache loop methods for speedIMP rangeOfLineIMP = [textString methodForSelector:rangeOfLineSel];addObjectIMP = [lineIndices methodForSelector:addObjectSel];do{(void)(*addObjectIMP)(lineIndices, addObjectSel, (*numberWithUnsignedIntegerIMP)(numberClass, numberWithUnsignedIntegerSel, index));(*rangeOfLineIMP)(textString, rangeOfLineSel, NULL, &index, NULL, NSMakeRange(index, 0));}while (index < stringLength);// Check if text ends with a new line.(*rangeOfLineIMP)(textString, rangeOfLineSel, NULL, &lineEnd, &contentEnd, NSMakeRange([[lineIndices lastObject] intValue], 0));if (contentEnd < lineEnd)(void)(*addObjectIMP)(lineIndices, addObjectSel, (*numberWithUnsignedIntegerIMP)(numberClass, numberWithUnsignedIntegerSel, index));NSUInteger lineCount = [lineIndices count];if(lineCount < 100)newThickness = maxWidthOfGlyph2;else if(lineCount < 1000)newThickness = maxWidthOfGlyph3;else if(lineCount < 10000)newThickness = maxWidthOfGlyph4;else if(lineCount < 100000)newThickness = maxWidthOfGlyph5;else if(lineCount < 1000000)newThickness = maxWidthOfGlyph6;else if(lineCount < 10000000)newThickness = maxWidthOfGlyph7;else if(lineCount < 100000000)newThickness = maxWidthOfGlyph8;elsenewThickness = 100;currentNumberOfLines = lineCount;if (currentRuleThickness != newThickness){currentRuleThickness = newThickness;// Not a good idea to resize the view during calculations (which can happen during// display). Do a delayed perform (using NSInvocation since arg is a float).NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(setRuleThickness:)]];[invocation setSelector:@selector(setRuleThickness:)];[invocation setTarget:self];[invocation setArgument:&newThickness atIndex:2];[invocation performSelector:@selector(invoke) withObject:nil afterDelay:0.0];}}- (void)updateGutterThicknessConstants{// maxWidthOfGlyph1 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph + RULER_MARGIN2));maxWidthOfGlyph2 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph * 2 + RULER_MARGIN2));maxWidthOfGlyph3 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph * 3 + RULER_MARGIN2));maxWidthOfGlyph4 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph * 4 + RULER_MARGIN2));maxWidthOfGlyph5 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph * 5 + RULER_MARGIN2));maxWidthOfGlyph6 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph * 6 + RULER_MARGIN2));maxWidthOfGlyph7 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph * 7 + RULER_MARGIN2));maxWidthOfGlyph8 = ceil(MAX(DEFAULT_THICKNESS, maxWidthOfGlyph * 8 + RULER_MARGIN2));}@end