2007-02-18 17:35:28 +00:00
|
|
|
|
//
|
|
|
|
|
// 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;
|
2007-06-03 14:12:15 +00:00
|
|
|
|
[super keyDown:keyEvent];
|
2007-02-18 17:35:28 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2007-06-03 14:12:15 +00:00
|
|
|
|
|
2007-02-18 17:35:28 +00:00
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|