diff options
author | Bibiko <bibiko@eva.mpg.de> | 2009-05-24 10:26:56 +0000 |
---|---|---|
committer | Bibiko <bibiko@eva.mpg.de> | 2009-05-24 10:26:56 +0000 |
commit | 5e2eb0be79f6f572724132f8be88069f8daac926 (patch) | |
tree | ff26ebbd4e397b5ecbe339ede00060e3669fcaa6 | |
parent | bcb75ae5eef97aadaf14d45f2355f5310a12958f (diff) | |
download | sequelpro-5e2eb0be79f6f572724132f8be88069f8daac926.tar.gz sequelpro-5e2eb0be79f6f572724132f8be88069f8daac926.tar.bz2 sequelpro-5e2eb0be79f6f572724132f8be88069f8daac926.zip |
• added class to support narrow-down completion in the Query Editor
- an image can be added
- display and insert string can differ
- fully customizable
This class is based on TextMate's TMDIncrementalPopUp implementation
(Dialog plugin) written by Joachim Mårtensson, Allan Odgaard, and H.-J. Bibiko. see license: http://svn.textmate.org/trunk/LICENSE
-rw-r--r-- | Source/SPNarrowDownCompletion.h | 65 | ||||
-rw-r--r-- | Source/SPNarrowDownCompletion.m | 460 | ||||
-rw-r--r-- | sequel-pro.xcodeproj/project.pbxproj | 6 |
3 files changed, 531 insertions, 0 deletions
diff --git a/Source/SPNarrowDownCompletion.h b/Source/SPNarrowDownCompletion.h new file mode 100644 index 00000000..baabb240 --- /dev/null +++ b/Source/SPNarrowDownCompletion.h @@ -0,0 +1,65 @@ +// +// $Id: SPNarrowDownCompletion.h 744 2009-05-22 20:00:00Z bibiko $ +// +// SPGrowlController.h +// sequel-pro +// +// Created by Hans-J. Bibiko on May 14, 2009. +// +// This class is based on TextMate's TMDIncrementalPopUp implementation +// (Dialog plugin) written by Joachim Mårtensson, Allan Odgaard, and H.-J. Bibiko. +// see license: http://svn.textmate.org/trunk/LICENSE +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import <Cocoa/Cocoa.h> + +#define SP_NARROWDOWNLIST_MAX_ROWS 15 + +#ifndef enumerate +#define enumerate(container,var) for(NSEnumerator* _enumerator = [container objectEnumerator]; var = [_enumerator nextObject]; ) +#endif + +#ifndef sizeofA +#define sizeofA(a) (sizeof(a)/sizeof(a[0])) +#endif + + +@interface SPNarrowDownCompletion : NSWindow { + + NSArray* suggestions; + NSMutableString* mutablePrefix; + NSString* staticPrefix; + NSArray* filtered; + NSTableView* theTableView; + NSPoint caretPos; + BOOL isAbove; + BOOL closeMe; + BOOL caseSensitive; + NSFont *tableFont; + NSRange theCharRange; + id theView; + + NSMutableCharacterSet* textualInputCharacters; + +} + +- (id)initWithItems:(NSArray*)someSuggestions alreadyTyped:(NSString*)aUserString staticPrefix:(NSString*)aStaticPrefix additionalWordCharacters:(NSString*)someAdditionalWordCharacters caseSensitive:(BOOL)isCaseSensitive charRange:(NSRange)initRange inView:(id)aView; +- (void)setCaretPos:(NSPoint)aPos; +- (void)insert_text:(NSString* )aString; + +@end diff --git a/Source/SPNarrowDownCompletion.m b/Source/SPNarrowDownCompletion.m new file mode 100644 index 00000000..19d20ead --- /dev/null +++ b/Source/SPNarrowDownCompletion.m @@ -0,0 +1,460 @@ +// +// $Id: SPNarrowDownCompletion.m 744 2009-05-22 20:00:00Z bibiko $ +// +// SPGrowlController.m +// sequel-pro +// +// Created by Hans-J. Bibiko on May 14, 2009. +// +// This class is based on TextMate's TMDIncrementalPopUp implementation +// (Dialog plugin) written by Joachim Mårtensson, Allan Odgaard, and H.-J. Bibiko. +// see license: http://svn.textmate.org/trunk/LICENSE +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPNarrowDownCompletion.h" +#import "ImageAndTextCell.h" +#import <Foundation/NSObjCRuntime.h> + + +@interface NSTableView (MovingSelectedRow) +- (BOOL)SP_NarrowDownCompletion_canHandleEvent:(NSEvent*)anEvent; +@end + +@implementation NSTableView (MovingSelectedRow) +- (BOOL)SP_NarrowDownCompletion_canHandleEvent:(NSEvent*)anEvent +{ + int visibleRows = (int)floorf(NSHeight([self visibleRect]) / ([self rowHeight]+[self intercellSpacing].height)) - 1; + + struct { unichar key; int rows; } const key_movements[] = + { + { NSUpArrowFunctionKey, -1 }, + { NSDownArrowFunctionKey, +1 }, + { NSPageUpFunctionKey, -visibleRows }, + { NSPageDownFunctionKey, +visibleRows }, + { NSHomeFunctionKey, -(INT_MAX >> 1) }, + { NSEndFunctionKey, +(INT_MAX >> 1) }, + }; + + unichar keyCode = 0; + if([anEvent type] == NSScrollWheel) + keyCode = [anEvent deltaY] >= 0.0 ? NSUpArrowFunctionKey : NSDownArrowFunctionKey; + else if([anEvent type] == NSKeyDown && [[anEvent characters] length] == 1) + keyCode = [[anEvent characters] characterAtIndex:0]; + + + for(size_t i = 0; i < sizeofA(key_movements); ++i) + { + if(keyCode == key_movements[i].key) + { + int row = MAX(0, MIN([self selectedRow] + key_movements[i].rows, [self numberOfRows]-1)); + [self selectRow:row byExtendingSelection:NO]; + [self scrollRowToVisible:row]; + + return YES; + } + } + + return NO; + +} + +@end + +@interface SPNarrowDownCompletion (Private) +- (NSRect)rectOfMainScreen; +- (NSString*)filterString; +- (void)setupInterface; +- (void)filter; +- (void)insertCommonPrefix; +- (void)completeAndInsertSnippet; +@end + +@implementation SPNarrowDownCompletion +// ============================= +// = Setup/tear-down functions = +// ============================= +- (id)init +{ + if(self = [super initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]) + { + mutablePrefix = [NSMutableString new]; + textualInputCharacters = [[NSMutableCharacterSet alphanumericCharacterSet] retain]; + caseSensitive = YES; + + tableFont = [NSUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] dataForKey:@"CustomQueryEditorFont"]]; + [self setupInterface]; + } + return self; +} + +- (void)dealloc +{ + [staticPrefix release]; + [mutablePrefix release]; + [textualInputCharacters release]; + + [suggestions release]; + + [filtered release]; + + [super dealloc]; +} + +- (id)initWithItems:(NSArray*)someSuggestions alreadyTyped:(NSString*)aUserString staticPrefix:(NSString*)aStaticPrefix additionalWordCharacters:(NSString*)someAdditionalWordCharacters caseSensitive:(BOOL)isCaseSensitive charRange:(NSRange)initRange inView:(id)aView +{ + if(self = [self init]) + { + suggestions = [someSuggestions retain]; + + if(aUserString) + [mutablePrefix appendString:aUserString]; + + if(aStaticPrefix) + staticPrefix = [aStaticPrefix retain]; + + if(someAdditionalWordCharacters) + [textualInputCharacters addCharactersInString:someAdditionalWordCharacters]; + + caseSensitive = isCaseSensitive; + theCharRange = initRange; + theView = aView; + } + return self; +} + +- (void)setCaretPos:(NSPoint)aPos +{ + caretPos = aPos; + isAbove = NO; + + NSRect mainScreen = [self rectOfMainScreen]; + + int offx = (caretPos.x/mainScreen.size.width) + 1; + if((caretPos.x + [self frame].size.width) > (mainScreen.size.width*offx)) + caretPos.x = caretPos.x - [self frame].size.width; + + if(caretPos.y>=0 && caretPos.y<[self frame].size.height) + { + caretPos.y = caretPos.y + ([self frame].size.height + [tableFont pointSize]*1.5); + isAbove = YES; + } + if(caretPos.y<0 && (mainScreen.size.height-[self frame].size.height)<(caretPos.y*-1)) + { + caretPos.y = caretPos.y + ([self frame].size.height + [tableFont pointSize]*1.5); + isAbove = YES; + } + [self setFrameTopLeftPoint:caretPos]; +} + +- (void)setupInterface +{ + [self setReleasedWhenClosed:YES]; + [self setLevel:NSStatusWindowLevel]; + [self setHidesOnDeactivate:YES]; + [self setHasShadow:YES]; + + NSScrollView* scrollView = [[[NSScrollView alloc] initWithFrame:NSZeroRect] autorelease]; + [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [scrollView setAutohidesScrollers:YES]; + [scrollView setHasVerticalScroller:YES]; + [[scrollView verticalScroller] setControlSize:NSSmallControlSize]; + + theTableView = [[[NSTableView alloc] initWithFrame:NSZeroRect] autorelease]; + [theTableView setFocusRingType:NSFocusRingTypeNone]; + [theTableView setAllowsEmptySelection:NO]; + [theTableView setHeaderView:nil]; + + NSTableColumn *column = [[[NSTableColumn alloc] initWithIdentifier:@"foo"] autorelease]; + //TODO maybe in the future add an image... + [column setDataCell:[ImageAndTextCell new]]; + [column setEditable:NO]; + [theTableView addTableColumn:column]; + [column setWidth:[theTableView bounds].size.width]; + + [theTableView setDataSource:self]; + [scrollView setDocumentView:theTableView]; + + [self setContentView:scrollView]; +} + +// ======================== +// = TableView DataSource = +// ======================== +- (int)numberOfRowsInTableView:(NSTableView *)aTableView +{ + return [filtered count]; +} + +- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex +{ + NSImage* image = nil; + NSString* imageName = nil; + imageName = [[filtered objectAtIndex:rowIndex] objectForKey:@"image"]; + if(imageName) + image = [NSImage imageNamed:imageName]; + [[aTableColumn dataCell] setImage:image]; + + return [[filtered objectAtIndex:rowIndex] objectForKey:@"display"]; +} + +// ==================== +// = Filter the items = +// ==================== +- (void)filter +{ + NSRect mainScreen = [self rectOfMainScreen]; + + NSArray* newFiltered; + if([mutablePrefix length] > 0) + { + NSPredicate* predicate; + if(caseSensitive) + predicate = [NSPredicate predicateWithFormat:@"match BEGINSWITH %@ OR (match == NULL AND display BEGINSWITH %@)", [self filterString], [self filterString]]; + else + predicate = [NSPredicate predicateWithFormat:@"match BEGINSWITH[c] %@ OR (match == NULL AND display BEGINSWITH[c] %@)", [self filterString], [self filterString]]; + newFiltered = [suggestions filteredArrayUsingPredicate:predicate]; + } + else + { + newFiltered = suggestions; + } + NSPoint old = NSMakePoint([self frame].origin.x, [self frame].origin.y + [self frame].size.height); + + int displayedRows = [newFiltered count] < SP_NARROWDOWNLIST_MAX_ROWS ? [newFiltered count] : SP_NARROWDOWNLIST_MAX_ROWS; + float newHeight = ([theTableView rowHeight] + [theTableView intercellSpacing].height) * displayedRows; + + float maxLen = 1; + NSString* item; + int i; + float maxWidth = [self frame].size.width; + if([newFiltered count]>0) + { + for(i=0; i<[newFiltered count]; i++) + { + item = [[newFiltered objectAtIndex:i] objectForKey:@"display"]; + if([item length]>maxLen) + maxLen = [item length]; + } + maxWidth = maxLen*18; + maxWidth = (maxWidth>340) ? 340 : maxWidth; + } + if(caretPos.y>=0 && (isAbove || caretPos.y<newHeight)) + { + isAbove = YES; + old.y = caretPos.y + (newHeight + [tableFont pointSize]*1.5); + } + if(caretPos.y<0 && (isAbove || (mainScreen.size.height-newHeight)<(caretPos.y*-1))) + { + old.y = caretPos.y + (newHeight + [tableFont pointSize]*1.5); + } + + // newHeight is currently the new height for theTableView, but we need to resize the whole window + // so here we use the difference in height to find the new height for the window + // newHeight = [[self contentView] frame].size.height + (newHeight - [theTableView frame].size.height); + [self setFrame:NSMakeRect(old.x,old.y-newHeight,maxWidth,newHeight) display:YES]; + [filtered release]; + filtered = [newFiltered retain]; + [theTableView reloadData]; +} + +// ========================= +// = Convenience functions = +// ========================= +- (NSString*)filterString +{ + return staticPrefix ? [staticPrefix stringByAppendingString:mutablePrefix] : mutablePrefix; +} + +- (NSRect)rectOfMainScreen +{ + NSRect mainScreen = [[NSScreen mainScreen] frame]; + NSScreen* candidate; + enumerate([NSScreen screens], candidate) + { + if(NSMinX([candidate frame]) == 0.0f && NSMinY([candidate frame]) == 0.0f) + mainScreen = [candidate frame]; + } + return mainScreen; +} + +// ============================= +// = Run the actual popup-menu = +// ============================= +- (void)orderFront:(id)sender +{ + [self filter]; + [super orderFront:sender]; + [self performSelector:@selector(watchUserEvents) withObject:nil afterDelay:0.05]; +} + +- (void)watchUserEvents +{ + closeMe = NO; + while(!closeMe) + { + NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask + untilDate:[NSDate distantFuture] + inMode:NSDefaultRunLoopMode + dequeue:YES]; + + if(!event) + continue; + + NSEventType t = [event type]; + if([theTableView SP_NarrowDownCompletion_canHandleEvent:event]) + { + // skip the rest + } + else if(t == NSKeyDown) + { + unsigned int flags = [event modifierFlags]; + unichar key = [[event characters] length] == 1 ? [[event characters] characterAtIndex:0] : 0; + if((flags & NSControlKeyMask) || (flags & NSAlternateKeyMask) || (flags & NSCommandKeyMask)) + { + [NSApp sendEvent:event]; + break; + } + else if([event keyCode] == 53) // escape + { + break; + } + else if(key == NSCarriageReturnCharacter) + { + [self completeAndInsertSnippet]; + } + else if(key == NSBackspaceCharacter || key == NSDeleteCharacter) + { + [NSApp sendEvent:event]; + if([mutablePrefix length] == 0) + break; + + [mutablePrefix deleteCharactersInRange:NSMakeRange([mutablePrefix length]-1, 1)]; + theCharRange = NSMakeRange(theCharRange.location, theCharRange.length-1); + [self filter]; + } + else if(key == NSTabCharacter) + { + if([filtered count] == 0) + { + [NSApp sendEvent:event]; + break; + } + else if([filtered count] == 1) + { + [self completeAndInsertSnippet]; + } + else + { + [self insertCommonPrefix]; + } + } + else if([textualInputCharacters characterIsMember:key]) + { + [NSApp sendEvent:event]; + [mutablePrefix appendString:[event characters]]; + theCharRange = NSMakeRange(theCharRange.location, theCharRange.length+1); + [self filter]; + } + else + { + [NSApp sendEvent:event]; + break; + } + } + else if(t == NSRightMouseDown || t == NSLeftMouseDown) + { + [NSApp sendEvent:event]; + if(!NSPointInRect([NSEvent mouseLocation], [self frame])) + break; + } + else + { + [NSApp sendEvent:event]; + } + } + [self close]; +} + +// ================== +// = Action methods = +// ================== +- (void)insertCommonPrefix +{ + int row = [theTableView selectedRow]; + if(row == -1) + return; + + id cur = [filtered objectAtIndex:row]; + NSString* curMatch = [cur objectForKey:@"match"] ?: [cur objectForKey:@"display"]; + if([[self filterString] length] + 1 < [curMatch length]) + { + NSString* prefix = [curMatch substringToIndex:[[self filterString] length] + 1]; + NSMutableArray* candidates = [NSMutableArray array]; + for(int i = row; i < [filtered count]; ++i) + { + id candidate = [filtered objectAtIndex:i]; + NSString* candidateMatch = [candidate objectForKey:@"match"] ?: [candidate objectForKey:@"display"]; + if([candidateMatch hasPrefix:prefix]) + [candidates addObject:candidateMatch]; + } + + NSString* commonPrefix = curMatch; + NSString* candidateMatch; + enumerate(candidates, candidateMatch) + commonPrefix = [commonPrefix commonPrefixWithString:candidateMatch options:NSLiteralSearch]; + + if([[self filterString] length] < [commonPrefix length]) + { + // NSString* toInsert = [commonPrefix substringFromIndex:[[self filterString] length]]; + // [mutablePrefix appendString:toInsert]; + // [self insert_text:toInsert]; + [self insert_text:commonPrefix]; + [self filter]; + } + } + else + { + [self completeAndInsertSnippet]; + } +} + +- (void)insert_text:(NSString* )aString +{ + // Register the auto-pairing for undo + [theView shouldChangeTextInRange:theCharRange replacementString:aString]; + [theView replaceCharactersInRange:theCharRange withString:aString]; +} + +- (void)completeAndInsertSnippet +{ + if([theTableView selectedRow] == -1) + return; + + NSMutableDictionary* selectedItem = [[[filtered objectAtIndex:[theTableView selectedRow]] mutableCopy] autorelease]; + + NSString* candidateMatch = [selectedItem objectForKey:@"match"] ?: [selectedItem objectForKey:@"display"]; + if([[self filterString] length] < [candidateMatch length]) + // [self insert_text:[candidateMatch substringFromIndex:[[self filterString] length]]]; + [self insert_text:candidateMatch]; + + // NSString* toInsert = [selectedItem objectForKey:@"insert"]; + // [self insert_text:toInsert]; + + closeMe = YES; +} +@end diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj index 774c674f..b3fdbe31 100644 --- a/sequel-pro.xcodeproj/project.pbxproj +++ b/sequel-pro.xcodeproj/project.pbxproj @@ -132,6 +132,7 @@ B5EAC0FD0EC87FF900CC579C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5EAC0FC0EC87FF900CC579C /* Security.framework */; }; B5F4F7810F7BCF990059AE84 /* toolbar-switch-to-procedures.tiff in Resources */ = {isa = PBXBuildFile; fileRef = B5F4F7800F7BCF990059AE84 /* toolbar-switch-to-procedures.tiff */; }; BC2C8E220FA8C2DB008468C7 /* sequel-pro-mysql-help-template.html in Resources */ = {isa = PBXBuildFile; fileRef = BC2C8E210FA8C2DB008468C7 /* sequel-pro-mysql-help-template.html */; }; + BCB151910FC9542B00977C87 /* SPNarrowDownCompletion.m in Sources */ = {isa = PBXBuildFile; fileRef = BCB151900FC9542B00977C87 /* SPNarrowDownCompletion.m */; }; BCD0AD490FBBFC340066EA5C /* SPSQLTokenizer.l in Sources */ = {isa = PBXBuildFile; fileRef = BCD0AD480FBBFC340066EA5C /* SPSQLTokenizer.l */; }; /* End PBXBuildFile section */ @@ -359,6 +360,8 @@ B5EAC0FC0EC87FF900CC579C /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; B5F4F7800F7BCF990059AE84 /* toolbar-switch-to-procedures.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "toolbar-switch-to-procedures.tiff"; sourceTree = "<group>"; }; BC2C8E210FA8C2DB008468C7 /* sequel-pro-mysql-help-template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "sequel-pro-mysql-help-template.html"; sourceTree = "<group>"; }; + BCB1518F0FC9542B00977C87 /* SPNarrowDownCompletion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPNarrowDownCompletion.h; sourceTree = "<group>"; }; + BCB151900FC9542B00977C87 /* SPNarrowDownCompletion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPNarrowDownCompletion.m; sourceTree = "<group>"; }; BCD0AD480FBBFC340066EA5C /* SPSQLTokenizer.l */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.lex; path = SPSQLTokenizer.l; sourceTree = "<group>"; }; BCD0AD4A0FBBFC480066EA5C /* SPSQLTokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPSQLTokenizer.h; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -614,6 +617,8 @@ 17E641710EF01F5C001BC333 /* GUI */ = { isa = PBXGroup; children = ( + BCB1518F0FC9542B00977C87 /* SPNarrowDownCompletion.h */, + BCB151900FC9542B00977C87 /* SPNarrowDownCompletion.m */, 17E6417C0EF01FA8001BC333 /* CMCopyTable.h */, 17E6417D0EF01FA8001BC333 /* CMCopyTable.m */, 17E6417E0EF01FA8001BC333 /* CMImageView.h */, @@ -1039,6 +1044,7 @@ 5841423F0F97E11000A34B47 /* NoodleLineNumberView.m in Sources */, BCD0AD490FBBFC340066EA5C /* SPSQLTokenizer.l in Sources */, 387BBBA80FBCB6CB00B31746 /* SPTableRelations.m in Sources */, + BCB151910FC9542B00977C87 /* SPNarrowDownCompletion.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; |