From 2e31a4ad75b8e47a4feacadd567fbfab65eeede9 Mon Sep 17 00:00:00 2001 From: stuconnolly Date: Mon, 7 Feb 2011 20:20:41 +0000 Subject: Bring outline view branch up to date with trunk (r3179:r3187). --- Source/NoodleLineNumberView.h | 49 ++-- Source/NoodleLineNumberView.m | 483 ++++++++++++++++++++------------------ Source/SPDatabaseDocument.m | 1 - Source/SPDatabaseViewController.m | 6 + Source/SPFieldMapperController.h | 2 + Source/SPFieldMapperController.m | 111 +++++++-- Source/SPTextView.m | 55 ++--- 7 files changed, 399 insertions(+), 308 deletions(-) (limited to 'Source') diff --git a/Source/NoodleLineNumberView.h b/Source/NoodleLineNumberView.h index 45f33c00..1d600720 100644 --- a/Source/NoodleLineNumberView.h +++ b/Source/NoodleLineNumberView.h @@ -29,37 +29,42 @@ #import -@class NoodleLineNumberMarker; - @interface NoodleLineNumberView : NSRulerView { - // Array of character indices for the beginning of each line - NSMutableArray *lineIndices; - NSFont *font; - NSColor *textColor; - NSColor *alternateTextColor; - NSColor *backgroundColor; - // Add support for selection by clicking/dragging - NSUInteger dragSelectionStartLine; -} + // Array of character indices for the beginning of each line + NSMutableArray *lineIndices; -- (id)initWithScrollView:(NSScrollView *)aScrollView; + NSFont *font; + NSColor *textColor; + NSColor *alternateTextColor; + NSColor *backgroundColor; + CGFloat maxWidthOfGlyph; + CGFloat maxWidthOfGlyph1; + CGFloat maxWidthOfGlyph2; + CGFloat maxWidthOfGlyph3; + CGFloat maxWidthOfGlyph4; + CGFloat maxWidthOfGlyph5; + CGFloat maxWidthOfGlyph6; + CGFloat maxWidthOfGlyph7; + CGFloat maxWidthOfGlyph8; + NSDictionary *textAttributes; -- (void)setFont:(NSFont *)aFont; -- (NSFont *)font; + // Add support for selection by clicking/dragging + NSUInteger dragSelectionStartLine; -- (void)setTextColor:(NSColor *)color; -- (NSColor *)textColor; +} -- (void)setAlternateTextColor:(NSColor *)color; -- (NSColor *)alternateTextColor; +@property(retain) NSColor *alternateTextColor; +@property(retain) NSColor *backgroundColor; -- (void)setBackgroundColor:(NSColor *)color; -- (NSColor *)backgroundColor; +- (NSFont*)font; +- (void)setFont:(NSFont*)aFont; +- (NSColor*)textColor; +- (void)setTextColor:(NSColor*)color; +- (id)initWithScrollView:(NSScrollView *)aScrollView; - (NSUInteger)lineNumberForLocation:(CGFloat)location; - -- (NSUInteger)lineNumberForCharacterIndex:(NSUInteger)index inText:(NSString *)text; +- (NSUInteger)lineNumberForCharacterIndex:(NSUInteger)index; @end diff --git a/Source/NoodleLineNumberView.m b/Source/NoodleLineNumberView.m index 3df2b423..ebca542d 100644 --- a/Source/NoodleLineNumberView.m +++ b/Source/NoodleLineNumberView.m @@ -28,32 +28,58 @@ // // This version of the NoodleLineNumberView for Sequel Pro removes marker -// functionality and adds selection by clicking on the ruler. +// functionality and adds selection by clicking on the ruler. Furthermore +// the code was optimized. #import "NoodleLineNumberView.h" #include -#define DEFAULT_THICKNESS 22.0 -#define RULER_MARGIN 5.0 +#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.0 +#define RULER_MARGIN 5.0 +#define RULER_MARGIN2 RULER_MARGIN * 2 + +typedef NSRange (*RangeOfLineIMP)(id object, SEL selector, NSRange range); + +#pragma mark - @interface NoodleLineNumberView (Private) - (NSMutableArray *)lineIndices; - (void)invalidateLineIndices; - (void)calculateLines; -- (NSDictionary *)textAttributes; +- (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]; + maxWidthOfGlyph = [[NSString stringWithString:@"8"] sizeWithAttributes:textAttributes].width; + [self updateGutterThicknessConstants]; } return self; @@ -69,27 +95,36 @@ [[NSNotificationCenter defaultCenter] removeObserver:self]; if (lineIndices) [lineIndices release]; - [font 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]; + maxWidthOfGlyph = [[NSString stringWithString:@"8"] sizeWithAttributes:textAttributes].width; + [self updateGutterThicknessConstants]; } } - (NSFont *)font { if (font == nil) - { return [NSFont labelFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]]; - } - return font; + + return font; } - (void)setTextColor:(NSColor *)color @@ -98,101 +133,68 @@ { [textColor autorelease]; textColor = [color retain]; + if (textAttributes) [textAttributes release]; + textAttributes = [[NSDictionary dictionaryWithObjectsAndKeys: + [self font], NSFontAttributeName, + textColor, NSForegroundColorAttributeName, + nil] retain]; + maxWidthOfGlyph = [[NSString stringWithString:@"8"] sizeWithAttributes:textAttributes].width; + [self updateGutterThicknessConstants]; } } - (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; + return textColor; } - (void)setClientView:(NSView *)aView { - id oldClientView; - - oldClientView = [self clientView]; + 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]]) { [[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 -{ - if (lineIndices) [lineIndices release], lineIndices = nil; -} +#pragma mark - - (void)textDidChange:(NSNotification *)notification { - // Invalidate the line indices. They will be recalculated and recached on demand. - [self invalidateLineIndices]; + + if(![self clientView]) return; + + NSUInteger editMask = [[(NSTextView *)[self clientView] textStorage] editedMask]; + + // 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(editMask != 1) + [self invalidateLineIndices]; [self setNeedsDisplay:YES]; + } - (NSUInteger)lineNumberForLocation:(CGFloat)location { - NSUInteger line, count, index, rectCount, i; - NSRectArray rects; - NSRect visibleRect; - NSLayoutManager *layoutManager; - NSTextContainer *container; - NSRange nullRange; - NSArray *lines; - id view; + NSUInteger line, count, rectCount, i; + NSRectArray rects; + NSRect visibleRect; + NSLayoutManager *layoutManager; + NSTextContainer *container; + NSRange nullRange; + NSArray *lines; + id view; view = [self clientView]; visibleRect = [[[self scrollView] contentView] bounds]; @@ -216,148 +218,83 @@ // It doesn't show up in the glyphs so would not be accounted for. range.length++; - for (line = [self lineNumberForCharacterIndex:range.location inText:@""]; line < count; line++) - { + // Cache loop methods for speed + SEL lineNumberForCharacterIndexSel = @selector(lineNumberForCharacterIndex:); + IMP lineNumberForCharacterIndexIMP = [self methodForSelector:lineNumberForCharacterIndexSel]; - index = [NSArrayObjectAtIndex(lines, line) unsignedIntegerValue]; + for (line = (NSUInteger)(*lineNumberForCharacterIndexIMP)(self, lineNumberForCharacterIndexSel, range.location); line < count; line++) + { - rects = [layoutManager rectArrayForCharacterRange:NSMakeRange(index, 0) + rects = [layoutManager rectArrayForCharacterRange:NSMakeRange([NSArrayObjectAtIndex(lines, line) unsignedIntegerValue], 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; + if(!rectCount) return NSNotFound; + + if ((location >= NSMinY(rects[0])) && (location < NSMaxY(rects[0]))) + return line + 1; } } return NSNotFound; } -- (void)calculateLines +- (NSUInteger)lineNumberForCharacterIndex:(NSUInteger)index { - id view = [self clientView]; - - if ([view isKindOfClass:[NSTextView class]]) - { - NSUInteger index, stringLength, lineEnd, contentEnd; - NSString *text; - CGFloat oldThickness, newThickness; - - text = [view string]; - stringLength = [text length]; - - // Switch off line numbering if text larger than 6MB - // for performance reasons. - // TODO improve performance maybe via threading - if(stringLength>6000000) - return; + NSUInteger left, right, mid, lineStart; + NSMutableArray *lines; - if (lineIndices) [lineIndices release], lineIndices = nil; - lineIndices = [[NSMutableArray alloc] initWithCapacity:1]; - - index = 0; + lines = [self lineIndices]; - do - { - [lineIndices addObject:[NSNumber numberWithUnsignedInteger:index]]; - index = NSMaxRange([text lineRangeForRange:NSMakeRange(index, 0)]); - } - while (index < stringLength); + // Binary search + left = 0; + right = [lines count]; - // Check if text ends with a new line. - [text getLineStart:NULL end:&lineEnd contentsEnd:&contentEnd forRange:NSMakeRange([[lineIndices lastObject] unsignedIntegerValue], 0)]; - if (contentEnd < lineEnd) - { - [lineIndices addObject:[NSNumber numberWithUnsignedInteger:index]]; - } + while ((right - left) > 1) + { - oldThickness = [self ruleThickness]; - newThickness = [self requiredThickness]; - if (fabs(oldThickness - newThickness) > 1) - { - NSInvocation *invocation; + mid = (right + left) >> 1; + lineStart = [NSArrayObjectAtIndex(lines, mid) unsignedIntegerValue]; - // 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]; + if (index < lineStart) + right = mid; + else if (index > lineStart) + left = mid; + else + return mid; - [invocation performSelector:@selector(invoke) withObject:nil afterDelay:0.0]; - } } -} - -- (NSUInteger)lineNumberForCharacterIndex:(NSUInteger)index inText:(NSString *)text -{ - NSUInteger 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 = [NSArrayObjectAtIndex(lines, mid) unsignedIntegerValue]; - - 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]; + return left; } - (CGFloat)requiredThickness { NSUInteger lineCount = [[self lineIndices] count]; if(lineCount < 10) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"8"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph1; else if(lineCount < 100) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"88"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph2; else if(lineCount < 1000) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"888"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph3; else if(lineCount < 10000) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"8888"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph4; else if(lineCount < 100000) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"88888"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph5; else if(lineCount < 1000000) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"888888"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph6; else if(lineCount < 10000000) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"8888888"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph7; else if(lineCount < 100000000) - return ceil(MAX(DEFAULT_THICKNESS, [[NSString stringWithString:@"88888888"] sizeWithAttributes:[self textAttributes]].width + RULER_MARGIN * 2)); + return maxWidthOfGlyph8; else return 100; } - (void)drawHashMarksAndLabelsInRect:(NSRect)aRect { - id view; - NSRect bounds; + id view; + NSRect bounds; bounds = [self bounds]; @@ -374,45 +311,45 @@ if ([view isKindOfClass:[NSTextView class]]) { - NSLayoutManager *layoutManager; - NSTextContainer *container; - NSRect visibleRect; - NSRange range, glyphRange, nullRange; - NSString *text, *labelText; - NSUInteger rectCount, index, line, count; - NSRectArray rects; - CGFloat ypos, yinset; - NSDictionary *textAttributes; - NSSize stringSize; - NSArray *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]; + NSLayoutManager *layoutManager; + NSTextContainer *container; + NSRect visibleRect; + NSRange range, nullRange; + NSString *labelText; + NSUInteger rectCount, index, line, count; + NSRectArray rects; + CGFloat ypos, yinset; + NSSize stringSize; + NSArray *lines; + + layoutManager = [view layoutManager]; + container = [view textContainer]; + nullRange = NSMakeRange(NSNotFound, 0); + + yinset = [view textContainerInset].height; + visibleRect = [[[self scrollView] contentView] bounds]; + + lines = [self lineIndices]; // Find the characters that are currently visible - glyphRange = [layoutManager glyphRangeForBoundingRect:visibleRect inTextContainer:container]; - range = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; + 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++; count = [lines count]; - - CGFloat boundsRULERMargin2 = NSWidth(bounds) - RULER_MARGIN * 2.0; + + CGFloat boundsRULERMargin2 = NSWidth(bounds) - RULER_MARGIN2; CGFloat boundsWidthRULER = NSWidth(bounds) - RULER_MARGIN; CGFloat yinsetMinY = yinset - NSMinY(visibleRect); + CGFloat rectHeight; - for (line = [self lineNumberForCharacterIndex:range.location inText:text]; line < count; line++) + // Cache loop methods for speed + SEL lineNumberForCharacterIndexSel = @selector(lineNumberForCharacterIndex:); + IMP lineNumberForCharacterIndexIMP = [self methodForSelector:lineNumberForCharacterIndexSel]; + + for (line = (NSUInteger)(*lineNumberForCharacterIndexIMP)(self, lineNumberForCharacterIndexSel, range.location); line < count; line++) { index = [NSArrayObjectAtIndex(lines, line) unsignedIntegerValue]; @@ -433,35 +370,35 @@ stringSize = [labelText sizeWithAttributes:textAttributes]; + rectHeight = NSHeight(rects[0]); // Draw string flush right, centered vertically within the line [labelText drawInRect: NSMakeRect(boundsWidthRULER - stringSize.width, - yinsetMinY + NSMinY(rects[0]) + ((NSHeight(rects[0]) - stringSize.height) / 2.0), - boundsRULERMargin2, NSHeight(rects[0])) + yinsetMinY + NSMinY(rects[0]) + ((NSInteger)(rectHeight - stringSize.height) >> 1), + boundsRULERMargin2, rectHeight) withAttributes:textAttributes]; } } + if (index > NSMaxRange(range)) - { break; - } + } } } - (void)mouseDown:(NSEvent *)theEvent { - NSPoint location; - NSUInteger line; - NSTextView *view; - - if (![[self clientView] isKindOfClass:[NSTextView class]]) return; + + NSUInteger line; + NSTextView *view; + + if (![[self clientView] isKindOfClass:[NSTextView class]]) return; view = (NSTextView *)[self clientView]; - - location = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - line = [self lineNumberForLocation:location.y]; + + line = [self lineNumberForLocation:[self convertPoint:[theEvent locationInWindow] fromView:nil].y]; dragSelectionStartLine = line; - + if (line != NSNotFound) { NSUInteger selectionStart, selectionEnd; @@ -479,15 +416,14 @@ - (void)mouseDragged:(NSEvent *)theEvent { - NSPoint location; - NSUInteger line, startLine, endLine; - NSTextView *view; - - if (![[self clientView] isKindOfClass:[NSTextView class]] || dragSelectionStartLine == NSNotFound) return; + + NSUInteger line, startLine, endLine; + NSTextView *view; + + if (![[self clientView] isKindOfClass:[NSTextView class]] || dragSelectionStartLine == NSNotFound) return; view = (NSTextView *)[self clientView]; - - location = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - line = [self lineNumberForLocation:location.y]; + + line = [self lineNumberForLocation:[self convertPoint:[theEvent locationInWindow] fromView:nil].y]; if (line != NSNotFound) { @@ -509,16 +445,11 @@ } [view setSelectedRange:NSMakeRange(selectionStart, selectionEnd - selectionStart)]; } - + [view autoscroll:theEvent]; } -#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 - - (id)initWithCoder:(NSCoder *)decoder { @@ -562,4 +493,94 @@ } } +#pragma mark - +#pragma mark PrivateAPI + +- (NSMutableArray *)lineIndices +{ + + if (lineIndices == nil) + [self calculateLines]; + + return lineIndices; + +} + +- (void)invalidateLineIndices +{ + + if (lineIndices) [lineIndices release], lineIndices = nil; + +} + +- (void)calculateLines +{ + id view = [self clientView]; + + if ([view isKindOfClass:[NSTextView class]]) + { + NSUInteger index, stringLength, lineEnd, contentEnd; + NSString *textString; + CGFloat newThickness; + + textString = [view string]; + stringLength = [textString length]; + + // Switch off line numbering if text larger than 6MB + // for performance reasons. + // TODO improve performance maybe via threading + if(stringLength>6000000) + return; + + if (lineIndices) [lineIndices release], lineIndices = nil; + // Init lineIndices with text length / 16 + 1 + lineIndices = [[NSMutableArray alloc] initWithCapacity:((NSUInteger)stringLength>>4)+1]; + + index = 0; + + // Cache loop methods for speed + SEL lineRangeForRangeSel = @selector(lineRangeForRange:); + SEL addObjectSel = @selector(addObject:); + RangeOfLineIMP rangeOfLineIMP = (RangeOfLineIMP)[textString methodForSelector:lineRangeForRangeSel]; + IMP addObjectIMP = [lineIndices methodForSelector:addObjectSel]; + + do + { + (void*)(*addObjectIMP)(lineIndices, addObjectSel, [NSNumber numberWithUnsignedInteger:index]); + index = NSMaxRange((*rangeOfLineIMP)(textString, lineRangeForRangeSel, NSMakeRange(index, 0))); + } + while (index < stringLength); + + // Check if text ends with a new line. + [textString getLineStart:NULL end:&lineEnd contentsEnd:&contentEnd forRange:NSMakeRange([[lineIndices lastObject] unsignedIntegerValue], 0)]; + if (contentEnd < lineEnd) + (void*)(*addObjectIMP)(lineIndices, addObjectSel, [NSNumber numberWithUnsignedInteger:index]); + + newThickness = [self requiredThickness]; + if (fabs([self ruleThickness] - newThickness) > 1) + { + // 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 diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index 1d4b4fc6..4bc82a21 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -3795,7 +3795,6 @@ // Add the progress window to this window [self centerTaskWindow]; - [taskProgressWindow orderFront:self]; [parentWindow addChildWindow:taskProgressWindow ordered:NSWindowAbove]; } diff --git a/Source/SPDatabaseViewController.m b/Source/SPDatabaseViewController.m index a7867c53..03153230 100644 --- a/Source/SPDatabaseViewController.m +++ b/Source/SPDatabaseViewController.m @@ -426,6 +426,12 @@ if (changeEncoding) [mySQLConnection restoreStoredEncoding]; + // Cache table information on the working thread + if (selectedTableType == SPTableTypeView) + [tableDataInstance updateInformationForCurrentView]; + else + [tableDataInstance updateInformationForCurrentTable]; + // Notify listeners of the table change now that the state is fully set up. [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:SPTableChangedNotification object:self]; diff --git a/Source/SPFieldMapperController.h b/Source/SPFieldMapperController.h index 8a70df90..813a3b3e 100644 --- a/Source/SPFieldMapperController.h +++ b/Source/SPFieldMapperController.h @@ -76,6 +76,7 @@ IBOutlet id highPriorityCheckBox; IBOutlet id skipexistingRowsCheckBox; IBOutlet SPTextView *onupdateTextView; + IBOutlet id gobackButton; IBOutlet id advancedButton; @@ -171,6 +172,7 @@ - (IBAction)openAdvancedSheet:(id)sender; - (IBAction)closeSheet:(id)sender; - (IBAction)goBackToFileChooser:(id)sender; +- (IBAction)goBackToFileChooserFromPathControl:(id)sender; - (IBAction)addGlobalValue:(id)sender; - (IBAction)removeGlobalValue:(id)sender; diff --git a/Source/SPFieldMapperController.m b/Source/SPFieldMapperController.m index e70a7c25..b77ca98e 100644 --- a/Source/SPFieldMapperController.m +++ b/Source/SPFieldMapperController.m @@ -29,6 +29,7 @@ #import "SPTextView.h" #import "SPTableView.h" #import "SPCategoryAdditions.h" +#import "RegexKitLite.h" #define SP_NUMBER_OF_RECORDS_STRING NSLocalizedString(@"%ld of %@%lu records", @"Label showing the index of the selected CSV row") @@ -120,7 +121,7 @@ static const NSString *SPTableViewSqlColumnID = @"sql"; [pc setURL:[NSURL fileURLWithPath:sourcePath]]; if([pc pathComponentCells]) [fileSourcePath setPathComponentCells:[pc pathComponentCells]]; - [fileSourcePath setDoubleAction:@selector(goBackToFileChooser:)]; + [fileSourcePath setDoubleAction:@selector(goBackToFileChooserFromPathControl:)]; [onupdateTextView setDelegate:theDelegate]; windowMinWidth = [[self window] minSize].width; @@ -700,15 +701,24 @@ static const NSString *SPTableViewSqlColumnID = @"sql"; if(possibleImports < 1) return; + // Set all operators to doNotImport + [fieldMappingOperatorArray removeAllObjects]; + for(i=0; i < [fieldMappingTableColumnNames count]; i++) + [fieldMappingOperatorArray addObject:doNotImport]; + switch([[alignByPopup selectedItem] tag]) { case 0: // file order - for(i=0; i=0; i--) + for(i=possibleImports; i>=0; i--) { [fieldMappingArray replaceObjectAtIndex:possibleImports-i withObject:[NSNumber numberWithInteger:i]]; + [fieldMappingOperatorArray replaceObjectAtIndex:possibleImports-i withObject:doImport]; + } break; case 2: // try to align header and table target field names via Levenshtein distance [self matchHeaderNames]; @@ -763,14 +773,21 @@ static const NSString *SPTableViewSqlColumnID = @"sql"; } } +- (IBAction)goBackToFileChooserFromPathControl:(id)sender +{ + [gobackButton performSelector:@selector(performClick:) withObject:nil afterDelay:0.0f]; +} + - (IBAction)goBackToFileChooser:(id)sender { + [NSApp endSheet:[self window] returnCode:[sender tag]]; - if([sourcePath hasPrefix:SPImportClipboardTempFileNamePrefix]) { + + if([sourcePath hasPrefix:SPImportClipboardTempFileNamePrefix]) [theDelegate importFromClipboard]; - } else { + else [theDelegate importFile]; - } + } - (IBAction)newTable:(id)sender @@ -1365,34 +1382,84 @@ static const NSString *SPTableViewSqlColumnID = @"sql"; NSMutableArray *tableHeaderNames = [NSMutableArray array]; [tableHeaderNames setArray:fieldMappingTableColumnNames]; + // Create a distance matrix for each file-table name + // distance will be calculated by using Levenshtein distance minus common prefix and suffix length + // and minus the length of a fuzzy regex search for a common sequence of characters NSInteger i,j; - NSMutableArray *matchedHeaderNames = [NSMutableArray array]; + NSMutableArray *distMatrix = [NSMutableArray array]; for(i=0; i < [tableHeaderNames count]; i++) { - CGFloat minDist = 1e6; + CGFloat minDist = 1e6; NSInteger minIndex = 0; + CGFloat dist = 1e6; for(j=0; j < [fileHeaderNames count]; j++) { id fileHeaderName = NSArrayObjectAtIndex(fileHeaderNames,j); if([fileHeaderName isKindOfClass:[NSNull class]] || [fileHeaderName isSPNotLoaded]) continue; NSString *headerName = [(NSString*)fileHeaderName lowercaseString]; - CGFloat dist = [[NSArrayObjectAtIndex(tableHeaderNames,i) lowercaseString] levenshteinDistanceWithWord:headerName]; - if(dist < minDist && ![matchedHeaderNames containsObject:headerName]) { - minDist = dist; - minIndex = j; + NSString *tableHeadName = [NSArrayObjectAtIndex(tableHeaderNames,i) lowercaseString]; + dist = [tableHeadName levenshteinDistanceWithWord:headerName]; + + // if dist > 0 subtract the length of common prefixes, suffixes, and in common sequence characters + if(dist > 0.0) { + dist -= [[tableHeadName commonPrefixWithString:headerName options:NSCaseInsensitiveSearch] length]; + dist -= [[tableHeadName commonPrefixWithString:headerName options:NSCaseInsensitiveSearch|NSBackwardsSearch] length]; + + NSMutableString *fuzzyRegexp = [[NSMutableString alloc] initWithCapacity:3]; + NSInteger i; + unichar c; + + for(i=0; i<[headerName length]; i++) { + c = [headerName characterAtIndex:i]; + if (c == '.' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}') + [fuzzyRegexp appendFormat:@".*?\\%c",c]; + else + [fuzzyRegexp appendFormat:@".*?%c",c]; + } + dist -= [tableHeadName rangeOfRegex:fuzzyRegexp].length; + [fuzzyRegexp release]; + + } else { + // Levenshtein distance == 0 means that both names are equal set dist to + // a large negative number since dist can be negative due to search for in common chars + dist = -1e6; } - if(dist == 0.0f) [matchedHeaderNames addObject:headerName]; + + [distMatrix addObject:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithFloat:dist], @"dist", + NSStringFromRange(NSMakeRange(i,j)), @"match", + (NSString*)fileHeaderName, @"file", + NSArrayObjectAtIndex(tableHeaderNames,i), @"table", + nil]]; + } - [fieldMappingArray replaceObjectAtIndex:i withObject:[NSNumber numberWithInteger:minIndex]]; - [fieldMappingOperatorArray replaceObjectAtIndex:i withObject:doImport]; + } - // If a pair with distance 0 was found set doNotImport to those fields which are still mapped - // to such csv file header name - if([matchedHeaderNames count]) - for(i=0; i < [tableHeaderNames count]; i++) { - NSString *mappedFileHeaderName = [NSArrayObjectAtIndex(fileHeaderNames, [[fieldMappingArray objectAtIndex:i] integerValue]) lowercaseString]; - if([matchedHeaderNames containsObject:mappedFileHeaderName] && ![mappedFileHeaderName isEqualToString:[NSArrayObjectAtIndex(tableHeaderNames, i) lowercaseString]]) - [fieldMappingOperatorArray replaceObjectAtIndex:i withObject:doNotImport]; + // Sort the matrix according distance + NSSortDescriptor *sortByDistance = [[[NSSortDescriptor alloc] initWithKey:@"dist" ascending:TRUE] autorelease]; + [distMatrix sortUsingDescriptors:[NSArray arrayWithObjects:sortByDistance, nil]]; + + NSMutableArray *matchedFile = [NSMutableArray array]; + NSMutableArray *matchedTable = [NSMutableArray array]; + NSInteger cnt = 0; + for(NSDictionary* m in distMatrix) { + if(![matchedFile containsObject:[m objectForKey:@"file"]] && ![matchedTable containsObject:[m objectForKey:@"table"]]) { + + NSRange match = NSRangeFromString([m objectForKey:@"match"]); + + // Set best match + [fieldMappingArray replaceObjectAtIndex:match.location withObject:[NSNumber numberWithInteger:match.length]]; + [fieldMappingOperatorArray replaceObjectAtIndex:match.location withObject:doImport]; + + // Remember matched pair + [matchedTable addObject:[m objectForKey:@"table"]]; + [matchedFile addObject:[m objectForKey:@"file"]]; + cnt++; } + + // break if all file names are mapped + if(cnt >= [fileHeaderNames count]) break; + + } } /* diff --git a/Source/SPTextView.m b/Source/SPTextView.m index 82bfdedf..705f2a8a 100644 --- a/Source/SPTextView.m +++ b/Source/SPTextView.m @@ -269,15 +269,16 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) // If caret is not inside backticks add keywords and all words coming from the view. if(!dbBrowseMode) { - // Only parse for words if text size is less than 6MB - if([[self string] length] && [[self string] length]<6000000) + // Only parse for words if text size is less than 1MB + if([[self string] length] && [[self string] length]<1000000) { NSMutableSet *uniqueArray = [NSMutableSet setWithCapacity:5]; for(id w in [[self textStorage] words]) - [uniqueArray addObject:[w string]]; - // Remove current word from list + if([[w string] hasPrefix:currentWord]) + [uniqueArray addObject:[w string]]; + // Remove current word from list [uniqueArray removeObject:currentWord]; NSInteger reverseSort = NO; @@ -863,7 +864,7 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) */ - (NSUInteger) getLineNumberForCharacterIndex:(NSUInteger)anIndex { - return [lineNumberView lineNumberForCharacterIndex:anIndex inText:[self string]]+1; + return [lineNumberView lineNumberForCharacterIndex:anIndex]+1; } /** @@ -2153,12 +2154,6 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) } // Note: switch(insertedCharacter) {} does not work instead use charactersIgnoringModifiers - if([charactersIgnMod isEqualToString:@"c"]) // ^C copy as RTF - if(curFlags==(NSControlKeyMask)) - { - [self copyAsRTF]; - return; - } if([charactersIgnMod isEqualToString:@"h"]) // ^H show MySQL Help if(curFlags==(NSControlKeyMask)) { @@ -2690,10 +2685,6 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) tokenColor = quoteColor; allowToCheckForUpperCase = NO; break; - case SPT_BACKTICK_QUOTED_TEXT: - tokenColor = backtickColor; - allowToCheckForUpperCase = NO; - break; case SPT_RESERVED_WORD: tokenColor = keywordColor; break; @@ -2701,6 +2692,10 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) tokenColor = numericColor; allowToCheckForUpperCase = NO; break; + case SPT_BACKTICK_QUOTED_TEXT: + tokenColor = backtickColor; + allowToCheckForUpperCase = NO; + break; case SPT_COMMENT: tokenColor = commentColor; allowToCheckForUpperCase = NO; @@ -2726,31 +2721,28 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) if (!tokenRange.length) continue; // If the current token is marked as SQL keyword, uppercase it if required. - tokenEnd = tokenRange.location+tokenRange.length-1; + tokenEnd = NSMaxRange(tokenRange) - 1; // Check the end of the token - if (textBufferSizeIncreased + if (autouppercaseKeywordsEnabled && allowToCheckForUpperCase - && autouppercaseKeywordsEnabled + && textBufferSizeIncreased && !delBackwardsWasPressed + && (tokenEnd+1) < strlength && [(NSString*)NSMutableAttributedStringAttributeAtIndex(textStore, kSQLkeyword, tokenEnd, nil) length]) // check if next char is not a kSQLkeyword or current kSQLkeyword is at the end; // if so then upper case keyword if not already done - // @try catch() for catching valid index esp. after deleteBackward: { - NSString* curTokenString = [selfstr substringWithRange:tokenRange]; - BOOL doIt = NO; - @try - { - doIt = ![(NSString*)NSMutableAttributedStringAttributeAtIndex(textStore, kSQLkeyword,tokenEnd+1,nil) length]; - } @catch(id ae) { doIt = NO; } - - if(doIt) + NSString* curTokenString = [selfstr substringWithRange:tokenRange]; + if(![(NSString*)NSMutableAttributedStringAttributeAtIndex(textStore, kSQLkeyword,tokenEnd+1,nil) length]) { - // Register it for undo works only partly for now, at least the uppercased keyword will be selected - [self shouldChangeTextInRange:tokenRange replacementString:curTokenString]; - [self replaceCharactersInRange:tokenRange withString:[curTokenString uppercaseString]]; + NSString *curTokenStringUP = [curTokenString uppercaseString]; + if(![curTokenString isEqualToString:curTokenStringUP]) { + // Register it for undo works only partly for now, at least the uppercased keyword will be selected + [self shouldChangeTextInRange:tokenRange replacementString:curTokenStringUP]; + [self replaceCharactersInRange:tokenRange withString:curTokenStringUP]; + } } } @@ -2986,9 +2978,8 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) } if ([[[self class] defaultMenu] itemWithTag:SP_CQ_COPY_AS_RTF_MENU_ITEM_TAG] == nil) { - NSMenuItem *copyAsRTFMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Copy as RTF", @"Copy as RTF") action:@selector(copyAsRTF) keyEquivalent:@"c"]; + NSMenuItem *copyAsRTFMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Copy as RTF", @"Copy as RTF") action:@selector(copyAsRTF) keyEquivalent:@""]; [copyAsRTFMenuItem setTag:SP_CQ_COPY_AS_RTF_MENU_ITEM_TAG]; - [copyAsRTFMenuItem setKeyEquivalentModifierMask:NSControlKeyMask]; [menu insertItem:copyAsRTFMenuItem atIndex:2]; [copyAsRTFMenuItem release]; } -- cgit v1.2.3