Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

545 lines (423 sloc) 15.832 kb
//
// 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:@"--"];
[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.string]) {
[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
Jump to Line
Something went wrong with that request. Please try again.