mirror of
https://github.com/kennethreitz-archive/gitx.git
synced 2026-06-05 15:30:18 +00:00
546 lines
16 KiB
Objective-C
546 lines
16 KiB
Objective-C
//
|
|
// PBHistorySearchController.m
|
|
// GitX
|
|
//
|
|
// Created by Nathan Kinsinger on 8/21/10.
|
|
// Copyright 2010 Nathan Kinsinger. All rights reserved.
|
|
//
|
|
|
|
#import "PBHistorySearchController.h"
|
|
#import "PBGitHistoryController.h"
|
|
#import "PBGitRepository.h"
|
|
#import "PBGitDefaults.h"
|
|
#import <QuartzCore/CoreAnimation.h>
|
|
|
|
|
|
@interface PBHistorySearchController ()
|
|
|
|
- (void)selectNextResultInDirection:(NSInteger)direction;
|
|
|
|
- (void)updateUI;
|
|
- (void)setupSearchMenuTemplate;
|
|
|
|
- (void)startBasicSearch;
|
|
- (void)startBackgroundSearch;
|
|
- (void)clearProgressIndicator;
|
|
|
|
- (void)showSearchRewindPanelReverse:(BOOL)isReversed;
|
|
|
|
@end
|
|
|
|
|
|
#define kGitXSearchDirectionNext 1
|
|
#define kGitXSearchDirectionPrevious -1
|
|
|
|
#define kGitXBasicSearchLabel @"Subject, Author, SHA"
|
|
#define kGitXPickaxeSearchLabel @"Commit (pickaxe)"
|
|
#define kGitXRegexSearchLabel @"Commit (pickaxe regex)"
|
|
#define kGitXPathSearchLabel @"File path"
|
|
|
|
#define kGitXSearchArrangedObjectsContext @"GitXSearchArrangedObjectsContext"
|
|
|
|
|
|
@implementation PBHistorySearchController
|
|
|
|
@synthesize historyController;
|
|
@synthesize commitController;
|
|
|
|
@synthesize searchField;
|
|
@synthesize stepper;
|
|
@synthesize numberOfMatchesField;
|
|
@synthesize progressIndicator;
|
|
|
|
@synthesize searchMode;
|
|
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Public methods
|
|
|
|
- (BOOL)isRowInSearchResults:(NSInteger)rowIndex
|
|
{
|
|
return [results containsIndex:rowIndex];
|
|
}
|
|
|
|
- (BOOL)hasSearchResults
|
|
{
|
|
return ([results count] > 0);
|
|
}
|
|
|
|
- (void)selectSearchMode:(id)sender
|
|
{
|
|
self.searchMode = [sender tag];
|
|
[self updateSearch:self];
|
|
}
|
|
|
|
- (void)selectNextResult
|
|
{
|
|
[self selectNextResultInDirection:kGitXSearchDirectionNext];
|
|
}
|
|
|
|
- (void)selectPreviousResult
|
|
{
|
|
[self selectNextResultInDirection:kGitXSearchDirectionPrevious];
|
|
}
|
|
|
|
- (IBAction)stepperPressed:(id)sender
|
|
{
|
|
NSInteger selectedSegment = [sender selectedSegment];
|
|
|
|
if (selectedSegment == 0)
|
|
[self selectPreviousResult];
|
|
else
|
|
[self selectNextResult];
|
|
}
|
|
|
|
- (void)clearSearch
|
|
{
|
|
[searchField setStringValue:@""];
|
|
if (results) {
|
|
results = nil;
|
|
[historyController.commitList reloadData];
|
|
}
|
|
[self updateUI];
|
|
}
|
|
|
|
- (IBAction)updateSearch:(id)sender
|
|
{
|
|
if (self.searchMode == kGitXBasicSeachMode)
|
|
[self startBasicSearch];
|
|
else
|
|
[self startBackgroundSearch];
|
|
}
|
|
|
|
- (void)setHistorySearch:(NSString *)searchString mode:(NSInteger)mode
|
|
{
|
|
if (searchString && ![searchString isEqualToString:@""]) {
|
|
self.searchMode = mode;
|
|
[searchField setStringValue:searchString];
|
|
// use performClick: so that the search field will save it as a recent search
|
|
[searchField performClick:self];
|
|
}
|
|
}
|
|
|
|
- (void)awakeFromNib
|
|
{
|
|
[self setupSearchMenuTemplate];
|
|
self.searchMode = [PBGitDefaults historySearchMode];
|
|
|
|
[self updateUI];
|
|
|
|
[commitController addObserver:self forKeyPath:@"arrangedObjects" options:0 context:kGitXSearchArrangedObjectsContext];
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
|
{
|
|
if ([(NSString *)context isEqualToString:kGitXSearchArrangedObjectsContext]) {
|
|
// the objects in the commitlist changed so the result indexes are no longer valid
|
|
[self clearSearch];
|
|
return;
|
|
}
|
|
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
}
|
|
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Private methods
|
|
|
|
- (void)selectIndex:(NSUInteger)index
|
|
{
|
|
if ([[commitController arrangedObjects] count] > index) {
|
|
PBGitCommit *commit = [[commitController arrangedObjects] objectAtIndex:index];
|
|
[historyController selectCommit:[commit sha]];
|
|
}
|
|
}
|
|
|
|
- (void)selectNextResultInDirection:(NSInteger)direction
|
|
{
|
|
if (![results count])
|
|
return;
|
|
|
|
NSUInteger selectedRow = [historyController.commitList selectedRow];
|
|
if (selectedRow == NSNotFound) {
|
|
[self selectIndex:[results firstIndex]];
|
|
return;
|
|
}
|
|
|
|
NSUInteger currentResult = NSNotFound;
|
|
if (direction == kGitXSearchDirectionNext)
|
|
currentResult = [results indexGreaterThanIndex:selectedRow];
|
|
else
|
|
currentResult = [results indexLessThanIndex:selectedRow];
|
|
|
|
if (currentResult == NSNotFound) {
|
|
if (direction == kGitXSearchDirectionNext)
|
|
currentResult = [results firstIndex];
|
|
else
|
|
currentResult = [results lastIndex];
|
|
|
|
[self showSearchRewindPanelReverse:(direction != kGitXSearchDirectionNext)];
|
|
}
|
|
|
|
[self selectIndex:currentResult];
|
|
}
|
|
|
|
- (NSString *)numberOfMatchesString
|
|
{
|
|
NSUInteger numberOfMatches = [results count];
|
|
|
|
if (numberOfMatches == 0)
|
|
return @"Not found";
|
|
|
|
if (numberOfMatches == 1)
|
|
return @"1 match";
|
|
|
|
return [NSString stringWithFormat:@"%d matches", numberOfMatches];
|
|
}
|
|
|
|
- (void)updateUI
|
|
{
|
|
if ([[searchField stringValue] isEqualToString:@""]) {
|
|
[numberOfMatchesField setHidden:YES];
|
|
[stepper setHidden:YES];
|
|
}
|
|
else {
|
|
[numberOfMatchesField setStringValue:[self numberOfMatchesString]];
|
|
[numberOfMatchesField setHidden:NO];
|
|
[stepper setHidden:NO];
|
|
[historyController.commitList reloadData];
|
|
}
|
|
[self clearProgressIndicator];
|
|
}
|
|
|
|
// changes the selection to the next match after the current selected row unless the current row is already a match
|
|
- (void)updateSelectedResult
|
|
{
|
|
NSString *searchString = [searchField stringValue];
|
|
if ([searchString isEqualToString:@""]) {
|
|
[self clearSearch];
|
|
return;
|
|
}
|
|
|
|
if (![self isRowInSearchResults:[historyController.commitList selectedRow]])
|
|
[self selectNextResult];
|
|
|
|
[self updateUI];
|
|
}
|
|
|
|
- (void)setupSearchMenuTemplate
|
|
{
|
|
NSMenu *searchMenu = [[NSMenu alloc] initWithTitle:@"Search Menu"];
|
|
NSMenuItem *item;
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:kGitXBasicSearchLabel action:@selector(selectSearchMode:) keyEquivalent:@""];
|
|
[item setTarget:self];
|
|
[item setTag:kGitXBasicSeachMode];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:kGitXPickaxeSearchLabel action:@selector(selectSearchMode:) keyEquivalent:@""];
|
|
[item setTarget:self];
|
|
[item setTag:kGitXPickaxeSearchMode];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:kGitXRegexSearchLabel action:@selector(selectSearchMode:) keyEquivalent:@""];
|
|
[item setTarget:self];
|
|
[item setTag:kGitXRegexSearchMode];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:kGitXPathSearchLabel action:@selector(selectSearchMode:) keyEquivalent:@""];
|
|
[item setTarget:self];
|
|
[item setTag:kGitXPathSearchMode];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [NSMenuItem separatorItem];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:@"Recent Searches" action:NULL keyEquivalent:@""];
|
|
[item setTag:NSSearchFieldRecentsTitleMenuItemTag];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:@"Recents" action:NULL keyEquivalent:@""];
|
|
[item setTag:NSSearchFieldRecentsMenuItemTag];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [NSMenuItem separatorItem];
|
|
[item setTag:NSSearchFieldRecentsTitleMenuItemTag];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:@"Clear Recent Searches" action:NULL keyEquivalent:@""];
|
|
[item setTag:NSSearchFieldClearRecentsMenuItemTag];
|
|
[searchMenu addItem:item];
|
|
|
|
item = [[NSMenuItem alloc] initWithTitle:@"No Recent Searches" action:NULL keyEquivalent:@""];
|
|
[item setTag:NSSearchFieldNoRecentsMenuItemTag];
|
|
[searchMenu addItem:item];
|
|
|
|
[[searchField cell] setSearchMenuTemplate:searchMenu];
|
|
}
|
|
|
|
- (void)updateSearchMenuState
|
|
{
|
|
NSMenu *searchMenu = [[searchField cell] searchMenuTemplate];
|
|
if (!searchMenu)
|
|
return;
|
|
|
|
NSMenuItem *item;
|
|
|
|
item = [searchMenu itemWithTag:kGitXBasicSeachMode];
|
|
[item setState:(searchMode == kGitXBasicSeachMode) ? NSOnState : NSOffState];
|
|
|
|
item = [searchMenu itemWithTag:kGitXPickaxeSearchMode];
|
|
[item setState:(searchMode == kGitXPickaxeSearchMode) ? NSOnState : NSOffState];
|
|
|
|
item = [searchMenu itemWithTag:kGitXRegexSearchMode];
|
|
[item setState:(searchMode == kGitXRegexSearchMode) ? NSOnState : NSOffState];
|
|
|
|
item = [searchMenu itemWithTag:kGitXPathSearchMode];
|
|
[item setState:(searchMode == kGitXPathSearchMode) ? NSOnState : NSOffState];
|
|
|
|
[[searchField cell] setSearchMenuTemplate:searchMenu];
|
|
|
|
[PBGitDefaults setHistorySearchMode:searchMode];
|
|
}
|
|
|
|
- (void)updateSearchPlaceholderString
|
|
{
|
|
switch (self.searchMode) {
|
|
case kGitXPickaxeSearchMode:
|
|
[[searchField cell] setPlaceholderString:kGitXPickaxeSearchLabel];
|
|
break;
|
|
case kGitXRegexSearchMode:
|
|
[[searchField cell] setPlaceholderString:kGitXRegexSearchLabel];
|
|
break;
|
|
case kGitXPathSearchMode:
|
|
[[searchField cell] setPlaceholderString:kGitXPathSearchLabel];
|
|
break;
|
|
default:
|
|
[[searchField cell] setPlaceholderString:kGitXBasicSearchLabel];
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)setSearchMode:(PBHistorySearchMode)mode
|
|
{
|
|
if ((mode < kGitXBasicSeachMode) || (mode >= kGitXMaxSearchMode))
|
|
mode = kGitXBasicSeachMode;
|
|
|
|
searchMode = mode;
|
|
[PBGitDefaults setHistorySearchMode:searchMode];
|
|
|
|
[self updateSearchMenuState];
|
|
[self updateSearchPlaceholderString];
|
|
}
|
|
|
|
- (void)searchTimerFired:(NSTimer*)theTimer
|
|
{
|
|
[self.progressIndicator setHidden:NO];
|
|
[self.progressIndicator startAnimation:self];
|
|
}
|
|
|
|
- (void)clearProgressIndicator
|
|
{
|
|
[searchTimer invalidate];
|
|
searchTimer = nil;
|
|
[self.progressIndicator setHidden:YES];
|
|
[self.progressIndicator stopAnimation:self];
|
|
}
|
|
|
|
- (void)startProgressIndicator
|
|
{
|
|
[self clearProgressIndicator];
|
|
[numberOfMatchesField setHidden:YES];
|
|
[stepper setHidden:YES];
|
|
searchTimer = [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(searchTimerFired:) userInfo:nil repeats:NO];
|
|
}
|
|
|
|
|
|
|
|
#pragma mark Basic Search
|
|
|
|
- (void)startBasicSearch
|
|
{
|
|
NSString *searchString = [searchField stringValue];
|
|
if ([searchString isEqualToString:@""]) {
|
|
[self clearSearch];
|
|
return;
|
|
}
|
|
|
|
NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet];
|
|
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"subject CONTAINS[cd] %@ OR author CONTAINS[cd] %@ OR realSha BEGINSWITH[c] %@", searchString, searchString, searchString];
|
|
|
|
NSUInteger index = 0;
|
|
for (PBGitCommit *commit in [commitController arrangedObjects]) {
|
|
if ([searchPredicate evaluateWithObject:commit])
|
|
[indexes addIndex:index];
|
|
index++;
|
|
}
|
|
|
|
results = indexes;
|
|
|
|
[self updateSelectedResult];
|
|
}
|
|
|
|
|
|
|
|
#pragma mark Background Search
|
|
|
|
- (void)startBackgroundSearch
|
|
{
|
|
if (backgroundSearchTask) {
|
|
NSFileHandle *handle = [[backgroundSearchTask standardOutput] fileHandleForReading];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
|
|
[backgroundSearchTask terminate];
|
|
}
|
|
|
|
NSString *searchString = [[searchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
if ([searchString isEqualToString:@""]) {
|
|
[self clearSearch];
|
|
return;
|
|
}
|
|
|
|
results = nil;
|
|
|
|
NSMutableArray *searchArguments = [NSMutableArray arrayWithObjects:@"log", @"--pretty=format:%H", nil];
|
|
switch (self.searchMode) {
|
|
case kGitXRegexSearchMode:
|
|
[searchArguments addObject:@"--pickaxe-regex"];
|
|
case kGitXPickaxeSearchMode:
|
|
[searchArguments addObject:[NSString stringWithFormat:@"-S%@", searchString]];
|
|
break;
|
|
case kGitXPathSearchMode:
|
|
[searchArguments addObject:@"--follow"];
|
|
[searchArguments addObject:@"--"];
|
|
[searchArguments addObjectsFromArray:[searchString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
backgroundSearchTask = [PBEasyPipe taskForCommand:[PBGitBinary path] withArgs:searchArguments inDir:[[historyController.repository fileURL] path]];
|
|
[backgroundSearchTask launch];
|
|
|
|
NSFileHandle *handle = [[backgroundSearchTask standardOutput] fileHandleForReading];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(parseBackgroundSearchResults:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
|
|
[handle readToEndOfFileInBackgroundAndNotify];
|
|
|
|
[self startProgressIndicator];
|
|
}
|
|
|
|
- (void)parseBackgroundSearchResults:(NSNotification *)notification
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:[notification object]];
|
|
backgroundSearchTask = nil;
|
|
|
|
NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet];
|
|
NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
|
|
|
|
NSString *resultsString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
NSArray *resultsArray = [resultsString componentsSeparatedByString:@"\n"];
|
|
|
|
for (NSString *resultSHA in resultsArray) {
|
|
NSUInteger index = 0;
|
|
for (PBGitCommit *commit in [commitController arrangedObjects]) {
|
|
if ([resultSHA isEqualToString:commit.sha]) {
|
|
[indexes addIndex:index];
|
|
break;
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
|
|
results = indexes;
|
|
[self clearProgressIndicator];
|
|
[self updateSelectedResult];
|
|
}
|
|
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Rewind Panel
|
|
|
|
#define kRewindPanelSize 125.0f
|
|
|
|
- (void)closeRewindPanel
|
|
{
|
|
[[[historyController view] window] removeChildWindow:rewindPanel];
|
|
[rewindPanel close];
|
|
rewindPanel = nil;
|
|
}
|
|
|
|
- (NSPanel *)rewindPanelReverse:(BOOL)isReversed
|
|
{
|
|
NSRect windowFrame = [[[historyController view] window] frame];
|
|
NSRect historyFrame = [[historyController view] convertRectToBase:[[historyController view] frame]];
|
|
NSRect panelRect = NSMakeRect(0.0f, 0.0f, kRewindPanelSize, kRewindPanelSize);
|
|
panelRect.origin.x = windowFrame.origin.x + historyFrame.origin.x + ((historyFrame.size.width - kRewindPanelSize) / 2.0f);
|
|
panelRect.origin.y = windowFrame.origin.y + historyFrame.origin.y + ((historyFrame.size.height - kRewindPanelSize) / 2.0f);
|
|
|
|
NSPanel *panel = [[NSPanel alloc] initWithContentRect:panelRect
|
|
styleMask:NSBorderlessWindowMask
|
|
backing:NSBackingStoreBuffered
|
|
defer:YES];
|
|
[panel setIgnoresMouseEvents:YES];
|
|
[panel setOneShot:YES];
|
|
[panel setOpaque:NO];
|
|
[panel setBackgroundColor:[NSColor clearColor]];
|
|
[panel setHasShadow:NO];
|
|
[panel useOptimizedDrawing:YES];
|
|
[panel setAlphaValue:0.0f];
|
|
|
|
NSBox *box = [[NSBox alloc] initWithFrame:[[panel contentView] frame]];
|
|
[box setBoxType:NSBoxCustom];
|
|
[box setBorderType:NSLineBorder];
|
|
[box setFillColor:[NSColor colorWithCalibratedWhite:0.0f alpha:0.5f]];
|
|
[box setBorderColor:[NSColor colorWithCalibratedWhite:0.5f alpha:0.5f]];
|
|
[box setCornerRadius:12.0f];
|
|
[[panel contentView] addSubview:box];
|
|
|
|
NSImage *rewindImage = [[NSImage imageNamed:@"rewindImage"] copy];
|
|
[rewindImage setFlipped:isReversed];
|
|
NSSize imageSize = [rewindImage size];
|
|
NSRect imageViewFrame = NSMakeRect(21.0f, 5.0f, imageSize.width, imageSize.height);
|
|
NSImageView *rewindImageView = [[NSImageView alloc] initWithFrame:imageViewFrame];
|
|
[rewindImageView setImage:rewindImage];
|
|
[[box contentView] addSubview:rewindImageView];
|
|
|
|
return panel;
|
|
}
|
|
|
|
- (CAKeyframeAnimation *)rewindPanelFadeOutAnimation
|
|
{
|
|
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
|
|
animation.duration = 1.0f;
|
|
animation.values = [NSArray arrayWithObjects:
|
|
[NSNumber numberWithFloat:1.0f],
|
|
[NSNumber numberWithFloat:1.0f],
|
|
[NSNumber numberWithFloat:0.0f],
|
|
[NSNumber numberWithFloat:0.0f], nil];
|
|
animation.keyTimes = [NSArray arrayWithObjects:
|
|
[NSNumber numberWithFloat:0.1f],
|
|
[NSNumber numberWithFloat:0.3f],
|
|
[NSNumber numberWithFloat:0.7f],
|
|
[NSNumber numberWithFloat:animation.duration], nil];
|
|
|
|
return animation;
|
|
}
|
|
|
|
- (void)showSearchRewindPanelReverse:(BOOL)isReversed
|
|
{
|
|
if (rewindPanel)
|
|
[self closeRewindPanel];
|
|
|
|
rewindPanel = [self rewindPanelReverse:isReversed];
|
|
|
|
[[[historyController view] window] addChildWindow:rewindPanel ordered:NSWindowAbove];
|
|
|
|
CAKeyframeAnimation *alphaAnimation = [self rewindPanelFadeOutAnimation];
|
|
[rewindPanel setAnimations:[NSDictionary dictionaryWithObject:alphaAnimation forKey:@"alphaValue"]];
|
|
[[rewindPanel animator] setAlphaValue:0.0f];
|
|
|
|
[self performSelector:@selector(closeRewindPanel) withObject:nil afterDelay:0.7f];
|
|
}
|
|
|
|
@end
|