aboutsummaryrefslogtreecommitdiffstats
path: root/Source/SPNarrowDownCompletion.m
diff options
context:
space:
mode:
authorBibiko <bibiko@eva.mpg.de>2009-05-24 10:26:56 +0000
committerBibiko <bibiko@eva.mpg.de>2009-05-24 10:26:56 +0000
commit5e2eb0be79f6f572724132f8be88069f8daac926 (patch)
treeff26ebbd4e397b5ecbe339ede00060e3669fcaa6 /Source/SPNarrowDownCompletion.m
parentbcb75ae5eef97aadaf14d45f2355f5310a12958f (diff)
downloadsequelpro-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
Diffstat (limited to 'Source/SPNarrowDownCompletion.m')
-rw-r--r--Source/SPNarrowDownCompletion.m460
1 files changed, 460 insertions, 0 deletions
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