root / trunk / TagEditor.m

Revision 45, 40.9 kB (checked in by stephen_booth, 2 years ago)

Use NSMultipleValuesMarker

  • Property svn:keywords set to Id
Line 
1/*
2 *  $Id$
3 *
4 *  Copyright (C) 2005, 2006 Stephen F. Booth <me@sbooth.org>
5 *
6 *  This program is free software; you can redistribute it and/or modify
7 *  it under the terms of the GNU General Public License as published by
8 *  the Free Software Foundation; either version 2 of the License, or
9 *  (at your option) any later version.
10 *
11 *  This program is distributed in the hope that it will be useful,
12 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 *  GNU General Public License for more details.
15 *
16 *  You should have received a copy of the GNU General Public License
17 *  along with this program; if not, write to the Free Software
18 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19 */
20
21#import "TagEditor.h"
22#import "KeyValueTaggedFile.h"
23#import "Genres.h"
24#import "AddTagSheet.h"
25#import "GuessTagsSheet.h"
26#import "RenameFilesSheet.h"
27#import "FileSelectionSheet.h"
28
29#import "UKKQueue.h"
30
31static TagEditor *sharedEditor = nil;
32
33@interface TagEditor (Private)
34- (BOOL)        addOneFile:(NSString *)filename atIndex:(unsigned)index;
35- (void)        alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
36- (void)        tagsChanged;
37- (void)        undoManagerNotification:(NSNotification *)aNotification;
38- (void)        openPanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo;
39@end
40
41@implementation TagEditor
42
43+ (void) initialize
44{
45        NSArray         *defaultValues;
46        NSArray         *defaultKeys;
47        NSArray         *predefinedPatterns;
48
49        predefinedPatterns      = [NSArray arrayWithObjects:
50                @"{artist} - {title}",
51                @"{artist}/{album}/{trackNumber} {title}",
52                @"Compilations/{album}/{discNumber}-{trackNumber} {title}",
53                nil];
54        defaultValues           = [NSArray arrayWithObjects:predefinedPatterns, predefinedPatterns, nil];
55        defaultKeys                     = [NSArray arrayWithObjects:@"guessTagsPatterns", @"renameFilesPatterns", nil];
56       
57        [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjects:defaultValues forKeys:defaultKeys]];
58}
59
60+ (TagEditor *) sharedEditor
61{
62        @synchronized(self) {
63                if(nil == sharedEditor) {
64                        sharedEditor = [[self alloc] init];
65                }
66        }
67        return sharedEditor;
68}
69
70+ (id) allocWithZone:(NSZone *)zone
71{
72    @synchronized(self) {
73        if(nil == sharedEditor) {
74            return [super allocWithZone:zone];
75        }
76    }
77    return sharedEditor;
78}
79
80- (id) init
81{
82        if((self = [super initWithWindowNibName:@"TagEditor"])) {
83
84                _validKeys      = [[NSArray arrayWithObjects:@"title", @"artist", @"album", @"year", @"genre", @"composer", @"MCN", @"ISRC", @"encoder", @"comment", @"trackNumber", @"trackTotal", @"discNumber", @"discTotal", @"compilation", @"custom", nil] retain];
85                _files          = [[NSMutableArray arrayWithCapacity:20] retain];
86               
87                [[UKKQueue sharedFileWatcher] setDelegate:self];
88
89                return self;
90        }
91        return nil;
92}
93
94- (void) dealloc
95{
96        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUndoManagerDidUndoChangeNotification object:[self undoManager]];
97        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUndoManagerDidRedoChangeNotification object:[self undoManager]];
98
99        [_validKeys release];
100        [_files release];
101       
102        [super dealloc];
103}
104
105- (id)                          copyWithZone:(NSZone *)zone                                     { return self; }
106- (id)                          retain                                                                          { return self; }
107- (unsigned)            retainCount                                                                     { return UINT_MAX;  /* denotes an object that cannot be released */ }
108- (void)                        release                                                                         { /* do nothing */ }
109- (id)                          autorelease                                                                     { return self; }
110
111- (NSArray *)           genres                                                                          { return [Genres sharedGenres]; }
112- (NSWindow *)          windowForSheet                                                          { return [self window]; }
113- (IBAction)            toggleFilesDrawer:(id)sender                            { [_filesDrawer toggle:sender]; }
114- (IBAction)            openFilesDrawer:(id)sender                                      { [_filesDrawer open:sender]; }
115- (IBAction)            closeFilesDrawer:(id)sender                                     { [_filesDrawer close:sender]; }
116- (unsigned)            countOfFiles                                                            { return [_files count]; }
117- (unsigned)            countOfSelectedFiles                                            { return [[_filesController selectedObjects] count]; }
118- (KeyValueTaggedFile *) objectInFilesAtIndex:(unsigned)idx             { return [_files objectAtIndex:idx]; }
119- (IBAction)            selectNextFile:(id)sender                                       { [_filesController selectNext:sender]; }
120- (IBAction)            selectPreviousFile:(id)sender                           { [_filesController selectPrevious:sender]; }
121- (IBAction)            selectAllFiles:(id)sender                                       { [_filesController setSelectionIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self countOfFiles])]]; }
122
123- (IBAction)            selectBasicTab:(id)sender                                       { [_tabView selectTabViewItemAtIndex:kBasicTabViewItemIndex]; }
124- (IBAction)            selectAdvancedTab:(id)sender                            { [_tabView selectTabViewItemAtIndex:kAdvancedTabViewItemIndex]; }
125- (IBAction)            selectTabularTab:(id)sender                                     { [_tabView selectTabViewItemAtIndex:kTabularTabViewItemIndex]; }
126
127- (NSUndoManager *) undoManager                                                                 { return [[self window] undoManager]; }
128
129- (void) undoManagerNotification:(NSNotification *)aNotification
130{
131        NSString        *name   = [aNotification name];
132       
133        if([name isEqualToString:NSUndoManagerDidUndoChangeNotification]) {
134                [self tagsChanged];
135        }
136        else if([name isEqualToString:NSUndoManagerDidRedoChangeNotification]) {
137                [self tagsChanged];
138        }
139}
140
141- (BOOL) applicationShouldTerminate
142{
143        NSEnumerator                    *enumerator;
144        KeyValueTaggedFile              *current;
145        NSAlert                                 *alert;
146        int                                             result;
147
148        if(0 == [self countOfFiles] || NO == [self dirty]) {
149                return YES;
150        }
151        else {
152                enumerator = [[_filesController arrangedObjects] objectEnumerator];
153                while((current = [enumerator nextObject])) {
154                        if(YES == [current dirty]) {
155                                alert = [[[NSAlert alloc] init] autorelease];
156                                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
157                                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"Cancel", @"General", @"")];
158                                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"Don't Save", @"General", @"")];
159                                [alert setMessageText:[NSString stringWithFormat:NSLocalizedStringFromTable(@"Do you want to save the changes you made in the document \"%@\"?", @"General", @""), [current displayName]]];
160                                [alert setInformativeText:NSLocalizedStringFromTable(@"Your changes will be lost if you don't save them.", @"General", @"")];
161                                [alert setAlertStyle:NSInformationalAlertStyle];
162                               
163                                result = [alert runModal];
164                                switch(result) {
165                                        case NSAlertFirstButtonReturn:          [current save];                         break;
166                                        case NSAlertSecondButtonReturn:         return NO;                                      break;
167                                        case NSAlertThirdButtonReturn:          ;                                                       break;
168                                }
169                        }
170                }
171               
172                return YES;
173        }
174}
175
176- (void) awakeFromNib
177{
178        [_tagsTable setAutosaveTableColumns:YES];
179        [_tabularTagsTable setAutosaveTableColumns:YES];
180       
181        [_sortFilesPopUpButton selectItemWithTag:kSortByFilenameMenuItemTag];
182
183        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(undoManagerNotification:) name:NSUndoManagerDidUndoChangeNotification object:[self undoManager]];
184        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(undoManagerNotification:) name:NSUndoManagerDidRedoChangeNotification object:[self undoManager]];
185
186        [_tagsController setSortDescriptors:[NSArray arrayWithObjects:
187                [[[NSSortDescriptor alloc] initWithKey:@"key" ascending:YES] autorelease],
188                [[[NSSortDescriptor alloc] initWithKey:@"value" ascending:YES] autorelease],
189                nil]];
190        [_filesController setSortDescriptors:[NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:@"filename" ascending:YES] autorelease]]];
191        [_selectedFilesController setSortDescriptors:[NSArray arrayWithObjects:
192                [[[NSSortDescriptor alloc] initWithKey:@"artist" ascending:YES] autorelease],
193                [[[NSSortDescriptor alloc] initWithKey:@"album" ascending:YES] autorelease],
194                [[[NSSortDescriptor alloc] initWithKey:@"trackNumber" ascending:YES] autorelease],
195                nil]];
196}
197
198- (void) windowDidLoad
199{
200        [self setShouldCascadeWindows:NO];
201        [self setWindowFrameAutosaveName:@"Editor"];
202        [[self window] setExcludedFromWindowsMenu:YES];
203}
204
205- (BOOL) dirty
206{
207        NSEnumerator                    *enumerator;
208        KeyValueTaggedFile              *current;
209
210        enumerator      = [[_filesController arrangedObjects] objectEnumerator];       
211        while((current = [enumerator nextObject])) {
212                if([current dirty]) {
213                        return YES;
214                }
215        }
216       
217        return NO;
218}
219
220- (BOOL) selectionDirty
221{
222        NSEnumerator                    *enumerator;
223        KeyValueTaggedFile              *current;
224       
225        enumerator      = [[_filesController selectedObjects] objectEnumerator];       
226        while((current = [enumerator nextObject])) {
227                if([current dirty]) {
228                        return YES;
229                }
230        }
231       
232        return NO;
233}
234
235- (void) openFilesDrawerIfNeeded
236{
237        if(1 < [self countOfFiles]) {
238                [_filesController rearrangeObjects];
239                [self openFilesDrawer:self];
240        }
241}
242
243#pragma mark File Manipulation
244
245- (IBAction) sortFiles:(id)sender
246{
247        NSArray         *sortDescriptors;
248        NSString        *sortKey;
249        BOOL            ascending;
250       
251        sortDescriptors         = [_filesController sortDescriptors];
252        sortKey                         = (0 < [sortDescriptors count] ? [[sortDescriptors objectAtIndex:0] key] : @"filename");
253        ascending                       = (0 < [sortDescriptors count] ? [[sortDescriptors objectAtIndex:0] ascending] : YES);
254
255        if([sender isKindOfClass:[NSPopUpButton class]]) {
256                switch([[(NSPopUpButton *)sender selectedItem] tag]) {
257                        case kSortByFilenameMenuItemTag:                        sortKey = @"filename";                  break;
258                        case kSortByTitleMenuItemTag:                           sortKey = @"title";                             break;
259                        case kSortByArtistMenuItemTag:                          sortKey = @"artist";                    break;
260                        case kSortByAlbumMenuItemTag:                           sortKey = @"album";                             break;
261                        case kSortByYearMenuItemTag:                            sortKey = @"year";                              break;
262                        case kSortByGenreMenuItemTag:                           sortKey = @"genre";                             break;
263                        case kSortByComposerMenuItemTag:                        sortKey = @"composer";                  break;
264                        case kSortByTrackNumberMenuItemTag:                     sortKey = @"trackNumber";               break;
265                        case kSortByDiscNumberMenuItemTag:                      sortKey = @"discNumber";                break;
266                        default:                                                                        sortKey = @"filename";                  break;
267                }
268        }       
269        else if([sender isKindOfClass:[NSButton class]]) {
270                ascending = (NSOnState == [(NSButton *)sender state]);
271        }
272       
273        [_filesController setSortDescriptors:[NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:sortKey ascending:ascending] autorelease]]];
274}
275
276- (IBAction) openDocument:(id)sender
277{
278        BOOL                            success                         = YES;
279        NSOpenPanel                     *panel                          = [NSOpenPanel openPanel];
280        NSArray                         *allowedTypes           = [NSArray arrayWithObjects:@"flac", @"ogg", @"ape", @"wv", nil];
281        NSEnumerator            *enumerator;
282        NSString                        *filename;
283        NSMutableArray          *newFiles;
284        KeyValueTaggedFile      *file;
285        int                                     returnCode;
286       
287        [panel setAllowsMultipleSelection:YES];
288        [panel setCanChooseDirectories:YES];
289       
290        returnCode = [panel runModalForTypes:allowedTypes];
291       
292        if(NSOKButton == returnCode) {         
293
294                newFiles        = [NSMutableArray arrayWithCapacity:10];
295                enumerator      = [[panel filenames] objectEnumerator];
296               
297                while((filename = [enumerator nextObject])) {
298                        success &= [self addFile:filename];
299
300                        if(success) {
301                                file = [_filesController findFile:filename];
302                                if(nil != file) {
303                                        [newFiles addObject:file];
304                                }
305                        }
306                }
307
308                if(1 < [[panel filenames] count] && success) {
309                        [_filesController setSelectedObjects:newFiles];
310                }
311
312                [self openFilesDrawerIfNeeded];
313        }
314}
315
316- (IBAction) performClose:(id)sender
317{
318        NSEnumerator                    *enumerator;
319        KeyValueTaggedFile              *current;
320        NSString                                *key;
321       
322        [self willChangeValueForKey:@"tags"];
323        enumerator = [[_filesController selectedObjects] objectEnumerator];
324        while((current = [enumerator nextObject])) {
325                if(YES == [current dirty]) {
326                        NSAlert         *alert;
327                        int                     result;
328                       
329                        alert = [[[NSAlert alloc] init] autorelease];
330                        [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
331                        [alert addButtonWithTitle:NSLocalizedStringFromTable(@"Cancel", @"General", @"")];
332                        [alert addButtonWithTitle:NSLocalizedStringFromTable(@"Don't Save", @"General", @"")];
333                        [alert setMessageText:[NSString stringWithFormat:NSLocalizedStringFromTable(@"Do you want to save the changes you made in the document \"%@\"?", @"General", @""), [current displayName]]];
334                        [alert setInformativeText:NSLocalizedStringFromTable(@"Your changes will be lost if you don't save them.", @"General", @"")];
335                        [alert setAlertStyle:NSInformationalAlertStyle];
336                       
337                        result = [alert runModal];
338                        switch(result) {
339                                case NSAlertFirstButtonReturn:          [current save];                         break;
340                                case NSAlertSecondButtonReturn:         continue;                                       break;
341                                case NSAlertThirdButtonReturn:          ;                                                       break;
342                        }
343                }
344               
345                [_filesController removeObject:current];
346               
347                [[UKKQueue sharedFileWatcher] removePath:[current filename]];
348        }
349        [self didChangeValueForKey:@"tags"];
350       
351        enumerator = [_validKeys objectEnumerator];
352        while((key = [enumerator nextObject])) {
353                [self willChangeValueForKey:key];
354                [self didChangeValueForKey:key];
355        }
356
357        if(1 >= [self countOfFiles]) {
358                [self closeFilesDrawer:self];
359        }
360}
361
362- (IBAction) saveDocument:(id)sender
363{
364        NSEnumerator                    *enumerator;
365        KeyValueTaggedFile              *current;
366       
367        enumerator = [[_filesController selectedObjects] objectEnumerator];
368        while((current = [enumerator nextObject])) {
369                if([current dirty]) {
370                        @try {
371                                [[UKKQueue sharedFileWatcher] removePath:[current filename]];
372                                [current save];
373                                [[UKKQueue sharedFileWatcher] addPath:[current filename]];
374                        }
375                        @catch(NSException *exception) {
376                                NSAlert         *alert;
377                               
378                                alert = [[[NSAlert alloc] init] autorelease];
379                                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
380                                [alert setMessageText:[NSString stringWithFormat:NSLocalizedStringFromTable(@"An error occurred while saving the document \"%@\".", @"Errors", @""), [current displayName]]];
381                                [alert setInformativeText:[exception reason]];
382                                [alert setAlertStyle:NSInformationalAlertStyle];
383                                [alert runModal];
384                        }
385                }
386        }
387}
388
389- (IBAction) revertDocumentToSaved:(id)sender
390{
391        NSEnumerator                    *enumerator;
392        KeyValueTaggedFile              *current;
393        NSString                                *key;
394       
395        [self willChangeValueForKey:@"tags"];
396        enumerator = [[_filesController selectedObjects] objectEnumerator];
397        while((current = [enumerator nextObject])) {
398                if(YES == [current dirty]) {
399                        NSAlert         *alert;
400                        int                     result;
401                       
402                        alert = [[[NSAlert alloc] init] autorelease];
403                        [alert addButtonWithTitle:NSLocalizedStringFromTable(@"Revert", @"General", @"")];
404                        [alert addButtonWithTitle:NSLocalizedStringFromTable(@"Cancel", @"General", @"")];
405                        [alert setMessageText:[NSString stringWithFormat:NSLocalizedStringFromTable(@"Do you want to revert to the most recently saved revision of the document \"%@\"?", @"General", @""), [current displayName]]];
406                        [alert setInformativeText:NSLocalizedStringFromTable(@"Your changes will be lost if you don't save them.", @"General", @"")];
407                        [alert setAlertStyle:NSInformationalAlertStyle];
408                       
409                        result = [alert runModal];
410                        switch(result) {
411                                case NSAlertFirstButtonReturn:
412                                        @try {
413                                                [[UKKQueue sharedFileWatcher] removePath:[current filename]];
414                                                [current revert];
415                                                [[UKKQueue sharedFileWatcher] addPath:[current filename]];
416                                        }
417                                        @catch(NSException *exception) {
418                                                alert = [[[NSAlert alloc] init] autorelease];
419                                                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
420                                                [alert setMessageText:[NSString stringWithFormat:NSLocalizedStringFromTable(@"An error occurred while reverting the document \"%@\".", @"Errors", @""), [current displayName]]];
421                                                [alert setInformativeText:[exception reason]];
422                                                [alert setAlertStyle:NSInformationalAlertStyle];
423                                                [alert runModal];
424                                        }
425                                        break;
426                                       
427                                case NSAlertSecondButtonReturn:         continue;                                       break;
428                        }
429                }
430        }
431        [self didChangeValueForKey:@"tags"];
432       
433        enumerator = [_validKeys objectEnumerator];
434        while((key = [enumerator nextObject])) {
435                [self willChangeValueForKey:key];
436                [self didChangeValueForKey:key];
437        }
438}
439
440- (BOOL) addFile:(NSString *)filename
441{
442        return [self addFile:filename atIndex:/*[[_filesController arrangedObjects] count]*/NSNotFound];
443}
444
445- (BOOL) addFile:(NSString *)filename atIndex:(unsigned)index
446{
447        NSFileManager           *manager                        = [NSFileManager defaultManager];
448        NSArray                         *allowedTypes           = [NSArray arrayWithObjects:@"flac", @"ogg", @"ape", @"wv", nil];
449        NSMutableArray          *newFiles;
450        KeyValueTaggedFile      *file;
451        NSArray                         *subpaths;
452        BOOL                            isDir;
453        NSEnumerator            *enumerator;
454        NSString                        *subpath;
455        NSString                        *composedPath;
456        BOOL                            success                         = YES;
457
458        if([manager fileExistsAtPath:filename isDirectory:&isDir]) {
459                newFiles = [NSMutableArray arrayWithCapacity:10];
460
461                if(isDir) {
462                        subpaths        = [manager subpathsAtPath:filename];
463                        enumerator      = [subpaths objectEnumerator];
464                       
465                        while((subpath = [enumerator nextObject])) {
466                                composedPath = [NSString stringWithFormat:@"%@/%@", filename, subpath];
467                               
468                                // Ignore dotfiles
469                                if([[subpath lastPathComponent] hasPrefix:@"."]) {
470                                        continue;
471                                }
472                                // Ignore files that don't have our extensions
473                                else if(NO == [allowedTypes containsObject:[subpath pathExtension]]) {
474                                        continue;
475                                }
476                               
477                                // Ignore directories
478                                if([manager fileExistsAtPath:composedPath isDirectory:&isDir] && NO == isDir) {
479                                        success &= [self addOneFile:composedPath atIndex:(unsigned)index];
480                                }
481
482                                if(success) {
483                                        file = [_filesController findFile:composedPath];
484                                        if(nil != file) {
485                                                [newFiles addObject:file];
486                                        }
487                                }
488                        }
489                       
490                        if(success) {
491                                [_filesController setSelectedObjects:newFiles];
492                        }
493                }
494                else {
495                        success &= [self addOneFile:filename atIndex:(unsigned)index];
496                        if(success) {
497                                [_filesController selectFile:filename];
498                        }
499                }
500        }
501        else {
502                NSAlert         *alert;
503               
504                alert = [[[NSAlert alloc] init] autorelease];
505                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
506                [alert setMessageText:[NSString stringWithFormat:NSLocalizedStringFromTable(@"An error occurred while opening the document \"%@\".", @"Errors", @""), [filename lastPathComponent]]];
507                [alert setInformativeText:NSLocalizedStringFromTable(@"The file was not found.", @"Errors", @"")];
508                [alert setAlertStyle:NSInformationalAlertStyle];
509               
510                [alert runModal];
511                success = NO;
512        }
513       
514        return success;
515}
516
517- (BOOL) addOneFile:(NSString *)filename atIndex:(unsigned)index
518{
519        BOOL                    success                 = YES;
520
521        @try {
522                if([_filesController containsFile:filename]) {
523                        return YES;
524                }
525               
526                if(NSNotFound == index) {
527                        [_filesController addObject:[KeyValueTaggedFile parseFile:filename]];
528                }
529                else {
530                        [_filesController insertObject:[KeyValueTaggedFile parseFile:filename] atArrangedObjectIndex:index];                   
531                }
532                [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:filename]];
533
534                [[UKKQueue sharedFileWatcher] addPath:filename];
535        }
536       
537        @catch(NSException *exception) {
538                NSAlert         *alert;
539               
540                alert = [[[NSAlert alloc] init] autorelease];
541                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
542                [alert setMessageText:[NSString stringWithFormat:NSLocalizedStringFromTable(@"An error occurred while opening the document \"%@\".", @"Errors", @""), [filename lastPathComponent]]];
543                [alert setInformativeText:[exception reason]];
544                [alert setAlertStyle:NSInformationalAlertStyle];
545               
546                [alert runModal];
547                success = NO;
548        }
549
550        return success;
551}
552
553#pragma mark Tag Manipulation
554
555- (void) tagsChanged
556{
557        [self willChangeValueForKey:@"tags"];
558        [self didChangeValueForKey:@"tags"];
559}
560
561- (IBAction) newTag:(id)sender
562{
563        AddTagSheet *sheet;
564       
565        @try {
566                sheet = [[AddTagSheet alloc] init];
567                [sheet setDelegate:self];
568                [sheet showSheet];
569               
570                // TODO: How do I avoid a memory leak here?  For some reason sheet is being autoreleased while it is being displayed
571                //[sheet autorelease];
572        }
573
574        @catch(NSException *exception) {
575                NSAlert         *alert;
576               
577                alert = [[[NSAlert alloc] init] autorelease];
578                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
579                [alert setMessageText:NSLocalizedStringFromTable(@"Your Tag installation appears to be incomplete.", @"Errors", @"")];
580                [alert setInformativeText:[exception reason]];
581                [alert setAlertStyle:NSInformationalAlertStyle];
582               
583                [alert runModal];
584        }
585}
586
587- (IBAction) addTagsFromFile:(id)sender
588{
589        NSOpenPanel                     *panel                          = [NSOpenPanel openPanel];
590        NSArray                         *allowedTypes           = [NSArray arrayWithObjects:@"flac", @"ogg", @"ape", @"wv", nil];
591       
592        [panel setAllowsMultipleSelection:YES];
593        [panel setCanChooseDirectories:YES];
594       
595        [panel beginSheetForDirectory:nil file:nil types:allowedTypes modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo:NULL];
596}
597
598- (void) openPanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo
599{
600        NSEnumerator            *enumerator;
601        NSString                        *filename;
602        KeyValueTaggedFile      *file;
603        unsigned                        i;
604        NSDictionary            *tag;
605
606        if(NSOKButton == returnCode) {         
607               
608                enumerator      = [[panel filenames] objectEnumerator];
609               
610                while((filename = [enumerator nextObject])) {
611                        file = [KeyValueTaggedFile parseFile:filename];
612                       
613                        if(nil != file) {
614                                for(i = 0; i < [file countOfTags]; ++i) {
615                                        tag = [file objectInTagsAtIndex:i];
616                                        [self addValue:[tag objectForKey:@"value"] forTag:[tag objectForKey:@"key"]];
617                                }
618                        }
619                }               
620        }
621}
622
623- (IBAction) renameFiles:(id)sender
624{
625        RenameFilesSheet *sheet;
626       
627        @try {
628                sheet = [[RenameFilesSheet alloc] init];
629                [sheet setDelegate:self];
630                [sheet showSheet];
631               
632                // TODO: How do I avoid a memory leak here?  For some reason sheet is being autoreleased while it is being displayed
633                //[sheet autorelease];
634        }
635       
636        @catch(NSException *exception) {
637                NSAlert         *alert;
638               
639                alert = [[[NSAlert alloc] init] autorelease];
640                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
641                [alert setMessageText:NSLocalizedStringFromTable(@"Your Tag installation appears to be incomplete.", @"Errors", @"")];
642                [alert setInformativeText:[exception reason]];
643                [alert setAlertStyle:NSInformationalAlertStyle];
644               
645                [alert runModal];
646        }}
647
648- (IBAction) delete:(id)sender                  { [self deleteTag:sender]; }
649
650- (IBAction) deleteTag:(id)sender
651{
652        NSEnumerator                    *enumerator;
653        NSArray                                 *selectedTags;
654        unsigned                                i;
655        KeyValueTaggedFile              *current;
656        NSDictionary                    *tag;
657        NSUndoManager                   *undoManager                                    = [self undoManager];
658       
659        [self willChangeValueForKey:@"tags"];
660        [undoManager beginUndoGrouping];
661        selectedTags    = [_tagsController selectedObjects];
662        enumerator              = [[_filesController selectedObjects] objectEnumerator];
663        while((current = [enumerator nextObject])) {
664                for(i = 0; i < [selectedTags count]; ++i) {
665                        tag = [selectedTags objectAtIndex:i];
666                        [current updateTag:[tag valueForKey:@"key"] withValue:[tag valueForKey:@"value"] toValue:nil];
667                }
668        }       
669        [undoManager endUndoGrouping];
670        [self didChangeValueForKey:@"tags"];
671}
672
673- (IBAction) guessTags:(id)sender
674{
675        GuessTagsSheet *sheet;
676       
677        @try {
678                sheet = [[GuessTagsSheet alloc] init];
679                [sheet setDelegate:self];
680                [sheet showSheet];
681               
682                // TODO: How do I avoid a memory leak here?  For some reason sheet is being autoreleased while it is being displayed
683                //[sheet autorelease];
684        }
685       
686        @catch(NSException *exception) {
687                NSAlert         *alert;
688               
689                alert = [[[NSAlert alloc] init] autorelease];
690                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
691                [alert setMessageText:NSLocalizedStringFromTable(@"Your Tag installation appears to be incomplete.", @"Errors", @"")];
692                [alert setInformativeText:[exception reason]];
693                [alert setAlertStyle:NSInformationalAlertStyle];
694               
695                [alert runModal];
696        }
697}
698
699- (IBAction) copySelectedTags:(id)sender
700{
701        FileSelectionSheet              *sheet                          = nil;
702        NSMutableArray                  *files                          = nil;
703        NSArray                                 *selectedFiles          = nil;
704        KeyValueTaggedFile              *file                           = nil;
705        unsigned                                i;
706       
707        @try {
708               
709                files                   = [NSMutableArray array];
710                selectedFiles   = [_filesController selectedObjects];
711                for(i = 0; i < [self countOfFiles]; ++i) {
712                        file = [self objectInFilesAtIndex:i];
713                        if(NO == [selectedFiles containsObject:file]) {
714                                [files addObject:file];
715                        }
716                }
717               
718                sheet = [[FileSelectionSheet alloc] init];
719                [sheet setDelegate:self];
720                [sheet setValue:files forKey:@"files"];
721                [sheet showSheet];
722               
723                // TODO: How do I avoid a memory leak here?  For some reason sheet is being autoreleased while it is being displayed
724                //[sheet autorelease];
725        }
726       
727        @catch(NSException *exception) {
728                NSAlert         *alert;
729               
730                alert = [[[NSAlert alloc] init] autorelease];
731                [alert addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"General", @"")];
732                [alert setMessageText:NSLocalizedStringFromTable(@"Your Tag installation appears to be incomplete.", @"Errors", @"")];
733                [alert setInformativeText:[exception reason]];
734                [alert setAlertStyle:NSInformationalAlertStyle];
735               
736                [alert runModal];
737        }
738}
739
740- (void) setValue:(NSString *)value forTag:(NSString *)tag
741{
742        NSEnumerator                    *enumerator;
743        KeyValueTaggedFile              *current;
744        NSUndoManager                   *undoManager            = [self undoManager];
745       
746        [self willChangeValueForKey:@"tags"];
747        [undoManager beginUndoGrouping];
748        enumerator = [[_filesController selectedObjects] objectEnumerator];
749        while((current = [enumerator nextObject])) {
750                [current setValue:value forTag:tag];
751        }
752        [undoManager endUndoGrouping];
753        [self didChangeValueForKey:@"tags"];
754}
755
756- (void) addValue:(NSString *)value forTag:(NSString *)tag
757{
758        NSEnumerator                    *enumerator;
759        KeyValueTaggedFile              *current;
760        NSUndoManager                   *undoManager            = [self undoManager];
761       
762        [self willChangeValueForKey:@"tags"];
763        [undoManager beginUndoGrouping];
764        enumerator = [[_filesController selectedObjects] objectEnumerator];
765        while((current = [enumerator nextObject])) {
766                [current addValue:value forTag:tag];
767        }
768        [undoManager endUndoGrouping];
769        [self didChangeValueForKey:@"tags"];
770}
771
772- (void) updateTag:(NSString *)tag withValue:(NSString *)currentValue toValue:(NSString *)newValue
773{
774        NSEnumerator                    *enumerator;
775        KeyValueTaggedFile              *current;
776        NSUndoManager                   *undoManager            = [self undoManager];
777       
778        [self willChangeValueForKey:@"tags"];
779        [undoManager beginUndoGrouping];
780        enumerator = [[_filesController selectedObjects] objectEnumerator];
781        while((current = [enumerator nextObject])) {
782                [current updateTag:tag withValue:currentValue toValue:newValue];
783        }
784        [undoManager endUndoGrouping];
785        [self didChangeValueForKey:@"tags"];
786}
787
788- (void) renameTag:(NSString *)currentTag withValue:(NSString *)currentValue toTag:(NSString *)newTag
789{
790        NSEnumerator                    *enumerator;
791        KeyValueTaggedFile              *current;
792        NSUndoManager                   *undoManager            = [self undoManager];
793       
794        [self willChangeValueForKey:@"tags"];
795        [undoManager beginUndoGrouping];
796        enumerator = [[_filesController selectedObjects] objectEnumerator];
797        while((current = [enumerator nextObject])) {
798                [current renameTag:currentTag withValue:currentValue toTag:newTag];
799        }
800        [undoManager endUndoGrouping];
801        [self didChangeValueForKey:@"tags"];
802}
803
804- (void) guessTagsUsingPattern:(NSString *)pattern
805{
806        NSEnumerator                    *enumerator;
807        KeyValueTaggedFile              *current;
808        NSUndoManager                   *undoManager            = [self undoManager];
809       
810        [self willChangeValueForKey:@"tags"];
811        [undoManager beginUndoGrouping];
812        enumerator = [[_filesController selectedObjects] objectEnumerator];
813        while((current = [enumerator nextObject])) {
814                [current guessTagsUsingPattern:pattern];
815        }
816        [undoManager endUndoGrouping];
817        [self didChangeValueForKey:@"tags"];
818}
819
820- (void) renameFilesUsingPattern:(NSString *)pattern
821{
822        NSEnumerator                    *enumerator;
823        KeyValueTaggedFile              *current;
824        NSString                                *key;
825
826        [self willChangeValueForKey:@"tags"];
827        enumerator = [[_filesController selectedObjects] objectEnumerator];
828        while((current = [enumerator nextObject])) {
829               
830                // Remove the file from our list of open files
831                [_filesController removeObject:current];
832                [[UKKQueue sharedFileWatcher] removePath:[current filename]];
833
834                // Rename the file
835                [current renameFileUsingPattern:pattern];
836               
837                // Add it back to our list of open files
838                [_filesController addObject:current];
839                [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:[current filename]]];
840                [[UKKQueue sharedFileWatcher] addPath:[current filename]];
841        }
842        [self didChangeValueForKey:@"tags"];
843
844        enumerator = [_validKeys objectEnumerator];
845        while((key = [enumerator nextObject])) {
846                [self willChangeValueForKey:key];
847                [self didChangeValueForKey:key];
848        }
849}
850
851- (void) copySelectedTagsToFiles:(NSArray *)files
852{       
853        NSArray                                 *selectedTags           = [_tagsController selectedObjects];
854        NSUndoManager                   *undoManager            = [self undoManager];
855        KeyValueTaggedFile              *file                           = nil;
856        NSDictionary                    *tag                            = nil;
857        unsigned                                i, j;
858       
859        [undoManager beginUndoGrouping];
860        for(i = 0; i < [files count]; ++i) {
861                file = [files objectAtIndex:i];
862                if([_files containsObject:file]) {
863                        for(j = 0; j < [selectedTags count]; ++j) {
864                                tag = [selectedTags objectAtIndex:j];
865                                [file addValue:[tag objectForKey:@"value"] forTag:[tag objectForKey:@"key"]];
866                        }                       
867                }
868        }
869        [undoManager endUndoGrouping];
870}
871
872- (void) cutSelectedTagsToPasteboard
873{
874        [self copySelectedTagsToPasteboard];
875        [self deleteTag:self];
876}
877
878- (void) copySelectedTagsToPasteboard
879{
880        NSPasteboard            *pboard                 = [NSPasteboard generalPasteboard];
881       
882        [pboard declareTypes:[NSArray arrayWithObject:@"org.sbooth.Tag.TagItem"] owner:self];
883        [pboard setPropertyList:[_tagsController selectedObjects] forType:@"org.sbooth.Tag.TagItem"];
884}
885
886- (void) pasteTagsFromPasteboard
887{
888        NSUndoManager           *undoManager    = [self undoManager];
889        NSPasteboard            *pboard                 = [NSPasteboard generalPasteboard];
890        NSArray                         *tags                   = [pboard propertyListForType:@"org.sbooth.Tag.TagItem"];
891        NSDictionary            *tag                    = nil;
892        unsigned                        i                               = 0;
893                               
894        [undoManager beginUndoGrouping];
895        for(i = 0; i < [tags count]; ++i) {
896                tag = [tags objectAtIndex:i];
897                [self addValue:[tag objectForKey:@"value"] forTag:[tag objectForKey:@"key"]];
898        }       
899        [undoManager endUndoGrouping];
900}
901
902- (void) tableViewSelectionDidChange:(NSNotification *)aNotification
903{
904        NSEnumerator    *enumerator;
905        NSString                *key;
906        NSArray                 *selectedObjects;
907       
908        // Update window title with filename if only one file is being edited
909        selectedObjects = [_filesController selectedObjects];
910        [[self window] setTitle:(1 == [selectedObjects count] ? [[selectedObjects objectAtIndex:0] displayName] : @"Tag")];
911       
912        [self tagsChanged];
913
914        enumerator = [_validKeys objectEnumerator];
915        while((key = [enumerator nextObject])) {
916                [self willChangeValueForKey:key];
917                [self didChangeValueForKey:key];
918        }
919}
920
921- (id) valueForKey:(NSString *)key
922{
923        NSEnumerator                    *enumerator;
924        KeyValueTaggedFile              *current;
925       
926        if([_validKeys containsObject:key]) {
927                NSArray                                 *reverseMappedKeys;
928                NSString                                *currentValue;
929                NSString                                *lastValue;
930                NSColor                                 *markerColor;
931                BOOL                                    firstTime;
932               
933                enumerator              = [[_filesController selectedObjects] objectEnumerator];
934                currentValue    = nil;
935                lastValue               = nil;
936                firstTime               = YES;
937               
938                while((current = [enumerator nextObject])) {
939                        reverseMappedKeys = [[current tagMapping] allKeysForObject:key];
940                       
941                        if(0 < [reverseMappedKeys count]) {
942                                currentValue = [current valueForTag:[reverseMappedKeys objectAtIndex:0]];
943                               
944                                if(NO == firstTime && ((nil != currentValue || nil != lastValue) && NO == [currentValue isEqualToString:lastValue])) {
945                                                                               
946                                        // Special case for non-text field
947                                        if([key isEqualToString:@"compilation"]) {
948                                                return [NSNumber numberWithInt:NSMixedState];
949                                        }
950
951                                        return NSMultipleValuesMarker;
952                                }
953                               
954                                lastValue = currentValue;
955                                firstTime = NO;
956                        }
957                }
958               
959                return lastValue;
960        }
961        else if([key isEqualToString:@"tags"]) {
962                NSEnumerator                    *tagEnumerator;
963                NSDictionary                    *currentTag;
964                NSArray                                 *currentValue;
965                NSArray                                 *lastValue;
966                NSMutableArray                  *result;
967                BOOL                                    firstTime;
968
969                enumerator              = [[_filesController selectedObjects] objectEnumerator];
970                currentValue    = nil;
971                lastValue               = nil;
972                result                  = nil;
973                firstTime               = YES;
974               
975                while((current = [enumerator nextObject])) {
976                        currentValue = [current valueForKey:@"tags"];
977                       
978                        if(firstTime) {
979                                // Make a deep copy so the actual file's tags are not modified
980                                result                  = [[NSMutableArray alloc] initWithCapacity:[currentValue count]];
981                                tagEnumerator   = [currentValue objectEnumerator];
982                                while((currentTag = [tagEnumerator nextObject])) {
983                                        [result addObject:[[currentTag mutableCopy] autorelease]];
984                                }
985                        }
986                       
987                        if(NO == firstTime && NO == [currentValue isEqual:lastValue]) {
988                                // Winnow the result to contain only tags that match in every file
989                                tagEnumerator = [result objectEnumerator];
990                               
991                                while((currentTag = [tagEnumerator nextObject])) {
992                                        if(NO == [currentValue containsObject:currentTag]) {
993                                                [result removeObject:currentTag];
994                                        }
995                                }
996                        }
997                       
998                        lastValue = currentValue;
999                        firstTime = NO;
1000                }
1001               
1002                return [result autorelease];
1003                       
1004        }
1005        else {
1006                return [super valueForKey:key];
1007        }       
1008}
1009
1010- (void) setValue:(id)value forKey:(NSString *)key
1011{
1012        NSEnumerator                    *enumerator;
1013        KeyValueTaggedFile              *current;
1014        NSArray                                 *reverseMappedKeys;
1015        NSUndoManager                   *undoManager            = [self undoManager];
1016        NSString                                *stringValue;
1017
1018        if([_validKeys containsObject:key]) {
1019                enumerator = [[_filesController selectedObjects] objectEnumerator];
1020                [self willChangeValueForKey:@"tags"];
1021                [undoManager beginUndoGrouping];
1022                while((current = [enumerator nextObject])) {
1023                       
1024                        reverseMappedKeys = [[current tagMapping] allKeysForObject:key];
1025
1026                        if(0 < [reverseMappedKeys count]) {                             
1027                                stringValue = ([value isKindOfClass:[NSString class]] ? (NSString *)value : ([value respondsToSelector:@selector(stringValue)] ? [value stringValue] : nil));
1028                                [current setValue:stringValue forTag:[reverseMappedKeys objectAtIndex:0]];
1029                        }
1030                }
1031                [undoManager endUndoGrouping];
1032                [self didChangeValueForKey:@"tags"];
1033
1034                if(NO == [key isEqualToString:@"compilation"]) {
1035                        [[self valueForKey:[NSString stringWithFormat:@"%@TextField", key]] setBackgroundColor:[NSColor whiteColor]];
1036                }
1037        }
1038        else {
1039                [super setValue:value forKey:key];
1040        }       
1041}
1042
1043- (BOOL) validateMenuItem:(NSMenuItem *)menuItem
1044{
1045        if(@selector(delete:) == [menuItem action]) {
1046                return ([[[_tabView selectedTabViewItem] identifier] isEqualToString:@"advanced"] && 0 < [[_tagsController selectedObjects] count]);
1047        }
1048       
1049        switch([menuItem tag]) {
1050                case kSaveMenuItemTag:
1051                case kRevertMenuItemTag:
1052                        return [self selectionDirty];
1053                        break;
1054                       
1055                case kOpenMenuItemTag:
1056                case kToggleDrawerMenuItemTag:
1057                        return YES;
1058                        break;
1059                       
1060                case kSelectNextMenuItemTag:
1061                        return [_filesController canSelectNext];
1062                        break;
1063                       
1064                case kSelectPreviousMenuItemTag:
1065                        return [_filesController canSelectPrevious];
1066                        break;
1067
1068                case kSelectAllFilesMenuItemTag:
1069                        return (1 < [self countOfFiles]);
1070                        break;
1071                       
1072                case kBasicTabMenuItemTag:
1073                case kAdvancedTabMenuItemTag:
1074                case kTabularTabMenuItemTag:
1075                        return YES;
1076                        break;
1077                       
1078                case kNewTagMenuItemTag:
1079                case kAddTagsFromFileMenuItemTag:
1080                case kGuessTagsMenuItemTag:
1081                case kRenameFilesMenuItemTag:
1082                        return (0 < [self countOfSelectedFiles]);
1083                        break;
1084                       
1085                case kCopySelectedTagsMenuItemTag:
1086                        return ([[[_tabView selectedTabViewItem] identi