cog/Spotlight/SpotlightWindowController.m

320 lines
12 KiB
Objective-C

//
// SpotlightWindowController.m
// Cog
//
// Created by Matthew Grinshpun on 10/02/08.
// Copyright 2008 Matthew Leon Grinshpun. All rights reserved.
//
#import "SpotlightWindowController.h"
#import "PlaylistLoader.h"
#import "SpotlightPlaylistEntry.h"
#import "NSComparisonPredicate+CogPredicate.h"
#import "NSArray+CogSort.h"
#import "NSString+CogSort.h"
#import "NSNumber+CogSort.h"
#import "SpotlightTransformers.h"
#import "Logging.h"
// Minimum length of a search string (searching for very small strings gets ugly)
#define MINIMUM_SEARCH_STRING_LENGTH 3
// Store a class predicate for searching for music
static NSPredicate * musicOnlyPredicate = nil;
@implementation SpotlightWindowController
+ (void)initialize
{
musicOnlyPredicate = [NSPredicate predicateWithFormat:
@"kMDItemContentTypeTree==\'public.audio\'"];
// Register value transformers
NSValueTransformer *pausingQueryTransformer = [[PausingQueryTransformer alloc]init];
[NSValueTransformer setValueTransformer:pausingQueryTransformer forName:@"PausingQueryTransformer"];
NSValueTransformer *authorToArtistTransformer = [[AuthorToArtistTransformer alloc]init];
[NSValueTransformer setValueTransformer:authorToArtistTransformer forName:@"AuthorToArtistTransformer"];
NSValueTransformer *pathToURLTransformer = [[PathToURLTransformer alloc]init];
[NSValueTransformer setValueTransformer:pathToURLTransformer forName:@"PathToURLTransformers"];
NSValueTransformer *stringToSearchScopeTransformer = [[StringToSearchScopeTransformer alloc]init];
[NSValueTransformer setValueTransformer:stringToSearchScopeTransformer forName:@"StringToSearchScopeTransformer"];
}
- (void)registerDefaults
{
// Set the home directory as the default search directory
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString * homeDir = @"~";
homeDir = [homeDir stringByExpandingTildeInPath];
homeDir = [[NSURL fileURLWithPath:homeDir isDirectory:YES] absoluteString];
NSDictionary *searchDefault =
[NSDictionary dictionaryWithObject:homeDir
forKey:@"spotlightSearchPath"];
[defaults registerDefaults:searchDefault];
}
- (id)init
{
if (self = [super initWithWindowNibName:@"SpotlightPanel"]) {
self.query = [[NSMetadataQuery alloc]init];
[self.query setDelegate:self];
self.query.sortDescriptors = [NSArray arrayWithObjects:
[[NSSortDescriptor alloc]initWithKey:@"kMDItemAuthors"
ascending:YES
selector:@selector(compareFirstString:)],
[[NSSortDescriptor alloc]initWithKey:@"kMDItemAlbum"
ascending:YES
selector:@selector(caseInsensitiveCompare:)],
[[NSSortDescriptor alloc]initWithKey:@"kMDItemAudioTrackNumber"
ascending:YES
selector:@selector(compareTrackNumbers:)],
nil];
// hook my query transformer up to me
[PausingQueryTransformer setSearchController:self];
[self registerDefaults];
// TODO: spotlightSearchPath is bound via IB, is the below needed?
// NSDictionary *bindOptions =
// [NSDictionary dictionaryWithObject:@"StringToSearchScopeTransformer"
// forKey:NSValueTransformerNameBindingOption];
//
// [self.query bind:@"searchScopes"
// toObject:[NSUserDefaultsController sharedUserDefaultsController]
// withKeyPath:@"values.spotlightSearchPath"
// options:bindOptions];
}
return self;
}
- (void)awakeFromNib
{
}
- (IBAction)toggleWindow:(id)sender
{
if ([[self window] isVisible])
[[self window] orderOut:self];
else
[self showWindow:self];
}
- (void)performSearch
{
NSPredicate *searchPredicate;
// Process the search string into a compound predicate. If Nil is returned do nothing
if((searchPredicate = [self processSearchString]))
{
// spotlightPredicate, which is what will finally be used for the spotlight search
// is the union of the (potentially) compound searchPredicate and the static
// musicOnlyPredicate
NSPredicate *spotlightPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:
[NSArray arrayWithObjects: musicOnlyPredicate,
searchPredicate,
nil]];
// Only preform a new search if the predicate has changed or there is a new path
if(![self.query.predicate isEqual:spotlightPredicate]
|| ![self.query.searchScopes isEqualToArray:
[NSArray arrayWithObjects:pathControl.URL, nil]])
{
if([self.query isStarted])
[self.query stopQuery];
self.query.predicate = spotlightPredicate;
// Set scope to contents of pathControl
self.query.searchScopes = [NSArray arrayWithObjects:pathControl.URL, nil];
[self.query startQuery];
DLog(@"Started query: %@", [self.query.predicate description]);
}
}
}
- (NSPredicate *)processSearchString
{
NSMutableArray *subpredicates = [NSMutableArray arrayWithCapacity:10];
NSScanner *scanner = [NSScanner scannerWithString:self.searchString];
BOOL exactString;
NSString * scannedString;
NSMutableString * parsingString;
while (![scanner isAtEnd])
{
exactString = NO;
if ([scanner scanUpToString:@" " intoString:&scannedString])
{
if ([scannedString length] < MINIMUM_SEARCH_STRING_LENGTH)
continue;
// We use NSMutableString because this string will get abused a bit
// It potentially could be reading the entire search string
parsingString = [NSMutableString stringWithCapacity: [self.searchString length]];
[parsingString setString: scannedString];
if ([parsingString characterAtIndex:0] == '%')
{
if ([parsingString length] < (MINIMUM_SEARCH_STRING_LENGTH + 2))
continue;
if ([parsingString characterAtIndex:2] == '\"')
{
exactString = YES;
// If the string does not end in a quotation mark and we're not at the end,
// scan until we find one.
// Allows strings within quotation marks to include spaces
if ([parsingString characterAtIndex:([parsingString length] - 1)] != '\"' &&
![scanner isAtEnd])
{
NSString *restOfString;
[scanner scanUpToString:@"\"" intoString:&restOfString];
[parsingString appendFormat:@" %@", restOfString];
}
else if ([parsingString characterAtIndex:([parsingString length] - 1)] == '\"')
{
// pick off the quotation mark at the end
[parsingString deleteCharactersInRange:
NSMakeRange([parsingString length] - 1, 1)];
}
// eliminate beginning quotation mark
[parsingString deleteCharactersInRange: NSMakeRange(2, 1)];
}
// Search for artist
if([parsingString characterAtIndex:1] == 'a')
{
[subpredicates addObject:
[NSComparisonPredicate predicateForMdKey:@"kMDItemAuthors"
withString:[parsingString substringFromIndex:2]
exactString:exactString]];
}
// Search for album
if([parsingString characterAtIndex:1] == 'l')
{
[subpredicates addObject:
[NSComparisonPredicate predicateForMdKey:@"kMDItemAlbum"
withString:[parsingString substringFromIndex:2]
exactString:exactString]];
}
// Search for title
if([parsingString characterAtIndex:1] == 't')
{
[subpredicates addObject:
[NSComparisonPredicate predicateForMdKey:@"kMDItemTitle"
withString:[parsingString substringFromIndex:2]
exactString:exactString]];
}
// Search for genre
if([parsingString characterAtIndex:1] == 'g')
{
[subpredicates addObject:
[NSComparisonPredicate predicateForMdKey:@"kMDItemMusicalGenre"
withString:[parsingString substringFromIndex:2]
exactString:exactString]];
}
// Search for comment
if([parsingString characterAtIndex:1] == 'c')
{
[subpredicates addObject:
[NSComparisonPredicate predicateForMdKey:@"kMDItemComment"
withString:[parsingString substringFromIndex:2]
exactString:exactString]];
}
}
else
{
NSString * wildcardString = [NSString stringWithFormat:@"*%@*", parsingString];
NSPredicate * pred = [NSPredicate predicateWithFormat:@"(kMDItemTitle LIKE[cd] %@) OR (kMDItemAlbum LIKE[cd] %@) OR (kMDItemAuthors LIKE[cd] %@)",
wildcardString, wildcardString, wildcardString];
[subpredicates addObject: pred];
}
}
}
if ([subpredicates count] == 0)
return nil;
else if ([subpredicates count] == 1)
return [subpredicates objectAtIndex: 0];
// Create a compound predicate from subPredicates
return [NSCompoundPredicate andPredicateWithSubpredicates: subpredicates];
}
- (void)searchForArtist:(NSString *)artist
{
[self showWindow:self];
self.searchString = [NSString stringWithFormat:@"%%a\"%@\"", artist];
}
- (void)searchForAlbum:(NSString *)album
{
[self showWindow:self];
self.searchString = [NSString stringWithFormat:@"%%l\"%@\"", album];
}
- (void)dealloc
{
self.query = nil;
self.searchString = nil;
}
- (IBAction)addToPlaylist:(id)sender
{
NSArray *tracks;
[self.query disableUpdates];
tracks = playlistController.selectedObjects;
if ([tracks count] == 0)
tracks = playlistController.arrangedObjects;
[playlistLoader willInsertURLs:[tracks valueForKey:@"URL"] origin:URLOriginExternal];
[playlistLoader didInsertURLs:[playlistLoader addURLs:[tracks valueForKey:@"URL"] sort:NO] origin:URLOriginExternal];
[self.query enableUpdates];
}
// If pop-up styled NSPathControl is set to /a/b/c path, then selecting either 'a' or 'b'
// from its pop-up menu won't do anything by default (while we'd like it to select /a and
// /a/b respectively). So here we set url of NSPathControl to be that of clicked cell.
- (IBAction)pathComponentClicked:(id)sender
{
NSPathComponentCell *pcc = [sender clickedPathComponentCell];
DLog(@"%@", pcc);
[sender setURL:[pcc URL]];
}
#pragma mark NSMetadataQuery delegate methods
// replace the NSMetadataItem with a PlaylistEntry
- (id)metadataQuery:(NSMetadataQuery*)query
replacementObjectForResultObject:(NSMetadataItem*)result
{
return [SpotlightPlaylistEntry playlistEntryWithMetadataItem: result];
}
#pragma mark Getters and setters
@synthesize query;
@synthesize searchString;
- (void)setSearchString:(NSString *)aString
{
// Make sure the string is changed
if (![searchString isEqualToString:aString])
{
searchString = [aString copy];
[self performSearch];
}
}
@end