Files
gitx/PBHistorySearchController.m
German Laullon ac1019d5cc no libgit2
2011-05-20 00:46:13 +02:00

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