//
// $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
#import "SPNarrowDownCompletion.h"
#import "SPArrayAdditions.h"
#import "ImageAndTextCell.h"
#import
@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;
filtered = nil;
tableFont = [NSUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] dataForKey:@"CustomQueryEditorFont"]];
[self setupInterface];
}
return self;
}
- (void)dealloc
{
[staticPrefix release];
[mutablePrefix release];
[textualInputCharacters release];
// TODO: This may cause errors, I cannot see if it is always set up - only if !dictMode.
// [suggestions release];
if (filtered) [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 dictMode:(BOOL)mode
{
if(self = [self init])
{
if(aUserString)
[mutablePrefix appendString:aUserString];
if(aStaticPrefix)
staticPrefix = [aStaticPrefix retain];
caseSensitive = isCaseSensitive;
theCharRange = initRange;
theView = aView;
dictMode = mode;
if(dictMode) {
words = [NSArray arrayWithArray:suggestions];
} else {
suggestions = [someSuggestions retain];
words = nil;
}
if(someAdditionalWordCharacters)
[textualInputCharacters addCharactersInString:someAdditionalWordCharacters];
}
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];
//
[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;
if(!dictMode) {
imageName = [[filtered objectAtIndex:rowIndex] objectForKey:@"image"];
if(imageName)
image = [NSImage imageNamed:imageName];
[[aTableColumn dataCell] setImage:image];
return [[filtered objectAtIndex:rowIndex] objectForKey:@"display"];
}
return [filtered objectAtIndex:rowIndex];
}
// ====================
// = Filter the items =
// ====================
- (void)filter
{
NSRect mainScreen = [self rectOfMainScreen];
NSArray* newFiltered;
if([mutablePrefix length] > 0)
{
if(dictMode)
{
newFiltered = [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,[[self filterString] length]) inString:[self filterString] language:nil inSpellDocumentWithTag:0];
} else {
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
{
if(dictMode)
newFiltered = nil;
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;
BOOL spaceInSuggestion = NO;
[textualInputCharacters removeCharactersInString:@" "];
float maxWidth = [self frame].size.width;
if([newFiltered count]>0)
{
for(i=0; i<[newFiltered count]; i++)
{
if(dictMode)
item = NSArrayObjectAtIndex(newFiltered, i);
else
item = [NSArrayObjectAtIndex(newFiltered, i) objectForKey:@"display"];
// If space in suggestion add space to allowed input chars
if(!spaceInSuggestion && [item rangeOfString:@" "].length) {
[textualInputCharacters addCharactersInString:@" "];
spaceInSuggestion = YES;
}
if([item length]>maxLen)
maxLen = [item length];
}
maxWidth = maxLen*16;
maxWidth = (maxWidth>340) ? 340 : maxWidth;
}
if(caretPos.y>=0 && (isAbove || caretPos.y