cog/ThirdParty/KFTypeSelectTableView/KFTypeSelectTableView.m

1274 lines
42 KiB
Objective-C
Raw Blame History

//
// KFTypeSelectTableView.m
// KFTypeSelectTableView v1.0.4
//
// ------------------------------------------------------------------------
// Copyright (c) 2005, Ken Ferry All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// (1) Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// (2) Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// (3) Neither Ken Ferry's name nor the names of other contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
#import <KFTypeSelectTableView.h>
#import <CoreServices/CoreServices.h>
#include <mach/mach.h>
#include <mach/mach_time.h>
static uint64_t SecondsToMachAbsolute(double seconds);
NSString *KFTypeSelectTableViewPatternDidChangeNotification = @"KFTypeSelectTableViewPatternDidChange";
/* NOTE - because of behavior detailed at cocoadev.com/index.pl?PosingWithCategoriesSuperGotcha,
* it's important that private methods be _implemented_ in the main implementation of the class,
* not in a category. It's okay for the declarations to be in a category, as below.
* (Summary of link: messages to super act like messages to self in categories on a posing class
* in system 10.3)
*/
@interface KFTypeSelectTableView (Private)
// responding to events
static BOOL KFKeyEventIsBeginFindEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsExtendFindEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsFindNextEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsFindPreviousEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsDeleteEvent(NSEvent *keyEvent);
static BOOL KFKeyEventIsCancelEvent(NSEvent *keyEvent);
// finding strings
- (void)kfFindPattern:(NSString *)pattern
initialRow:(int)initialRow
topToBottom:(BOOL)topToBottom
allowExtension:(BOOL)allowPatternExtension;
- (BOOL)kfWorkUnitGetMatch:(NSString **)match
range:(NSRange *)matchRange
lastSearchedRow:(int *)lastSearchedRow
forPattern:(NSString *)pattern
matchOptions:(unsigned)patternMatchOptions
initialRow:(int)initialRow
boundaryRow:(int)boundaryRow
rowIncrement:(int)rowIncrement
searchColumns:(NSArray *)searchColumns
timeout:(uint64_t)timeout;
- (BOOL)kfShouldAcceptMatch:(NSString *)match
range:(NSRange)matchedRange
inRow:(int)row;
- (BOOL)kfCanPerformTypeSelect;
- (BOOL)kfSelectionShouldChange;
- (BOOL)kfCanGetTableData;
- (NSString *)kfStringValueForTableColumn:(NSTableColumn *)column row:(int)row;
- (NSArray *)kfSearchColumns;
- (BOOL)kfSearchTopToBottom;
- (int)kfInitialRowForNewSearch;
// taking action
- (void)kfPatternDidChange:(id)sender;
- (void)kfDidFindMatch:(NSString *)match
range:(NSRange)matchedRange
inRow:(int)row;
- (void)kfDidFailToFindMatchSearchingToRow:(int)row;
- (void)kfResetSearch;
- (void)kfConfigureDelegateIfNeeded;
// utility
- (BOOL)kfRowIsVisible:(int)row;
- (void)kfScrollRectToCenter:(NSRect)aRect vertical:(BOOL)scrollVertical horizontal:(BOOL)scrollHorizontal;
// simulated ivars infrastructure
- (NSMutableDictionary *)kfSimulatedIvars;
- (id)kfIdentifier;
- (void)kfSetUpSimulatedIvars;
- (void)kfTearDownSimulatedIvars;
// accessors
- (int)kfSavedRowForExtensionSearch;
- (void)setKfSavedRowForExtensionSearch:(int)row;
- (NSString *)kfLastSuccessfullyMatchedPattern;
- (void)setKfLastSuccessfullyMatchedPattern:(NSString *)string;
- (BOOL)kfCanExtendFind;
- (void)setKfCanExtendFind:(BOOL)flag;
- (id)kfLastConfiguredDelegate;
- (void)setKfLastConfiguredDelegate:(id)anObject;
- (NSInvocation *)kfTimeoutInvocation;
- (void)setKfTimeoutInvocation:(NSInvocation *)anInvocation;
- (void)setPattern:(NSString *)pattern;
@end
@implementation KFTypeSelectTableView
#pragma mark -
#pragma mark SETUP/TEARDOWN
#pragma mark -
// Note: don't use init. Won't receive it for preexisting objects when posing.
- (void)dealloc
{
NSInvocation *timeoutInvocation = [self kfTimeoutInvocation];
[[timeoutInvocation class] cancelPreviousPerformRequestsWithTarget:[self kfTimeoutInvocation]
selector:@selector(invoke)
object:nil];
[self kfTearDownSimulatedIvars];
[super dealloc];
}
#pragma mark -
#pragma mark BODY
#pragma mark -
#pragma mark responding to events
- (void)keyDown:(NSEvent *)keyEvent
{
// Will we drop this event to super?
BOOL eatEvent = NO;
if ([self kfCanPerformTypeSelect] && ([[self window] firstResponder] == self))
{
BOOL canExtendFind = [self kfCanExtendFind];
if (canExtendFind && KFKeyEventIsExtendFindEvent(keyEvent))
{
NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
[fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];
[self kfFindPattern:[fieldEditor string]
initialRow:[self kfSavedRowForExtensionSearch]
topToBottom:[self kfSearchTopToBottom]
allowExtension:YES];
eatEvent = YES;
}
else if (KFKeyEventIsBeginFindEvent(keyEvent))
{
NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
[fieldEditor setString:@""];
[fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];
NSString *newPattern = [fieldEditor string];
if (![newPattern isEqualToString:@""])
{
[self kfFindPattern:[fieldEditor string]
initialRow:[self kfInitialRowForNewSearch]
topToBottom:[self kfSearchTopToBottom]
allowExtension:YES];
}
eatEvent = YES;
}
else if (canExtendFind && KFKeyEventIsDeleteEvent(keyEvent))
{
// User might expect us to knock a character off the pattern - that'd be dangerous.
// If the user mistimed he could trigger a table view delete action.
// Best to squelch the behavior by not doing anything useful.
eatEvent = YES;
}
else if (KFKeyEventIsFindNextEvent(keyEvent))
{
[self findNext:self];
eatEvent = YES;
}
else if (KFKeyEventIsFindPreviousEvent(keyEvent))
{
[self findPrevious:self];
eatEvent = YES;
}
else if (KFKeyEventIsCancelEvent(keyEvent))
{
// this is superfluous in 10.2 and 10.3, but may be useful on systems prior to
// 10.2. I haven't had a chance to find out.
[self cancelOperation:self];
eatEvent = YES;
[super keyDown:keyEvent];
}
}
if (!eatEvent)
{
// FIXME - hack
// I can't find a decent way to clear a hanging dead-key (i.e. option-e) in kfResetSearch.
// Sending an event following a dead key event through interpretKeyEvents is the
// only thing I've found to do it, so we make sure that any keyEvent that we don't understand
// goes through the field editor's interpretKeyEvents. It won't cause any damage because the field
// editor has no delegate and will be cleared before it's used again anyway.
// Without this workaround, entering "option-e, f" will stick this table in a state where all
// key-events start with character "<22>", which means type-select won't work. The state is only
// exited when a different control starts processing text.
//
// This workaround kills the above problem, but is suboptimal in that a dead-key never times out (besides just
// being nasty).
NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
[fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];
// end hack
[self setKfCanExtendFind:NO];
[super keyDown:keyEvent];
}
}
- (void)cancelOperation:(id)sender
{
[self kfResetSearch];
}
// 10.2 private version of cancelOperation
// I'm not sure how far back this will work.
- (void)_cancelKey:(id)sender
{
[self cancelOperation:sender];
}
// an outline view catches control-down and control-up
// and uses them to select next and previous rows (same as plain up and down)
// We can catch the events by implementing moveDown and moveUp.
- (void)moveDown:(id)sender
{
NSEvent *currentEvent = [NSApp currentEvent];
if ([currentEvent type] == NSKeyDown && KFKeyEventIsFindNextEvent(currentEvent))
{
[self findNext:self];
}
}
- (void)moveUp:(id)sender
{
NSEvent *currentEvent = [NSApp currentEvent];
if ([currentEvent type] == NSKeyDown && KFKeyEventIsFindPreviousEvent(currentEvent))
{
[self findPrevious:self];
}
}
- (BOOL)resignFirstResponder
{
BOOL shouldResign = [super resignFirstResponder];
if (shouldResign)
{
[self setKfCanExtendFind:NO];
}
return shouldResign;
}
static unsigned int modifierFlagsICareAboutMask = NSCommandKeyMask | NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSFunctionKeyMask;
// yes if every character in the event is alphanumeric and no command, control or function modifiers
static BOOL KFKeyEventIsBeginFindEvent(NSEvent *keyEvent)
{
unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
NSString *characters = [keyEvent characters];
int numCharacters = [characters length];
if ((modifiers & (NSCommandKeyMask | NSControlKeyMask | NSFunctionKeyMask)) != 0)
{
return NO;
}
NSMutableCharacterSet *beginFindCharacterSet = [[[NSCharacterSet alphanumericCharacterSet] mutableCopy] autorelease];
[beginFindCharacterSet formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
unichar character;
int i;
for (i = 0; i < numCharacters; i++)
{
character = [characters characterAtIndex:i];
if (![beginFindCharacterSet characterIsMember:character])
{
return NO;
}
}
return YES;
}
// yes if every character in the event is alphanumeric, punctuation or a space, and no command, control or function modifiers
static BOOL KFKeyEventIsExtendFindEvent(NSEvent *keyEvent)
{
unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
NSString *characters = [keyEvent characters];
int numCharacters = [characters length];
if ((modifiers & (NSCommandKeyMask | NSControlKeyMask | NSFunctionKeyMask)) != 0)
{
return NO;
}
NSMutableCharacterSet *extendFindCharacterSet = [[[NSCharacterSet alphanumericCharacterSet] mutableCopy] autorelease];
[extendFindCharacterSet formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
[extendFindCharacterSet addCharactersInString:@" "];
unichar character;
int i;
for (i = 0; i < numCharacters; i++)
{
character = [characters characterAtIndex:i];
if (![extendFindCharacterSet characterIsMember:character])
{
return NO;
}
}
return YES;
}
static BOOL KFKeyEventIsFindNextEvent(NSEvent *keyEvent)
{
unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
NSString *characters = [keyEvent characters];
int numCharacters = [characters length];
if (numCharacters == 1 && [characters characterAtIndex:0] == NSDownArrowFunctionKey && modifiers == (NSControlKeyMask | NSFunctionKeyMask))
{
return YES;
}
return NO;
}
static BOOL KFKeyEventIsFindPreviousEvent(NSEvent *keyEvent)
{
unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
NSString *characters = [keyEvent characters];
int numCharacters = [characters length];
if (numCharacters == 1 && [characters characterAtIndex:0] == NSUpArrowFunctionKey && modifiers == (NSControlKeyMask | NSFunctionKeyMask))
{
return YES;
}
return NO;
}
static BOOL KFKeyEventIsDeleteEvent(NSEvent *keyEvent)
{
unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
NSString *characters = [keyEvent characters];
int numCharacters = [characters length];
if (numCharacters == 1 && [characters characterAtIndex:0] == NSDeleteCharacter && modifiers == 0)
{
return YES;
}
if (numCharacters == 1 && [characters characterAtIndex:0] == NSBackspaceCharacter && modifiers == 0)
{
return YES;
}
return NO;
}
static BOOL KFKeyEventIsCancelEvent(NSEvent *keyEvent)
{
unsigned int modifiers = [keyEvent modifierFlags] & modifierFlagsICareAboutMask;
NSString *characters = [keyEvent characters];
int numCharacters = [characters length];
const unichar EscapeKeyCharacter = 0x1b;
if ((modifiers == NSCommandKeyMask) && [characters isEqualToString:@"."])
{
return YES;
}
if (numCharacters == 1 && [characters characterAtIndex:0] == EscapeKeyCharacter && modifiers == 0)
{
return YES;
}
return NO;
}
#pragma mark finding patterns
- (void)findNext:(id)sender
{
NSString *lastPattern = [self kfLastSuccessfullyMatchedPattern];
if (lastPattern == nil || ![self kfCanPerformTypeSelect])
{
NSBeep();
}
else
{
[self kfFindPattern:lastPattern
initialRow:[self selectedRow] + 1
topToBottom:YES
allowExtension:NO];
}
}
- (void)findPrevious:(id)sender
{
NSString *lastPattern = [self kfLastSuccessfullyMatchedPattern];
if (lastPattern == nil || ![self kfCanPerformTypeSelect])
{
NSBeep();
}
else
{
[self kfFindPattern:lastPattern
initialRow:[self selectedRow] - 1
topToBottom:NO
allowExtension:NO];
}
}
- (void)kfFindPattern:(NSString *)pattern
initialRow:(int)initialRow
topToBottom:(BOOL)topToBottom
allowExtension:(BOOL)allowPatternExtension
{
NSArray *searchColumns = [self kfSearchColumns];
BOOL shouldWrap = [self searchWraps];
unsigned patternMatchOptions;
NSDate *distantPast = [NSDate distantPast];
NSMutableArray *suspendedEvents = [NSMutableArray array];
const uint64_t eventCheckFrequency = SecondsToMachAbsolute(.01);
NSString *match = nil;
NSRange matchRange = {0,0};
// we'll translate topToBottom into these parameters
// so that we can use a single loop for both directions
int rowIncrement, boundaryRow;
if (topToBottom)
{
rowIncrement = 1;
boundaryRow = [self numberOfRows];
initialRow = (initialRow < boundaryRow) ? initialRow : boundaryRow;
initialRow = (initialRow > 0) ? initialRow : 0;
if (initialRow == 0)
shouldWrap = NO;
}
else
{
rowIncrement = -1;
boundaryRow = -1;
initialRow = (initialRow > boundaryRow) ? initialRow : boundaryRow;
initialRow = (initialRow < [self numberOfRows] - 1) ? initialRow : [self numberOfRows] - 1;
if (initialRow == [self numberOfRows] - 1)
shouldWrap = NO;
}
// keep ivars in sync
[self setPattern:pattern];
[self setKfCanExtendFind:allowPatternExtension];
// set up pattern match options
if ([self matchAlgorithm] == KFPrefixMatchAlgorithm)
{
patternMatchOptions = NSCaseInsensitiveSearch | NSAnchoredSearch;
}
else // substring match
{
patternMatchOptions = NSCaseInsensitiveSearch;
}
BOOL finished = NO;
int row = initialRow;
while (!finished)
{
// Mail generates 3MB in autoreleased objects in a search through 15000 rows
// there's a noticable pause when they're deallocated. We'll avoid it by using
// our own autorelease pool.
// (Update - implementing typeSelectTableView:stringValueForTableColumn:row: in the mail
// plugin dropped the number of allocations)
//
// Note: checking for new input no more often than 100 times a second drops time spent
// in -[NSApplication nextEventMatchingMask::::] from 30-60% of total function time to .2-.5%
// at a cost of < 1% for timing functions.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
finished = [self kfWorkUnitGetMatch:&match
range:&matchRange
lastSearchedRow:&row
forPattern:pattern
matchOptions:patternMatchOptions
initialRow:row
boundaryRow:boundaryRow
rowIncrement:rowIncrement
searchColumns:searchColumns
timeout:eventCheckFrequency];
[match retain];
[pool release];
[match autorelease];
if (!finished)
row += rowIncrement;
if (finished && match == nil && shouldWrap)
{
if (topToBottom)
row = 0;
else
row = [self numberOfRows] - 1;
boundaryRow = initialRow;
shouldWrap = NO;
finished = NO;
}
if (!finished)
{
NSEvent *keyEvent;
while ((keyEvent = [NSApp nextEventMatchingMask:NSKeyDownMask
untilDate:distantPast // means grab events that have already occurred
inMode:NSEventTrackingRunLoopMode
dequeue:YES]) != nil)
{
if (KFKeyEventIsCancelEvent(keyEvent))
{
[self kfResetSearch];
// we intentionally dump the suspended events in this case
return;
}
else if (allowPatternExtension && KFKeyEventIsExtendFindEvent(keyEvent))
{
NSText *fieldEditor = [[self window] fieldEditor:YES forObject:self];
[fieldEditor interpretKeyEvents:[NSArray arrayWithObject:keyEvent]];
pattern = [fieldEditor string];
[self setPattern:pattern];
}
else if (KFKeyEventIsDeleteEvent(keyEvent))
{
// eat the event, do nothing.
// User might expect us to knock a character off the pattern - that'd be dangerous.
// If the user mistimed he could trigger a table view delete action.
// Best to squelch the behavior by not doing anything useful.
}
else
{
[suspendedEvents addObject:keyEvent];
}
}
}
}
if (match != nil)
{
[self kfDidFindMatch:match
range:matchRange
inRow:row];
}
else
{
[self kfDidFailToFindMatchSearchingToRow:row];
}
int numSuspendedEvents, i;
numSuspendedEvents = [suspendedEvents count];
for (i = numSuspendedEvents-1; i >= 0; i--)
{
[NSApp postEvent:[suspendedEvents objectAtIndex:i] atStart:YES];
}
}
- (BOOL)kfWorkUnitGetMatch:(NSString **)match
range:(NSRange *)matchRange
lastSearchedRow:(int *)lastSearchedRow
forPattern:(NSString *)pattern
matchOptions:(unsigned)patternMatchOptions
initialRow:(int)initialRow
boundaryRow:(int)boundaryRow
rowIncrement:(int)rowIncrement
searchColumns:(NSArray *)searchColumns
timeout:(uint64_t)timeout // times are mach absolute times
{
int row, col;
int numCols = [searchColumns count];
NSString *candidateMatch;
NSRange rangeOfPattern;
uint64_t stopTime = mach_absolute_time() + timeout;
for (row = initialRow; row != boundaryRow; row += rowIncrement)
{
for (col = 0; col < numCols; col++)
{
candidateMatch = [self kfStringValueForTableColumn:[searchColumns objectAtIndex:col] row:row];
rangeOfPattern = [candidateMatch rangeOfString:pattern options:patternMatchOptions];
if ( (rangeOfPattern.location != NSNotFound)
&& [self kfShouldAcceptMatch:candidateMatch range:rangeOfPattern inRow:row])
{
*match = candidateMatch;
*matchRange = rangeOfPattern;
*lastSearchedRow = row;
return YES;
}
}
// think of this as part of the loop condition, but we want to make sure that
// the loop completes at least one iteration
if (mach_absolute_time() > stopTime)
{
row += rowIncrement;
break;
}
}
*match = nil;
*matchRange = NSMakeRange(NSNotFound, 0);
*lastSearchedRow = row - rowIncrement;
return row == boundaryRow;
}
- (BOOL)kfShouldAcceptMatch:(NSString *)match
range:(NSRange)matchedRange
inRow:(int)row
{
id delegate = [self delegate];
if ( [self isKindOfClass:[NSOutlineView class]]
&& [delegate respondsToSelector:@selector(outlineView:shouldSelectItem:)])
{
return [delegate outlineView:(NSOutlineView *)self shouldSelectItem:[(NSOutlineView *)self itemAtRow:row]];
}
else if ([delegate respondsToSelector:@selector(tableView:shouldSelectRow:)])
{
return [delegate tableView:self shouldSelectRow:row];
}
else
{
return YES;
}
}
- (NSTimeInterval)kfPatternTimeoutInterval
{
// from Dan Wood's 'Table Techniques Taught Tastefully', as pointed out by someone
// on cocoadev.com
// Timeout is two times the key repeat rate "InitialKeyRepeat" user default.
// (converted from sixtieths of a second to seconds), but no more than two seconds.
// This behavior is determined based on Inside Macintosh documentation on the List Manager.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
int keyThreshTicks = [defaults integerForKey:@"InitialKeyRepeat"]; // undocumented key. Still valid in 10.3.
if (0 == keyThreshTicks) // missing value in defaults? Means user has never changed the default.
{
keyThreshTicks = 35; // apparent default value. translates to 1.17 sec timeout.
}
return MIN(2.0/60.0*keyThreshTicks, 2.0);
}
- (BOOL)kfCanPerformTypeSelect
{
return [self kfCanGetTableData] && [self kfSelectionShouldChange];
}
- (BOOL)kfSelectionShouldChange
{
id delegate = [self delegate];
if ( [self isKindOfClass:[NSOutlineView class]]
&& [delegate respondsToSelector:@selector(selectionShouldChangeInOutlineView:)])
{
return [delegate selectionShouldChangeInOutlineView:(NSOutlineView *)self];
}
else if ([delegate respondsToSelector:@selector(selectionShouldChangeInTableView:)])
{
return [delegate selectionShouldChangeInTableView:self];
}
else
{
return YES;
}
}
- (BOOL)kfCanGetTableData
{
// First case: datasource implements NSTableViewDataSource protocol. Usually not true when
// table view uses bindings or is actually an outline view.
// Second case: self is an outline view and datasource implements NSOutlineViewDataSource protocol. This could arise when using class posing.
// Third case: our delegate supplies the info we need.
return ([[self dataSource] respondsToSelector:@selector(tableView:objectValueForTableColumn:row:)] ||
([self isKindOfClass:[NSOutlineView class]] && [[self dataSource] respondsToSelector:@selector(outlineView:objectValueForTableColumn:byItem:)]) ||
[[self delegate] respondsToSelector:@selector(typeSelectTableView:stringValueForTableColumn:row:)]);
}
- (NSString *)kfStringValueForTableColumn:(NSTableColumn *)column row:(int)row
{
// There are three ways we can get this information: (1) our delegate supplies it, (2) our datasource
// supplies it like an NSTableViewDataSource, (3) our datasource supplies it like an NSOutlineViewDataSource
// could optimize by factoring into three separate methods and precomputing which one to call (from keyDown:).
// current sharking indicates this wouldn't help much.
id delegate = [self delegate];
NSString *stringValue = nil;
if ([delegate respondsToSelector:@selector(typeSelectTableView:stringValueForTableColumn:row:)])
{
stringValue = [delegate typeSelectTableView:self stringValueForTableColumn:column row:row];
}
else
{
id objectValue = nil;
id dataSource = [self dataSource];
// why do we check our own class? outline view is not bindings enabled, so datasource could be
// acting as a datasource for an outline while being a binding data source for us
if ([self isKindOfClass:[NSOutlineView class]]
&& [dataSource respondsToSelector:@selector(outlineView:objectValueForTableColumn:byItem:)])
{
objectValue = [dataSource outlineView:(NSOutlineView *)self
objectValueForTableColumn:column
byItem:[(NSOutlineView *)self itemAtRow:row]];
}
else if ([dataSource respondsToSelector:@selector(tableView:objectValueForTableColumn:row:)])
{
objectValue = [dataSource tableView:self objectValueForTableColumn:column row:row];
}
NSCell *dataCell = [column dataCellForRow:row];
[dataCell setObjectValue:objectValue];
// sometimes the delegate changes the cell value in tableView:willDisplayCell:forTableColumn:row:
if ([self isKindOfClass:[NSOutlineView class]]
&& [delegate respondsToSelector:@selector(outlineView:willDisplayCell:forTableColumn:item:)])
{
[delegate outlineView:(NSOutlineView *)self
willDisplayCell:dataCell
forTableColumn:column
item:[(NSOutlineView *)self itemAtRow:row]];
}
else if ([delegate respondsToSelector:@selector(tableView:willDisplayCell:forTableColumn:row:)])
{
[delegate tableView:self
willDisplayCell:dataCell
forTableColumn:column
row:row];
}
stringValue = [dataCell stringValue];
}
if (stringValue == nil)
{
stringValue = @"";
}
return stringValue;
}
- (NSArray *)kfSearchColumns
{
NSArray *searchColumns;
NSSet *searchColumnIdentifiers = [self searchColumnIdentifiers];
if (searchColumnIdentifiers != nil)
{
NSMutableArray *partialSearchColumns;
NSArray *candidateColumns = [self tableColumns];
NSTableColumn *column;
int numCols, col;
partialSearchColumns = [NSMutableArray array];
numCols = [candidateColumns count];
for (col = 0; col < numCols; col++)
{
column = [candidateColumns objectAtIndex:col];
if ([searchColumnIdentifiers containsObject:[column identifier]])
{
[partialSearchColumns addObject:column];
}
}
searchColumns = partialSearchColumns;
}
else
{
searchColumns = [self tableColumns];
}
return searchColumns;
}
- (BOOL)kfSearchTopToBottom
{
BOOL topToBottom = YES;
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(typeSelectTableViewSearchTopToBottom:)])
{
topToBottom = [delegate typeSelectTableViewSearchTopToBottom:self];
}
return topToBottom;
}
- (int)kfInitialRowForNewSearch
{
int row;
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(typeSelectTableViewInitialSearchRow:)])
{
row = [delegate typeSelectTableViewInitialSearchRow:self];
}
else
{
if ([self kfSearchTopToBottom])
{
row = 0;
}
else
{
row = [self numberOfRows] - 1;
}
}
return row;
}
#pragma mark taking action
-(void)kfPatternDidChange:(id)sender
{
NSInvocation *timeoutInvocation = [self kfTimeoutInvocation];
[[timeoutInvocation class] cancelPreviousPerformRequestsWithTarget:[self kfTimeoutInvocation]
selector:@selector(invoke)
object:nil];
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(typeSelectTableViewPatternDidChange:)])
[delegate typeSelectTableViewPatternDidChange:sender];
}
- (void)kfDidFindMatch:(NSString *)match
range:(NSRange)matchedRange
inRow:(int)row
{
NSString *pattern = [self pattern];
// update ivars
[self setKfLastSuccessfullyMatchedPattern:pattern];
if ([self kfCanExtendFind])
{
[self setKfSavedRowForExtensionSearch:row];
}
// select row
[self selectRow:row byExtendingSelection:NO];
if (![self kfRowIsVisible:row])
{
// this is what NSTextView does when it finds patterns, and it's what Mail does
// when moving through message table with up and down arrows
[self kfScrollRectToCenter:[self rectOfRow:row] vertical:YES horizontal:NO];
}
// start pattern timeout timer (see kfTimeoutInvocation for details)
[[self kfTimeoutInvocation] performSelector:@selector(invoke)
withObject:nil
afterDelay:[self kfPatternTimeoutInterval]
inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]];
// inform the delegate
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(typeSelectTableView:didFindMatch:range:forPattern:)])
{
[delegate typeSelectTableView:self didFindMatch:match range:matchedRange forPattern:pattern];
}
}
- (void)kfDidFailToFindMatchSearchingToRow:(int)row
{
if ([self kfCanExtendFind])
{
[self setKfSavedRowForExtensionSearch:row];
}
// start pattern timeout timer (see kfTimeoutInvocation for details)
[[self kfTimeoutInvocation] performSelector:@selector(invoke)
withObject:nil
afterDelay:[self kfPatternTimeoutInterval]
inModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]];
id delegate = [self delegate];
if ([delegate respondsToSelector:@selector(typeSelectTableView:didFailToFindMatchForPattern:)])
{
[delegate typeSelectTableView:self didFailToFindMatchForPattern:[self pattern]];
}
else
{
NSBeep();
}
}
- (void)kfResetSearch
{
// note - doesn't clear hanging dead key. See keyDown for discussion and workaround.
[self setPattern:@""];
[self setKfCanExtendFind:NO];
}
// note: don't use setDelegate: to call this. NSOutlineView doesn't call through to
// -[NSTableView setDelegate:], so it messes us up when posing.
- (void)kfConfigureDelegateIfNeeded
{
id delegate = [self delegate];
if (delegate != [self kfLastConfiguredDelegate])
{
// order is important here
// We don't want to go into a recursion if the delegate tries to access a configurable value from
// configureTypeSelectTableView. The delegate is interested in the pre-configuration values anyway.
[self setKfLastConfiguredDelegate:delegate];
if ([delegate respondsToSelector:@selector(configureTypeSelectTableView:)])
[delegate configureTypeSelectTableView:self];
}
}
#pragma mark utility
- (BOOL)kfRowIsVisible:(int)row
{
NSScrollView *enclosingScrollView = [self enclosingScrollView];
if (enclosingScrollView == nil)
{
return NO;
}
else
{
NSRect visibleRect = [enclosingScrollView documentVisibleRect];
NSRect rowRect = [self rectOfRow:row];
// only care about whether we're onscreen vertically
return ( (NSMaxY(visibleRect) >= NSMaxY(rowRect))
&& (NSMinY(visibleRect) <= NSMinY(rowRect)));
}
}
- (void)kfScrollRectToCenter:(NSRect)aRect vertical:(BOOL)scrollVertical horizontal:(BOOL)scrollHorizontal
{
NSScrollView *scrollView = [self enclosingScrollView];
if (scrollView != nil)
{
NSRect newVisibleRect = [scrollView documentVisibleRect];
if (scrollVertical)
newVisibleRect.origin.y += NSMidY(aRect) - NSMidY([scrollView documentVisibleRect]);
if (scrollHorizontal)
newVisibleRect.origin.x += NSMidX(aRect) - NSMidX([scrollView documentVisibleRect]);
newVisibleRect = NSIntersectionRect(newVisibleRect,[self bounds]);
[self scrollRectToVisible:newVisibleRect];
}
}
#pragma mark -
#pragma mark ACCESSORS
#pragma mark -
#pragma mark simulated ivars setup
static NSMutableDictionary *idToSimulatedIvarsMap = nil;
- (NSMutableDictionary *)kfSimulatedIvars
{
NSMutableDictionary *simulatedIvars = [idToSimulatedIvarsMap objectForKey:[self kfIdentifier]];
if (simulatedIvars == nil)
{
[self kfSetUpSimulatedIvars];
simulatedIvars = [idToSimulatedIvarsMap objectForKey:[self kfIdentifier]];
}
return simulatedIvars;
}
// can avoid memory allocation if we use CFDictionary or NSMapTable and work with self directly
- (id)kfIdentifier
{
return [NSValue valueWithPointer:self];
}
- (void)kfSetUpSimulatedIvars
{
// prime idToSimulatedIvarsMap
if (idToSimulatedIvarsMap == nil)
{
idToSimulatedIvarsMap = [[NSMutableDictionary alloc] init];
}
// if the simulatedIvars dict doesn't exist yet, create it
NSMutableDictionary *simulatedIvars = [idToSimulatedIvarsMap objectForKey:[self kfIdentifier]];
if (!simulatedIvars)
{
simulatedIvars = [NSMutableDictionary dictionary];
[idToSimulatedIvarsMap setObject:simulatedIvars forKey:[self kfIdentifier]];
}
}
- (void)kfTearDownSimulatedIvars
{
[idToSimulatedIvarsMap removeObjectForKey:[self kfIdentifier]];
if ([idToSimulatedIvarsMap count] == 0)
{
[idToSimulatedIvarsMap release];
idToSimulatedIvarsMap = nil;
}
}
#pragma mark private accessors
- (int)kfSavedRowForExtensionSearch
{
int row;
NSNumber *rowNumber = [[self kfSimulatedIvars] objectForKey:@"initialRowForExtensionSearch"];
// default value
if (rowNumber == nil)
row = NSNotFound;
else
{
row = [rowNumber intValue];
}
return row;
}
- (void)setKfSavedRowForExtensionSearch:(int)row
{
[[self kfSimulatedIvars] setObject:[NSNumber numberWithInt:row]
forKey:@"initialRowForExtensionSearch"];
}
- (NSString *)kfLastSuccessfullyMatchedPattern
{
NSString *string = [[self kfSimulatedIvars] objectForKey:@"lastPattern"];
// defaults to nil
return string;
}
- (void)setKfLastSuccessfullyMatchedPattern:(NSString *)string
{
if (string == nil)
[[self kfSimulatedIvars] removeObjectForKey:@"lastPattern"];
else
[[self kfSimulatedIvars] setObject:[[string copy] autorelease] forKey:@"lastPattern"];
}
-(BOOL)kfCanExtendFind
{
NSNumber *canExtendFindNumber = [[self kfSimulatedIvars] objectForKey:@"canExtendFind"];
// default value
if (canExtendFindNumber == nil)
return NO;
else
return [canExtendFindNumber boolValue];
}
-(void)setKfCanExtendFind:(BOOL)flag
{
[[self kfSimulatedIvars] setObject:[NSNumber numberWithBool:flag]
forKey:@"canExtendFind"];
}
// keep track of the last delegate for which we tried to run configureTypeSelectTableView
- (id)kfLastConfiguredDelegate
{
return [[[self kfSimulatedIvars] objectForKey:@"lastConfiguredDelegate"] nonretainedObjectValue];
}
- (void)setKfLastConfiguredDelegate:(id)anObject
{
if (anObject == nil)
[[self kfSimulatedIvars] removeObjectForKey:@"lastConfiguredDelegate"];
else
[[self kfSimulatedIvars] setObject:[NSValue valueWithNonretainedObject:anObject] forKey:@"lastConfiguredDelegate"];
}
//
// the timeoutNotification encapsulates the message that we send to self when the timeout
// (for clearing the input buffer) expires. Invoke it with a delayed message send.
//
// Why do we use an invocation instead of doing the delayed message send directly?
// -[NSObject performSelector:afterDelay:] retains the receiver until after the message send is
// performed. That can extend the life of the tableView past the life of the delegate, which is
// bad mojo. Yielded a crash in Adium. By buffering with an invocation that doesn't retain its
// target, we can avoid the problem. Any pending delayed messages are cancelled when the table
// table is dealloc'd.
//
- (NSInvocation *)kfTimeoutInvocation
{
NSInvocation *invocation = [[self kfSimulatedIvars] objectForKey:@"timeoutInvocation"];
// defaults to a message to kfResetSearch
if (invocation == nil)
{
SEL selector = @selector(kfResetSearch);
invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
[invocation setTarget:self];
[invocation setSelector:selector];
[self setKfTimeoutInvocation:invocation];
}
return invocation;
}
- (void)setKfTimeoutInvocation:(NSInvocation *)anInvocation
{
if (anInvocation == nil)
[[self kfSimulatedIvars] removeObjectForKey:@"timeoutInvocation"];
else
[[self kfSimulatedIvars] setObject:anInvocation forKey:@"timeoutInvocation"];
}
-(void)setPattern:(NSString *)pattern
{
NSString *oldPattern = [self pattern];
if (pattern == nil)
pattern = @"";
[[self kfSimulatedIvars] setObject:[[pattern copy] autorelease]
forKey:@"pattern"];
NSNotification *patternChangedNotification = [NSNotification notificationWithName:KFTypeSelectTableViewPatternDidChangeNotification
object:self
userInfo:[NSDictionary dictionaryWithObject:oldPattern forKey:@"oldPattern"]];
[self kfPatternDidChange:patternChangedNotification];
[[NSNotificationCenter defaultCenter] postNotification:patternChangedNotification];
}
#pragma mark public accessors
-(NSString *)pattern
{
NSString *pattern = [[self kfSimulatedIvars] objectForKey:@"pattern"];
if (pattern == nil)
pattern = @"";
return [[pattern retain] autorelease];
}
static KFTypeSelectMatchAlgorithm defaultMatchAlgorith = KFPrefixMatchAlgorithm;
+ (KFTypeSelectMatchAlgorithm)defaultMatchAlgorithm
{
return defaultMatchAlgorith;
}
+ (void)setDefaultMatchAlgorithm:(KFTypeSelectMatchAlgorithm)algorithm
{
defaultMatchAlgorith = algorithm;
}
-(KFTypeSelectMatchAlgorithm)matchAlgorithm
{
[self kfConfigureDelegateIfNeeded];
NSNumber *algorithmNumber = [[self kfSimulatedIvars] objectForKey:@"matchAlgorithm"];
if (algorithmNumber == nil)
return defaultMatchAlgorith;
else
return [algorithmNumber intValue];
}
-(void)setMatchAlgorithm:(KFTypeSelectMatchAlgorithm)algorithm
{
[[self kfSimulatedIvars] setObject:[NSNumber numberWithInt:algorithm] forKey:@"matchAlgorithm"];
}
- (BOOL)searchWraps
{
[self kfConfigureDelegateIfNeeded];
NSNumber *searchWraphsNum = [[self kfSimulatedIvars] objectForKey:@"searchWraps"];
// default value
if (searchWraphsNum == nil)
return NO;
else
return [searchWraphsNum boolValue];
}
-(void)setSearchWraps:(BOOL)flag
{
[[self kfSimulatedIvars] setObject:[NSNumber numberWithBool:flag]
forKey:@"searchWraps"];
}
- (NSSet *)searchColumnIdentifiers
{
[self kfConfigureDelegateIfNeeded];
return [[self kfSimulatedIvars] objectForKey:@"searchColumnIdentifiers"];
}
- (void)setSearchColumnIdentifiers:(NSSet *)identifiers
{
if (identifiers == nil)
[[self kfSimulatedIvars] removeObjectForKey:@"searchColumnIdentifiers"];
else
[[self kfSimulatedIvars] setObject:identifiers forKey:@"searchColumnIdentifiers"];
}
@end
#pragma mark -
#pragma mark HELPER
#pragma mark -
// need a time function, don't want it to be sensitive to time zone switches or
// clock syncs, would rather not require linking Carbon. We'll go with mach_absolute_time.
// Written with reference to <http://developer.apple.com/qa/qa2004/qa1398.html>.
static uint64_t SecondsToMachAbsolute(double seconds)
{
double nanoseconds_d = seconds * 1000000000;
Nanoseconds nanoseconds_n = UInt64ToUnsignedWide((uint64_t) nanoseconds_d);
return UnsignedWideToUInt64(NanosecondsToAbsolute(nanoseconds_n));
}