diff options
author | rowanbeentje <rowan@beent.je> | 2009-04-16 23:21:40 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2009-04-16 23:21:40 +0000 |
commit | 93bf3137c544b66a84a5a780264abef960f8a1b9 (patch) | |
tree | 9a5a3f9a1bd4712639528a8f26242ea5d6fb0b37 /Source/NoodleLineNumberView.m | |
parent | 4051d858d9e189b7c5bd0919e0bf952cb42898b8 (diff) | |
download | sequelpro-93bf3137c544b66a84a5a780264abef960f8a1b9.tar.gz sequelpro-93bf3137c544b66a84a5a780264abef960f8a1b9.tar.bz2 sequelpro-93bf3137c544b66a84a5a780264abef960f8a1b9.zip |
- Implement line numbering for CMTextView:
- Add an implementation of NoodleLineNumberView, by Paul Kim. Slightly tweaked to remove markers.
- Add to CMTextView (to enable it for other CMTextView uses, hook up the scrollView outlet to the containing scroll view)
Diffstat (limited to 'Source/NoodleLineNumberView.m')
-rw-r--r-- | Source/NoodleLineNumberView.m | 493 |
1 files changed, 493 insertions, 0 deletions
diff --git a/Source/NoodleLineNumberView.m b/Source/NoodleLineNumberView.m new file mode 100644 index 00000000..c5d76187 --- /dev/null +++ b/Source/NoodleLineNumberView.m @@ -0,0 +1,493 @@ +// +// 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 for Sequel Pro removes marker +// functionality. + +#import "NoodleLineNumberView.h" + +#define DEFAULT_THICKNESS 22.0 +#define RULER_MARGIN 5.0 + +@interface NoodleLineNumberView (Private) + +- (NSMutableArray *)lineIndices; +- (void)invalidateLineIndices; +- (void)calculateLines; +- (unsigned)lineNumberForCharacterIndex:(unsigned)index inText:(NSString *)text; +- (NSDictionary *)textAttributes; + +@end + +@implementation NoodleLineNumberView + +- (id)initWithScrollView:(NSScrollView *)aScrollView +{ + if ((self = [super initWithScrollView:aScrollView orientation:NSVerticalRuler]) != nil) + { + [self setClientView:[aScrollView documentView]]; + } + return self; +} + +- (void)awakeFromNib +{ + [self setClientView:[[self scrollView] documentView]]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [lineIndices release]; + [font release]; + + [super dealloc]; +} + +- (void)setFont:(NSFont *)aFont +{ + if (font != aFont) + { + [font autorelease]; + font = [aFont retain]; + } +} + +- (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]; + } +} + +- (NSColor *)textColor +{ + if (textColor == nil) + { + return [NSColor colorWithCalibratedWhite:0.42 alpha:1.0]; + } + return textColor; +} + +- (void)setAlternateTextColor:(NSColor *)color +{ + if (alternateTextColor != color) + { + [alternateTextColor autorelease]; + alternateTextColor = [color retain]; + } +} + +- (NSColor *)alternateTextColor +{ + if (alternateTextColor == nil) + { + return [NSColor whiteColor]; + } + return alternateTextColor; +} + +- (void)setBackgroundColor:(NSColor *)color +{ + if (backgroundColor != color) + { + [backgroundColor autorelease]; + backgroundColor = [color retain]; + } +} + +- (NSColor *)backgroundColor +{ + return backgroundColor; +} + +- (void)setClientView:(NSView *)aView +{ + id oldClientView; + + 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]]) + { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:NSTextStorageDidProcessEditingNotification object:[(NSTextView *)aView textStorage]]; + + [self invalidateLineIndices]; + } +} + +- (NSMutableArray *)lineIndices +{ + if (lineIndices == nil) + { + [self calculateLines]; + } + return lineIndices; +} + +- (void)invalidateLineIndices +{ + [lineIndices release]; + lineIndices = nil; +} + +- (void)textDidChange:(NSNotification *)notification +{ + // Invalidate the line indices. They will be recalculated and recached on demand. + [self invalidateLineIndices]; + + [self setNeedsDisplay:YES]; +} + +- (unsigned)lineNumberForLocation:(float)location +{ + unsigned line, count, index, rectCount, i; + NSRectArray rects; + NSRect visibleRect; + NSLayoutManager *layoutManager; + NSTextContainer *container; + NSRange nullRange; + NSMutableArray *lines; + id view; + + view = [self clientView]; + visibleRect = [[[self scrollView] contentView] bounds]; + + lines = [self lineIndices]; + + location += NSMinY(visibleRect); + + if ([view isKindOfClass:[NSTextView class]]) + { + nullRange = NSMakeRange(NSNotFound, 0); + layoutManager = [view layoutManager]; + container = [view textContainer]; + count = [lines count]; + + for (line = 0; line < count; line++) + { + index = [[lines objectAtIndex:line] unsignedIntValue]; + + rects = [layoutManager rectArrayForCharacterRange:NSMakeRange(index, 0) + withinSelectedCharacterRange:nullRange + inTextContainer:container + rectCount:&rectCount]; + + for (i = 0; i < rectCount; i++) + { + if ((location >= NSMinY(rects[i])) && (location < NSMaxY(rects[i]))) + { + return line + 1; + } + } + } + } + return NSNotFound; +} + +- (void)calculateLines +{ + id view; + + view = [self clientView]; + + if ([view isKindOfClass:[NSTextView class]]) + { + unsigned index, numberOfLines, stringLength, lineEnd, contentEnd; + NSString *text; + float oldThickness, newThickness; + + text = [view string]; + stringLength = [text length]; + [lineIndices release]; + lineIndices = [[NSMutableArray alloc] init]; + + index = 0; + numberOfLines = 0; + + do + { + [lineIndices addObject:[NSNumber numberWithUnsignedInt:index]]; + + index = NSMaxRange([text lineRangeForRange:NSMakeRange(index, 0)]); + numberOfLines++; + } + while (index < stringLength); + + // Check if text ends with a new line. + [text getLineStart:NULL end:&lineEnd contentsEnd:&contentEnd forRange:NSMakeRange([[lineIndices lastObject] unsignedIntValue], 0)]; + if (contentEnd < lineEnd) + { + [lineIndices addObject:[NSNumber numberWithUnsignedInt:index]]; + } + + oldThickness = [self ruleThickness]; + newThickness = [self requiredThickness]; + if (fabs(oldThickness - newThickness) > 1) + { + NSInvocation *invocation; + + // 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). + 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]; + } + } +} + +- (unsigned)lineNumberForCharacterIndex:(unsigned)index inText:(NSString *)text +{ + unsigned left, right, mid, lineStart; + NSMutableArray *lines; + + lines = [self lineIndices]; + + // Binary search + left = 0; + right = [lines count]; + + while ((right - left) > 1) + { + mid = (right + left) / 2; + lineStart = [[lines objectAtIndex:mid] unsignedIntValue]; + + if (index < lineStart) + { + right = mid; + } + else if (index > lineStart) + { + left = mid; + } + else + { + return mid; + } + } + return left; +} + +- (NSDictionary *)textAttributes +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + [self font], NSFontAttributeName, + [self textColor], NSForegroundColorAttributeName, + nil]; +} + +- (float)requiredThickness +{ + unsigned lineCount, digits, i; + NSMutableString *sampleString; + NSSize stringSize; + + lineCount = [[self lineIndices] count]; + digits = (unsigned)log10(lineCount) + 1; + sampleString = [NSMutableString string]; + for (i = 0; i < digits; i++) + { + // Use "8" since it is one of the fatter numbers. Anything but "1" + // will probably be ok here. I could be pedantic and actually find the fattest + // number for the current font but nah. + [sampleString appendString:@"8"]; + } + + stringSize = [sampleString sizeWithAttributes:[self textAttributes]]; + + // Round up the value. There is a bug on 10.4 where the display gets all wonky when scrolling if you don't + // return an integral value here. + return ceilf(MAX(DEFAULT_THICKNESS, stringSize.width + RULER_MARGIN * 2)); +} + +- (void)drawHashMarksAndLabelsInRect:(NSRect)aRect +{ + id view; + NSRect bounds; + + 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))]; + } + + view = [self clientView]; + + if ([view isKindOfClass:[NSTextView class]]) + { + NSLayoutManager *layoutManager; + NSTextContainer *container; + NSRect visibleRect; + NSRange range, glyphRange, nullRange; + NSString *text, *labelText; + unsigned rectCount, index, line, count; + NSRectArray rects; + float ypos, yinset; + NSDictionary *textAttributes, *currentTextAttributes; + NSSize stringSize; + NSMutableArray *lines; + + layoutManager = [view layoutManager]; + container = [view textContainer]; + text = [view string]; + nullRange = NSMakeRange(NSNotFound, 0); + + yinset = [view textContainerInset].height; + visibleRect = [[[self scrollView] contentView] bounds]; + + textAttributes = [self textAttributes]; + + lines = [self lineIndices]; + + // Find the characters that are currently visible + glyphRange = [layoutManager glyphRangeForBoundingRect:visibleRect inTextContainer:container]; + range = [layoutManager characterRangeForGlyphRange:glyphRange 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++; + + count = [lines count]; + index = 0; + + for (line = [self lineNumberForCharacterIndex:range.location inText:text]; line < count; line++) + { + index = [[lines objectAtIndex:line] unsignedIntValue]; + + if (NSLocationInRange(index, range)) + { + rects = [layoutManager rectArrayForCharacterRange:NSMakeRange(index, 0) + withinSelectedCharacterRange:nullRange + inTextContainer:container + rectCount:&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. + ypos = yinset + NSMinY(rects[0]) - NSMinY(visibleRect); + + // Line numbers are internally stored starting at 0 + labelText = [NSString stringWithFormat:@"%d", line + 1]; + + stringSize = [labelText sizeWithAttributes:textAttributes]; + + currentTextAttributes = textAttributes; + + // Draw string flush right, centered vertically within the line + [labelText drawInRect: + NSMakeRect(NSWidth(bounds) - stringSize.width - RULER_MARGIN, + ypos + (NSHeight(rects[0]) - stringSize.height) / 2.0, + NSWidth(bounds) - RULER_MARGIN * 2.0, NSHeight(rects[0])) + withAttributes:currentTextAttributes]; + } + } + if (index > NSMaxRange(range)) + { + break; + } + } + } +} + + +#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" + +- (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]; + } +} + +@end |