source: trunk/macosx/Controller.m @ 256

Last change on this file since 256 was 256, checked in by titer, 16 years ago

Merged Mitchell's changes, this includes:
Adds stop at ratio,
Adds sorting by name, date or state,
Seperates Info window into its own nib,
Adds option to enable or disabled autostarting downloads,
Seperates Network / Transfers in the Preferences

File size: 40.2 KB
Line 
1/******************************************************************************
2 * Copyright (c) 2005-2006 Transmission authors and contributors
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *****************************************************************************/
22
23#import <IOKit/IOMessage.h>
24
25#import "Controller.h"
26#import "Torrent.h"
27#import "TorrentCell.h"
28#import "StringAdditions.h"
29#import "Utils.h"
30#import "TorrentTableView.h"
31
32#import "PrefsController.h"
33
34#define TOOLBAR_OPEN        @"Toolbar Open"
35#define TOOLBAR_REMOVE      @"Toolbar Remove"
36#define TOOLBAR_INFO        @"Toolbar Info"
37#define TOOLBAR_PAUSE_ALL   @"Toolbar Pause All"
38#define TOOLBAR_RESUME_ALL  @"Toolbar Resume All"
39
40#define WEBSITE_URL         @"http://transmission.m0k.org/"
41#define FORUM_URL           @"http://transmission.m0k.org/forum/"
42#define VERSION_PLIST_URL   @"http://transmission.m0k.org/version.plist"
43
44#define GROWL_PATH  @"/Library/PreferencePanes/Growl.prefPane/Contents/Resources/GrowlHelperApp.app"
45
46static void sleepCallBack( void * controller, io_service_t y,
47        natural_t messageType, void * messageArgument )
48{
49    Controller * c = controller;
50    [c sleepCallBack: messageType argument: messageArgument];
51}
52
53
54@implementation Controller
55
56- (id) init
57{
58    if ((self = [super init]))
59    {
60        fLib = tr_init();
61        fTorrents = [[NSMutableArray alloc] initWithCapacity: 10];
62    }
63    return self;
64}
65
66- (void) dealloc
67{
68    [fTorrents release];
69   
70    [fInfoController release];
71   
72    tr_close( fLib );
73    [super dealloc];
74}
75
76- (void) awakeFromNib
77{
78    [fPrefsController setPrefsWindow: fLib];
79    fDefaults = [NSUserDefaults standardUserDefaults];
80
81    fInfoController = [[InfoWindowController alloc] initWithWindowNibName: @"InfoWindow"];
82   
83    [fAdvancedBarItem setState: [fDefaults
84        boolForKey: @"UseAdvancedBar"] ? NSOnState : NSOffState];
85
86    fToolbar = [[NSToolbar alloc] initWithIdentifier: @"Transmission Toolbar"];
87    [fToolbar setDelegate: self];
88    [fToolbar setAllowsUserCustomization: YES];
89    [fToolbar setAutosavesConfiguration: YES];
90    [fWindow setToolbar: fToolbar];
91    [fWindow setDelegate: self];
92   
93    fStatusBar = YES;
94    if (![fDefaults boolForKey: @"StatusBar"])
95        [self toggleStatusBar: nil];
96   
97    [fActionButton setToolTip: @"Shortcuts for performing special actions."];
98
99    [fTableView setTorrents: fTorrents];
100    [[fTableView tableColumnWithIdentifier: @"Torrent"] setDataCell:
101        [[TorrentCell alloc] init]];
102
103    [fTableView registerForDraggedTypes:
104        [NSArray arrayWithObject: NSFilenamesPboardType]];
105
106    //Register for sleep notifications
107    IONotificationPortRef notify;
108    io_object_t anIterator;
109    if (fRootPort= IORegisterForSystemPower(self, & notify,
110                                sleepCallBack, & anIterator))
111    {
112        CFRunLoopAddSource( CFRunLoopGetCurrent(),
113                            IONotificationPortGetRunLoopSource( notify ),
114                            kCFRunLoopCommonModes );
115    }
116    else
117        NSLog( @"Could not IORegisterForSystemPower" );
118
119    //load torrents from history
120    Torrent * torrent;
121    NSDictionary * historyItem;
122    NSEnumerator * enumerator = [[fDefaults arrayForKey: @"History"] objectEnumerator];
123    while ((historyItem = [enumerator nextObject]))
124        if ((torrent = [[Torrent alloc] initWithHistory: historyItem lib: fLib]))
125        {
126            [fTorrents addObject: torrent];
127            [torrent release];
128        }
129   
130    [self torrentNumberChanged];
131   
132    //set sort
133    fSortType = [fDefaults stringForKey: @"Sort"];
134    if ([fSortType isEqualToString: @"Name"])
135        fCurrentSortItem = fNameSortItem;
136    else if ([fSortType isEqualToString: @"State"])
137        fCurrentSortItem = fStateSortItem;
138    else
139        fCurrentSortItem = fDateSortItem;
140    [fCurrentSortItem setState: NSOnState];
141
142    //check and register Growl if it is installed for this user or all users
143    NSFileManager * manager = [NSFileManager defaultManager];
144    fHasGrowl = [manager fileExistsAtPath: GROWL_PATH]
145                || [manager fileExistsAtPath: [[NSString stringWithFormat: @"~%@",
146                GROWL_PATH] stringByExpandingTildeInPath]];
147    [self growlRegister: self];
148
149    //initialize badging
150    fBadger = [[Badger alloc] init];
151
152    //timer to update the interface
153    fCompleted = 0;
154    [self updateUI: nil];
155    fTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self
156        selector: @selector( updateUI: ) userInfo: nil repeats: YES];
157    [[NSRunLoop currentRunLoop] addTimer: fTimer
158        forMode: NSModalPanelRunLoopMode];
159    [[NSRunLoop currentRunLoop] addTimer: fTimer
160        forMode: NSEventTrackingRunLoopMode];
161
162    //timer to check for updates
163    [self checkForUpdateTimer: nil];
164    fUpdateTimer = [NSTimer scheduledTimerWithTimeInterval: 60.0
165        target: self selector: @selector( checkForUpdateTimer: )
166        userInfo: nil repeats: YES];
167   
168    [self sortTorrents];
169   
170    //show windows
171    [fWindow makeKeyAndOrderFront: nil];
172    if ([fDefaults boolForKey: @"InfoVisible"])
173        [self showInfo: nil];
174}
175
176- (void) windowDidBecomeKey: (NSNotification *) n
177{
178    /* Reset the number of recently completed downloads */
179    fCompleted = 0;
180}
181
182- (BOOL) applicationShouldHandleReopen: (NSApplication *) app
183    hasVisibleWindows: (BOOL) flag
184{
185    [self showMainWindow: nil];
186    return NO;
187}
188
189- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) sender
190{
191    int active = 0;
192    Torrent * torrent;
193    NSEnumerator * enumerator = [fTorrents objectEnumerator];
194    while( ( torrent = [enumerator nextObject] ) )
195        if( [torrent isActive] )
196            active++;
197
198    if (active > 0 && [fDefaults boolForKey: @"CheckQuit"])
199    {
200        NSString * message = active == 1
201            ? @"There is an active torrent. Do you really want to quit?"
202            : [NSString stringWithFormat:
203                @"There are %d active torrents. Do you really want to quit?",
204                active];
205
206        NSBeginAlertSheet(@"Confirm Quit",
207                            @"Quit", @"Cancel", nil,
208                            fWindow, self,
209                            @selector(quitSheetDidEnd:returnCode:contextInfo:),
210                            nil, nil, message);
211        return NSTerminateLater;
212    }
213
214    return NSTerminateNow;
215}
216
217- (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode
218                        contextInfo: (void *) contextInfo
219{
220    [NSApp stopModal];
221    [NSApp replyToApplicationShouldTerminate:
222        (returnCode == NSAlertDefaultReturn)];
223}
224
225- (void) applicationWillTerminate: (NSNotification *) notification
226{
227    // Stop updating the interface
228    [fTimer invalidate];
229    [fUpdateTimer invalidate];
230
231    //clear badge
232    [fBadger clearBadge];
233    [fBadger release];
234   
235    //remember info window state
236    [fDefaults setBool: [[fInfoController window] isVisible] forKey: @"InfoVisible"];
237
238    //save history
239    [self updateTorrentHistory];
240
241    // Stop running torrents
242    [fTorrents makeObjectsPerformSelector: @selector(stop)];
243
244    // Wait for torrents to stop (5 seconds timeout)
245    NSDate * start = [NSDate date];
246    Torrent * torrent;
247    while ([fTorrents count] > 0)
248    {
249        torrent = [fTorrents objectAtIndex: 0];
250        while( [[NSDate date] timeIntervalSinceDate: start] < 5 &&
251                ![torrent isPaused] )
252        {
253            usleep( 100000 );
254            [torrent update];
255        }
256        [fTorrents removeObject: torrent];
257    }
258}
259
260- (void) showPreferenceWindow: (id) sender
261{
262    if (![fPrefsWindow isVisible])
263        [fPrefsWindow center];
264
265    [fPrefsWindow makeKeyAndOrderFront: nil];
266}
267
268- (void) folderChoiceClosed: (NSOpenPanel *) s returnCode: (int) code
269    contextInfo: (Torrent *) torrent
270{
271    if (code == NSOKButton)
272    {
273        [torrent setFolder: [[s filenames] objectAtIndex: 0]];
274        if ([fDefaults boolForKey: @"AutoStartDownload"])
275            [torrent start];
276        [fTorrents addObject: torrent];
277       
278        [self torrentNumberChanged];
279    }
280   
281    [NSApp stopModal];
282}
283
284- (void) application: (NSApplication *) sender
285         openFiles: (NSArray *) filenames
286{
287    BOOL autoStart = [fDefaults boolForKey: @"AutoStartDownload"];
288   
289    NSString * downloadChoice = [fDefaults stringForKey: @"DownloadChoice"];
290    NSString * torrentPath;
291    Torrent * torrent;
292    NSEnumerator * enumerator = [filenames objectEnumerator];
293    while ((torrentPath = [enumerator nextObject]))
294    {
295        if (!(torrent = [[Torrent alloc] initWithPath: torrentPath lib: fLib]))
296            continue;
297
298        /* Add it to the "File > Open Recent" menu */
299        [[NSDocumentController sharedDocumentController]
300            noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
301
302        if ([downloadChoice isEqualToString: @"Ask"])
303        {
304            NSOpenPanel * panel = [NSOpenPanel openPanel];
305
306            [panel setPrompt: @"Select Download Folder"];
307            [panel setAllowsMultipleSelection: NO];
308            [panel setCanChooseFiles: NO];
309            [panel setCanChooseDirectories: YES];
310
311            [panel setMessage: [NSString stringWithFormat:
312                                @"Select the download folder for %@",
313                                [torrentPath lastPathComponent]]];
314
315            [panel beginSheetForDirectory: nil file: nil types: nil
316                modalForWindow: fWindow modalDelegate: self didEndSelector:
317                @selector( folderChoiceClosed:returnCode:contextInfo: )
318                contextInfo: torrent];
319            [NSApp runModalForWindow: panel];
320        }
321        else
322        {
323            NSString * folder = [downloadChoice isEqualToString: @"Constant"]
324                                ? [[fDefaults stringForKey: @"DownloadFolder"]
325                                        stringByExpandingTildeInPath]
326                                : [torrentPath stringByDeletingLastPathComponent];
327
328            [torrent setFolder: folder];
329            if (autoStart)
330                [torrent start];
331            [fTorrents addObject: torrent];
332        }
333       
334        [torrent release];
335    }
336
337    [self torrentNumberChanged];
338
339    [self updateUI: nil];
340    [self sortTorrents];
341    [self updateTorrentHistory];
342}
343
344- (NSArray *) torrentsAtIndexes: (NSIndexSet *) indexSet
345{
346    NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [indexSet count]];
347    unsigned int i;
348    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
349        [torrents addObject: [fTorrents objectAtIndex: i]];
350
351    return torrents;
352}
353
354- (void) torrentNumberChanged
355{
356    int count = [fTorrents count];
357    [fTotalTorrentsField setStringValue: [NSString stringWithFormat:
358                @"%d Torrent%s", count, count == 1 ? "" : "s"]];
359}
360
361- (void) advancedChanged: (id) sender
362{
363    [fAdvancedBarItem setState: ![fAdvancedBarItem state]];
364    [fDefaults setBool: [fAdvancedBarItem state] forKey: @"UseAdvancedBar"];
365
366    [fTableView display];
367}
368
369//called on by applescript
370- (void) open: (NSArray *) files
371{
372    [self performSelectorOnMainThread: @selector(cantFindAName:)
373                withObject: files waitUntilDone: NO];
374}
375
376- (void) openShowSheet: (id) sender
377{
378    NSOpenPanel * panel = [NSOpenPanel openPanel];
379    NSArray * fileTypes = [NSArray arrayWithObject: @"torrent"];
380
381    [panel setAllowsMultipleSelection: YES];
382    [panel setCanChooseFiles: YES];
383    [panel setCanChooseDirectories: NO];
384
385    [panel beginSheetForDirectory: nil file: nil types: fileTypes
386        modalForWindow: fWindow modalDelegate: self didEndSelector:
387        @selector( openSheetClosed:returnCode:contextInfo: )
388        contextInfo: nil];
389}
390
391- (void) cantFindAName: (NSArray *) filenames
392{
393    [self application: NSApp openFiles: filenames];
394}
395
396- (void) openSheetClosed: (NSOpenPanel *) panel returnCode: (int) code
397    contextInfo: (void *) info
398{
399    if( code == NSOKButton )
400        [self performSelectorOnMainThread: @selector(cantFindAName:)
401                    withObject: [panel filenames] waitUntilDone: NO];
402}
403
404- (void) resumeTorrent: (id) sender
405{
406    [self resumeTorrentWithIndex: [fTableView selectedRowIndexes]];
407}
408
409- (void) resumeAllTorrents: (id) sender
410{
411    [self resumeTorrentWithIndex: [NSIndexSet indexSetWithIndexesInRange:
412                                    NSMakeRange(0, [fTorrents count])]];
413}
414
415- (void) resumeTorrentWithIndex: (NSIndexSet *) indexSet
416{
417    Torrent * torrent;
418    unsigned int i;
419    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
420    {
421        torrent = [fTorrents objectAtIndex: i];
422        [torrent start];
423    }
424   
425    [self updateUI: nil];
426    [self sortTorrents];
427    [self updateTorrentHistory];
428}
429
430- (void) stopTorrent: (id) sender
431{
432    [self stopTorrentWithIndex: [fTableView selectedRowIndexes]];
433}
434
435- (void) stopAllTorrents: (id) sender
436{
437    [self stopTorrentWithIndex: [NSIndexSet indexSetWithIndexesInRange:
438                                    NSMakeRange(0, [fTorrents count])]];
439}
440
441- (void) stopTorrentWithIndex: (NSIndexSet *) indexSet
442{
443    Torrent * torrent;
444    unsigned int i;
445    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
446    {
447        torrent = [fTorrents objectAtIndex: i];
448        [torrent stop];
449    }
450   
451    [self updateUI: nil];
452    [self sortTorrents];
453    [self updateTorrentHistory];
454}
455
456- (void) removeTorrentWithIndex: (NSIndexSet *) indexSet
457                  deleteTorrent: (BOOL) deleteTorrent
458                     deleteData: (BOOL) deleteData
459{
460    NSArray * torrents = [[self torrentsAtIndexes: indexSet] retain];
461    int active = 0;
462
463    Torrent * torrent;
464    NSEnumerator * enumerator = [torrents objectEnumerator];
465    while ((torrent = [enumerator nextObject]))
466        if ([torrent isActive])
467            active++;
468
469    if( active > 0 && [fDefaults boolForKey: @"CheckRemove"] )
470    {
471        NSDictionary * dict = [NSDictionary dictionaryWithObjectsAndKeys:
472            torrents, @"Torrents",
473            [NSNumber numberWithBool: deleteTorrent], @"DeleteTorrent",
474            [NSNumber numberWithBool: deleteData], @"DeleteData",
475            nil];
476        [dict retain];
477
478        NSString * title, * message;
479       
480        int selected = [fTableView numberOfSelectedRows];
481        if (selected == 1)
482        {
483            title = [NSString stringWithFormat: @"Comfirm Removal of %@",
484                        [[fTorrents objectAtIndex: [fTableView selectedRow]] name]];
485            message = @"This torrent is active. Do you really want to remove it?";
486        }
487        else
488        {
489            title = [NSString stringWithFormat: @"Comfirm Removal of %d Torrents", active];
490            if (selected == active)
491                message = [NSString stringWithFormat:
492                    @"There are %d active torrents. Do you really want to remove them?", active];
493            else
494                message = [NSString stringWithFormat:
495                    @"There are %d torrents (%d active). Do you really want to remove them?", selected, active];
496        }
497
498        NSBeginAlertSheet(title,
499            @"Remove", @"Cancel", nil, fWindow, self,
500            @selector(removeSheetDidEnd:returnCode:contextInfo:),
501            nil, dict, message);
502    }
503    else
504    {
505        [self confirmRemoveTorrents: torrents
506                deleteTorrent: deleteTorrent
507                deleteData: deleteData];
508    }
509}
510
511- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode
512                        contextInfo: (NSDictionary *) dict
513{
514    [NSApp stopModal];
515
516    NSArray * torrents = [dict objectForKey: @"Torrents"];
517    BOOL deleteTorrent = [[dict objectForKey: @"DeleteTorrent"] boolValue];
518    BOOL deleteData = [[dict objectForKey: @"DeleteData"] boolValue];
519    [dict release];
520   
521    if (returnCode == NSAlertDefaultReturn)
522    {
523        [self confirmRemoveTorrents: torrents
524            deleteTorrent: deleteTorrent
525            deleteData: deleteData];
526    }
527    else
528        [torrents release];
529}
530
531- (void) confirmRemoveTorrents: (NSArray *) torrents
532            deleteTorrent: (BOOL) deleteTorrent
533            deleteData: (BOOL) deleteData
534{
535    Torrent * torrent;
536    NSEnumerator * enumerator = [torrents objectEnumerator];
537    while ((torrent = [enumerator nextObject]))
538    {
539        [torrent stop];
540
541        if( deleteData )
542            [torrent trashData];
543           
544        if( deleteTorrent )
545            [torrent trashTorrent];
546
547        [fTorrents removeObject: torrent];
548    }
549    [torrents release];
550   
551    [self torrentNumberChanged];
552
553    [fTableView deselectAll: nil];
554    [self updateUI: nil];
555    [self updateTorrentHistory];
556}
557
558- (void) removeTorrent: (id) sender
559{
560    [self removeTorrentWithIndex: [fTableView selectedRowIndexes] deleteTorrent: NO deleteData: NO];
561}
562
563- (void) removeTorrentDeleteFile: (id) sender
564{
565    [self removeTorrentWithIndex: [fTableView selectedRowIndexes] deleteTorrent: YES deleteData: NO];
566}
567
568- (void) removeTorrentDeleteData: (id) sender
569{
570    [self removeTorrentWithIndex: [fTableView selectedRowIndexes] deleteTorrent: NO deleteData: YES];
571}
572
573- (void) removeTorrentDeleteBoth: (id) sender
574{
575    [self removeTorrentWithIndex: [fTableView selectedRowIndexes] deleteTorrent: YES deleteData: YES];
576}
577
578- (void) revealTorrent: (id) sender
579{
580    Torrent * torrent;
581    NSIndexSet * indexSet = [fTableView selectedRowIndexes];
582    unsigned int i;
583   
584    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
585    {
586        torrent = [fTorrents objectAtIndex: i];
587        [torrent reveal];
588    }
589}
590
591- (void) updateUI: (NSTimer *) t
592{
593    NSEnumerator * enumerator = [fTorrents objectEnumerator];
594    Torrent * torrent;
595    while( ( torrent = [enumerator nextObject] ) )
596    {
597        [torrent update];
598
599        if( [torrent justFinished] )
600        {
601            /* Notifications */
602            [self notifyGrowl: [torrent name]];
603            if( ![fWindow isKeyWindow] )
604                fCompleted++;
605               
606            [self sortTorrents];
607        }
608    }
609   
610    [fTableView reloadData];
611   
612    //Update the global DL/UL rates
613    float dl, ul;
614    tr_torrentRates( fLib, &dl, &ul );
615    NSString * downloadRate = [NSString stringForSpeed: dl];
616    NSString * uploadRate = [NSString stringForSpeed: ul];
617    [fTotalDLField setStringValue: downloadRate];
618    [fTotalULField setStringValue: uploadRate];
619
620    [self updateInfoStats];
621
622    //badge dock
623    [fBadger updateBadgeWithCompleted: fCompleted
624        uploadRate: ul >= 0.1 && [fDefaults boolForKey: @"BadgeUploadRate"]
625          ? [NSString stringForSpeedAbbrev: ul] : nil
626        downloadRate: dl >= 0.1 && [fDefaults boolForKey: @"BadgeDownloadRate"]
627          ? [NSString stringForSpeedAbbrev: dl] : nil];
628}
629
630- (void) updateTorrentHistory
631{
632    NSMutableArray * history = [NSMutableArray
633        arrayWithCapacity: [fTorrents count]];
634
635    NSEnumerator * enumerator = [fTorrents objectEnumerator];
636    Torrent * torrent;
637    while( ( torrent = [enumerator nextObject] ) )
638        [history addObject: [torrent history]];
639
640    [fDefaults setObject: history forKey: @"History"];
641}
642
643- (void) showInfo: (id) sender
644{
645    if ([[fInfoController window] isVisible])
646        [[fInfoController window] performClose: nil];
647    else
648    {
649        [fInfoController updateInfoForTorrents: [self torrentsAtIndexes:
650                                            [fTableView selectedRowIndexes]]];
651        [[fInfoController window] orderFront: nil];
652    }
653}
654
655- (void) updateInfo
656{
657    if ([[fInfoController window] isVisible])
658        [fInfoController updateInfoForTorrents: [self torrentsAtIndexes:
659                                            [fTableView selectedRowIndexes]]];
660}
661
662- (void) updateInfoStats
663{
664    if ([[fInfoController window] isVisible])
665        [fInfoController updateInfoStatsForTorrents: [self torrentsAtIndexes:
666                                            [fTableView selectedRowIndexes]]];
667}
668
669- (void) sortTorrents
670{
671    //remember selected rows if needed
672    NSArray * selectedTorrents = nil;
673    int numSelected = [fTableView numberOfSelectedRows];
674    if (numSelected > 0 && numSelected < [fTorrents count])
675        selectedTorrents = [self torrentsAtIndexes: [fTableView selectedRowIndexes]];
676
677    NSSortDescriptor * nameDescriptor = [[[NSSortDescriptor alloc] initWithKey:
678                                            @"name" ascending: YES] autorelease],
679                    * dateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
680                                            @"date" ascending: YES] autorelease];
681
682    NSArray * descriptors;
683    if ([fSortType isEqualToString: @"Name"])
684        descriptors = [[NSArray alloc] initWithObjects: nameDescriptor, dateDescriptor, nil];
685    else if ([fSortType isEqualToString: @"State"])
686    {
687        NSSortDescriptor * stateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
688                                                @"stateSortKey" ascending: NO] autorelease];
689        descriptors = [[NSArray alloc] initWithObjects: stateDescriptor, nameDescriptor, dateDescriptor, nil];
690    }
691    else
692        descriptors = [[NSArray alloc] initWithObjects: dateDescriptor, nameDescriptor, nil];
693
694    [fTorrents sortUsingDescriptors: descriptors];
695    [descriptors release];
696   
697    [fTableView reloadData];
698   
699    //set selected rows if needed
700    if (selectedTorrents)
701    {
702        [fTableView deselectAll: nil];
703       
704        Torrent * torrent;
705        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
706        while ((torrent = [enumerator nextObject]))
707            [fTableView selectRow: [fTorrents indexOfObject: torrent] byExtendingSelection: YES];
708    }
709}
710
711- (void) setSort: (id) sender
712{
713    [fCurrentSortItem setState: NSOffState];
714    fCurrentSortItem = sender;
715    [sender setState: NSOnState];
716
717    if (sender == fNameSortItem)
718        fSortType = @"Name";
719    else if (sender == fStateSortItem)
720        fSortType = @"State";
721    else
722        fSortType = @"Date";
723       
724    [fDefaults setObject: fSortType forKey: @"Sort"];
725
726    [self sortTorrents];
727}
728
729- (int) numberOfRowsInTableView: (NSTableView *) t
730{
731    return [fTorrents count];
732}
733
734- (void) tableView: (NSTableView *) t willDisplayCell: (id) cell
735    forTableColumn: (NSTableColumn *) tableColumn row: (int) rowIndex
736{
737    [cell setTorrent: [fTorrents objectAtIndex: rowIndex]];
738
739    if( [fWindow isKeyWindow] && [fTableView isRowSelected: rowIndex] )
740        [cell setTextColor: [NSColor whiteColor]];
741    else
742        [cell setTextColor: [NSColor blackColor]];
743}
744
745- (BOOL) tableView: (NSTableView *) t acceptDrop:
746    (id <NSDraggingInfo>) info row: (int) row dropOperation:
747    (NSTableViewDropOperation) operation
748{
749    [self application: NSApp openFiles: [[[info draggingPasteboard]
750        propertyListForType: NSFilenamesPboardType]
751        pathsMatchingExtensions: [NSArray arrayWithObject: @"torrent"]]];
752    return YES;
753}
754
755- (NSDragOperation) tableView: (NSTableView *) t validateDrop:
756    (id <NSDraggingInfo>) info proposedRow: (int) row
757    proposedDropOperation: (NSTableViewDropOperation) operation
758{
759    NSPasteboard * pasteboard = [info draggingPasteboard];
760
761    if (![[pasteboard types] containsObject: NSFilenamesPboardType]
762            || [[[pasteboard propertyListForType: NSFilenamesPboardType]
763                pathsMatchingExtensions: [NSArray arrayWithObject: @"torrent"]]
764                count] == 0)
765        return NSDragOperationNone;
766
767    [fTableView setDropRow: [fTableView numberOfRows]
768        dropOperation: NSTableViewDropAbove];
769    return NSDragOperationGeneric;
770}
771
772- (void) tableViewSelectionDidChange: (NSNotification *) n
773{
774    [self updateInfo];
775}
776
777- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier:
778    (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
779{
780    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
781
782    if( [ident isEqualToString: TOOLBAR_OPEN] )
783    {
784        [item setLabel: @"Open"];
785        [item setPaletteLabel: [item label]];
786        [item setToolTip: @"Open torrent files"];
787        [item setImage: [NSImage imageNamed: @"Open.png"]];
788        [item setTarget: self];
789        [item setAction: @selector( openShowSheet: )];
790    }
791    else if( [ident isEqualToString: TOOLBAR_REMOVE] )
792    {
793        [item setLabel: @"Remove"];
794        [item setPaletteLabel: [item label]];
795        [item setToolTip: @"Remove selected torrents"];
796        [item setImage: [NSImage imageNamed: @"Remove.png"]];
797        [item setTarget: self];
798        [item setAction: @selector( removeTorrent: )];
799    }
800    else if( [ident isEqualToString: TOOLBAR_INFO] )
801    {
802        [item setLabel: @"Info"];
803        [item setPaletteLabel: [item label]];
804        [item setToolTip: @"Display torrent info"];
805        [item setImage: [NSImage imageNamed: @"Info.png"]];
806        [item setTarget: self];
807        [item setAction: @selector( showInfo: )];
808    }
809    else if( [ident isEqualToString: TOOLBAR_RESUME_ALL] )
810    {
811        [item setLabel: @"Resume All"];
812        [item setPaletteLabel: [item label]];
813        [item setToolTip: @"Resume all torrents"];
814        [item setImage: [NSImage imageNamed: @"ResumeAll.png"]];
815        [item setTarget: self];
816        [item setAction: @selector( resumeAllTorrents: )];
817    }
818    else if( [ident isEqualToString: TOOLBAR_PAUSE_ALL] )
819    {
820        [item setLabel: @"Pause All"];
821        [item setPaletteLabel: [item label]];
822        [item setToolTip: @"Pause all torrents"];
823        [item setImage: [NSImage imageNamed: @"PauseAll.png"]];
824        [item setTarget: self];
825        [item setAction: @selector( stopAllTorrents: )];
826    }
827    else
828    {
829        [item release];
830        return nil;
831    }
832
833    return item;
834}
835
836- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t
837{
838    return [NSArray arrayWithObjects:
839            TOOLBAR_OPEN, TOOLBAR_REMOVE,
840            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
841            TOOLBAR_INFO,
842            NSToolbarSeparatorItemIdentifier,
843            NSToolbarSpaceItemIdentifier,
844            NSToolbarFlexibleSpaceItemIdentifier,
845            NSToolbarCustomizeToolbarItemIdentifier, nil];
846}
847
848- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t
849{
850    return [NSArray arrayWithObjects:
851            TOOLBAR_OPEN, TOOLBAR_REMOVE,
852            NSToolbarSeparatorItemIdentifier,
853            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
854            NSToolbarFlexibleSpaceItemIdentifier,
855            TOOLBAR_INFO, nil];
856}
857
858- (void) toggleStatusBar: (id) sender
859{
860    fStatusBar = !fStatusBar;
861
862    NSSize frameSize = [fScrollView frame].size;
863    [fStats setHidden: !fStatusBar];
864   
865    if (fStatusBar)
866        frameSize.height -= 18;
867    else
868        frameSize.height += 18;
869
870    [fScrollView setFrameSize: frameSize];
871    [fWindow display];
872   
873    [fDefaults setBool: fStatusBar forKey: @"StatusBar"];
874}
875
876- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
877{
878    SEL action = [toolbarItem action];
879
880    //enable remove item
881    if (action == @selector(removeTorrent:))
882        return [fTableView numberOfSelectedRows] > 0;
883
884    Torrent * torrent;
885    NSEnumerator * enumerator;
886
887    //enable resume all item
888    if (action == @selector(resumeAllTorrents:))
889    {
890        enumerator = [fTorrents objectEnumerator];
891        while( ( torrent = [enumerator nextObject] ) )
892            if( [torrent isPaused] )
893                return YES;
894        return NO;
895    }
896
897    //enable pause all item
898    if (action == @selector(stopAllTorrents:))
899    {
900        enumerator = [fTorrents objectEnumerator];
901        while( ( torrent = [enumerator nextObject] ) )
902            if( [torrent isActive] )
903                return YES;
904        return NO;
905    }
906
907    return YES;
908}
909
910- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
911{
912    SEL action = [menuItem action];
913
914    //only enable some menus if window is useable
915    BOOL canUseWindow = [fWindow isKeyWindow] && ![fToolbar customizationPaletteIsRunning];
916
917    //enable show info
918    if (action == @selector(showInfo:))
919    {
920        [menuItem setTitle: [[fInfoController window] isVisible] ? @"Hide Info" : @"Show Info"];
921        return YES;
922    }
923   
924    //enable toggle toolbar
925    if (action == @selector(toggleStatusBar:))
926    {
927        [menuItem setTitle: fStatusBar ? @"Hide Status Bar" : @"Show Status Bar"];
928        return canUseWindow;
929    }
930
931    //enable resume all item
932    if (action == @selector(resumeAllTorrents:))
933    {
934        Torrent * torrent;
935        NSEnumerator * enumerator = [fTorrents objectEnumerator];
936        while( ( torrent = [enumerator nextObject] ) )
937            if( [torrent isPaused] )
938                return YES;
939        return NO;
940    }
941
942    //enable pause all item
943    if (action == @selector(stopAllTorrents:))
944    {
945        Torrent * torrent;
946        NSEnumerator * enumerator = [fTorrents objectEnumerator];
947        while( ( torrent = [enumerator nextObject] ) )
948            if( [torrent isActive] )
949                return YES;
950        return NO;
951    }
952
953    if (action == @selector(revealTorrent:))
954    {
955        return canUseWindow && [fTableView numberOfSelectedRows] > 0;
956    }
957
958    //enable remove items
959    if (action == @selector(removeTorrent:)
960        || action == @selector(removeTorrentDeleteFile:)
961        || action == @selector(removeTorrentDeleteData:)
962        || action == @selector(removeTorrentDeleteBoth:))
963    {
964        BOOL active = NO;
965        Torrent * torrent;
966        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
967        unsigned int i;
968       
969        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
970        {
971            torrent = [fTorrents objectAtIndex: i];
972            if ([torrent isActive])
973            {
974                active = YES;
975                break;
976            }
977        }
978   
979        //append or remove ellipsis when needed
980        NSString * title = [menuItem title];
981        if (active && [fDefaults boolForKey: @"CheckRemove"])
982        {
983            if (![title hasSuffix: NS_ELLIPSIS])
984                [menuItem setTitle: [title stringByAppendingString: NS_ELLIPSIS]];
985        }
986        else
987        {
988            if ([title hasSuffix: NS_ELLIPSIS])
989                [menuItem setTitle:[title substringToIndex:
990                            [title rangeOfString: NS_ELLIPSIS].location]];
991        }
992        return canUseWindow && [fTableView numberOfSelectedRows] > 0;
993    }
994
995    //enable pause item
996    if( action == @selector(stopTorrent:) )
997    {
998        if (!canUseWindow)
999            return NO;
1000   
1001        Torrent * torrent;
1002        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1003        unsigned int i;
1004       
1005        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1006        {
1007            torrent = [fTorrents objectAtIndex: i];
1008            if ([torrent isActive])
1009                return YES;
1010        }
1011        return NO;
1012    }
1013   
1014    //enable resume item
1015    if( action == @selector(resumeTorrent:) )
1016    {
1017        if (!canUseWindow)
1018            return NO;
1019   
1020        Torrent * torrent;
1021        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1022        unsigned int i;
1023       
1024        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1025        {
1026            torrent = [fTorrents objectAtIndex: i];
1027            if ([torrent isPaused])
1028                return YES;
1029        }
1030        return NO;
1031    }
1032   
1033    //enable resume item
1034    if (action == @selector(setSort:))
1035        return canUseWindow;
1036
1037    return YES;
1038}
1039
1040- (void) sleepCallBack: (natural_t) messageType argument:
1041                          (void *) messageArgument
1042{
1043    NSEnumerator * enumerator;
1044    Torrent * torrent;
1045    BOOL active;
1046
1047    switch( messageType )
1048    {
1049        case kIOMessageSystemWillSleep:
1050            /* Close all connections before going to sleep and remember
1051               we should resume when we wake up */
1052            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
1053
1054            /* Wait for torrents to stop (5 seconds timeout) */
1055            NSDate * start = [NSDate date];
1056            enumerator = [fTorrents objectEnumerator];
1057            while( ( torrent = [enumerator nextObject] ) )
1058            {
1059                while( [[NSDate date] timeIntervalSinceDate: start] < 5 &&
1060                        ![torrent isPaused] )
1061                {
1062                    usleep( 100000 );
1063                    [torrent update];
1064                }
1065            }
1066
1067            IOAllowPowerChange( fRootPort, (long) messageArgument );
1068            break;
1069
1070        case kIOMessageCanSystemSleep:
1071            /* Prevent idle sleep unless all paused */
1072            active = NO;
1073            enumerator = [fTorrents objectEnumerator];
1074            while( !active && ( torrent = [enumerator nextObject] ) )
1075                if( [torrent isActive] )
1076                    active = YES;
1077
1078            if (active)
1079                IOCancelPowerChange( fRootPort, (long) messageArgument );
1080            else
1081                IOAllowPowerChange( fRootPort, (long) messageArgument );
1082            break;
1083
1084        case kIOMessageSystemHasPoweredOn:
1085            /* Resume download after we wake up */
1086            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
1087            break;
1088    }
1089}
1090
1091- (NSRect) windowWillUseStandardFrame: (NSWindow *) w
1092    defaultFrame: (NSRect) defaultFrame
1093{
1094    NSRect rectWin = [fWindow frame];
1095    float newHeight = rectWin.size.height - [fScrollView frame].size.height
1096            + [fTorrents count] * ([fTableView rowHeight] + [fTableView intercellSpacing].height);
1097
1098    float minHeight = [fWindow minSize].height;
1099    if (newHeight < minHeight)
1100        newHeight = minHeight;
1101
1102    rectWin.origin.y -= (newHeight - rectWin.size.height);
1103    rectWin.size.height = newHeight;
1104
1105    return rectWin;
1106}
1107
1108- (void) showMainWindow: (id) sender
1109{
1110    [fWindow makeKeyAndOrderFront: nil];
1111}
1112
1113- (void) linkHomepage: (id) sender
1114{
1115    [[NSWorkspace sharedWorkspace] openURL: [NSURL
1116        URLWithString: WEBSITE_URL]];
1117}
1118
1119- (void) linkForums: (id) sender
1120{
1121    [[NSWorkspace sharedWorkspace] openURL: [NSURL
1122        URLWithString: FORUM_URL]];
1123}
1124
1125- (void) notifyGrowl: (NSString * ) file
1126{
1127    NSString * growlScript;
1128    NSAppleScript * appleScript;
1129    NSDictionary * error;
1130
1131    if( !fHasGrowl )
1132        return;
1133
1134    growlScript = [NSString stringWithFormat:
1135        @"tell application \"System Events\"\n"
1136         "  if exists application process \"GrowlHelperApp\" then\n"
1137         "    tell application \"GrowlHelperApp\"\n "
1138         "      notify with name \"Download Complete\""
1139         "        title \"Download Complete\""
1140         "        description \"%@\""
1141         "        application name \"Transmission\"\n"
1142         "    end tell\n"
1143         "  end if\n"
1144         "end tell", file];
1145    appleScript = [[NSAppleScript alloc] initWithSource: growlScript];
1146    if( ![appleScript executeAndReturnError: &error] )
1147    {
1148        NSLog( @"Growl notify failed" );
1149    }
1150    [appleScript release];
1151}
1152
1153- (void) growlRegister: (id) sender
1154{
1155    NSString * growlScript;
1156    NSAppleScript * appleScript;
1157    NSDictionary * error;
1158
1159    if( !fHasGrowl )
1160        return;
1161
1162    growlScript = [NSString stringWithFormat:
1163        @"tell application \"System Events\"\n"
1164         "  if exists application process \"GrowlHelperApp\" then\n"
1165         "    tell application \"GrowlHelperApp\"\n"
1166         "      register as application \"Transmission\" "
1167         "        all notifications {\"Download Complete\"}"
1168         "        default notifications {\"Download Complete\"}"
1169         "        icon of application \"Transmission\"\n"
1170         "    end tell\n"
1171         "  end if\n"
1172         "end tell"];
1173
1174    appleScript = [[NSAppleScript alloc] initWithSource: growlScript];
1175    if( ![appleScript executeAndReturnError: &error] )
1176    {
1177        NSLog( @"Growl registration failed" );
1178    }
1179    [appleScript release];
1180}
1181
1182- (void) checkForUpdate: (id) sender
1183{
1184    [self checkForUpdateAuto: NO];
1185}
1186
1187- (void) checkForUpdateTimer: (NSTimer *) timer
1188{
1189    NSString * check = [fDefaults stringForKey: @"VersionCheck"];
1190
1191    NSTimeInterval interval;
1192    if( [check isEqualToString: @"Daily"] )
1193        interval = 24 * 3600;
1194    else if( [check isEqualToString: @"Weekly"] )
1195        interval = 7 * 24 * 3600;
1196    else
1197        return;
1198
1199    NSDate * lastDate = [fDefaults objectForKey: @"VersionCheckLast"];
1200    if( lastDate )
1201    {
1202        NSTimeInterval actualInterval = [[NSDate date]
1203                            timeIntervalSinceDate: lastDate];
1204        if( actualInterval > 0 && actualInterval < interval )
1205            return;
1206    }
1207
1208    [self checkForUpdateAuto: YES];
1209    [fDefaults setObject: [NSDate date] forKey: @"VersionCheckLast"];
1210}
1211
1212- (void) checkForUpdateAuto: (BOOL) automatic
1213{
1214    fCheckIsAutomatic = automatic;
1215    [[NSURL URLWithString: VERSION_PLIST_URL]
1216            loadResourceDataNotifyingClient: self usingCache: NO];
1217}
1218
1219- (void) URLResourceDidFinishLoading: (NSURL *) sender
1220{
1221    //check if plist was actually found and contains a version
1222    NSDictionary * dict = [NSPropertyListSerialization
1223                            propertyListFromData: [sender resourceDataUsingCache: NO]
1224                            mutabilityOption: NSPropertyListImmutable
1225                            format: nil errorDescription: nil];
1226    NSString * webVersion;
1227    if (!dict || !(webVersion = [dict objectForKey: @"Version"]))
1228    {
1229        if (!fCheckIsAutomatic)
1230        {
1231            NSAlert * dialog = [[NSAlert alloc] init];
1232            [dialog addButtonWithTitle: @"OK"];
1233            [dialog setMessageText: @"Error checking for updates."];
1234            [dialog setInformativeText:
1235                    @"Transmission was not able to check the latest version available."];
1236            [dialog setAlertStyle: NSInformationalAlertStyle];
1237
1238            [dialog runModal];
1239            [dialog release];
1240        }
1241        return;
1242    }
1243
1244    NSString * currentVersion = [[[NSBundle mainBundle] infoDictionary]
1245                                objectForKey: (NSString *)kCFBundleVersionKey];
1246
1247    NSEnumerator * webEnum = [[webVersion componentsSeparatedByString: @"."] objectEnumerator],
1248            * currentEnum = [[currentVersion componentsSeparatedByString: @"."] objectEnumerator];
1249    NSString * webSub, * currentSub;
1250
1251    BOOL webGreater = NO;
1252    NSComparisonResult result;
1253    while ((webSub = [webEnum nextObject]))
1254    {
1255        if (!(currentSub = [currentEnum nextObject]))
1256        {
1257            webGreater = YES;
1258            break;
1259        }
1260
1261        result = [currentSub compare: webSub options: NSNumericSearch];
1262        if (result != NSOrderedSame)
1263        {
1264            if (result == NSOrderedAscending)
1265                webGreater = YES;
1266            break;
1267        }
1268    }
1269
1270    if (webGreater)
1271    {
1272        NSAlert * dialog = [[NSAlert alloc] init];
1273        [dialog addButtonWithTitle: @"Go to Website"];
1274        [dialog addButtonWithTitle:@"Cancel"];
1275        [dialog setMessageText: @"New version is available!"];
1276        [dialog setInformativeText: [NSString stringWithFormat:
1277            @"A newer version (%@) is available for download from the Transmission website.", webVersion]];
1278        [dialog setAlertStyle: NSInformationalAlertStyle];
1279
1280        if ([dialog runModal] == NSAlertFirstButtonReturn)
1281            [self linkHomepage: nil];
1282
1283        [dialog release];
1284    }
1285    else if (!fCheckIsAutomatic)
1286    {
1287        NSAlert * dialog = [[NSAlert alloc] init];
1288        [dialog addButtonWithTitle: @"OK"];
1289        [dialog setMessageText: @"No new versions are available."];
1290        [dialog setInformativeText: [NSString stringWithFormat:
1291            @"You are running the most current version of Transmission (%@).", currentVersion]];
1292        [dialog setAlertStyle: NSInformationalAlertStyle];
1293
1294        [dialog runModal];
1295        [dialog release];
1296    }
1297    else;
1298}
1299
1300@end
Note: See TracBrowser for help on using the repository browser.