source: trunk/macosx/Controller.m @ 831

Last change on this file since 831 was 831, checked in by livings124, 15 years ago

Display an error if saving of the log file fails.

  • Property svn:keywords set to Date Rev Author Id
File size: 80.6 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 831 2006-08-24 02:41:12Z livings124 $
3 *
4 * Copyright (c) 2005-2006 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#import <IOKit/IOMessage.h>
26
27#import "Controller.h"
28#import "Torrent.h"
29#import "TorrentCell.h"
30#import "TorrentTableView.h"
31#import "StringAdditions.h"
32
33#import <Sparkle/Sparkle.h>
34
35#define TOOLBAR_OPEN            @"Toolbar Open"
36#define TOOLBAR_REMOVE          @"Toolbar Remove"
37#define TOOLBAR_INFO            @"Toolbar Info"
38#define TOOLBAR_PAUSE_ALL       @"Toolbar Pause All"
39#define TOOLBAR_RESUME_ALL      @"Toolbar Resume All"
40#define TOOLBAR_PAUSE_SELECTED  @"Toolbar Pause Selected"
41#define TOOLBAR_RESUME_SELECTED @"Toolbar Resume Selected"
42#define TOOLBAR_FILTER          @"Toolbar Toggle Filter"
43
44#define GROWL_DOWNLOAD_COMPLETE @"Download Complete"
45#define GROWL_SEEDING_COMPLETE  @"Seeding Complete"
46#define GROWL_AUTO_ADD          @"Torrent Auto Added"
47#define GROWL_AUTO_SPEED_LIMIT  @"Speed Limit Auto Changed"
48
49#define TORRENT_TABLE_VIEW_DATA_TYPE    @"TorrentTableViewDataType"
50
51#define ROW_HEIGHT_REGULAR      65.0
52#define ROW_HEIGHT_SMALL        40.0
53#define WINDOW_REGULAR_WIDTH    468.0
54
55#define UPDATE_UI_SECONDS           1.0
56#define AUTO_IMPORT_SECONDS         15.0
57#define AUTO_SPEED_LIMIT_SECONDS    10.0
58
59#define WEBSITE_URL @"http://transmission.m0k.org/"
60#define FORUM_URL   @"http://transmission.m0k.org/forum/"
61
62static void sleepCallBack(void * controller, io_service_t y, natural_t messageType, void * messageArgument)
63{
64    Controller * c = controller;
65    [c sleepCallBack: messageType argument: messageArgument];
66}
67
68@implementation Controller
69
70+ (void) initialize
71{
72    [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithContentsOfFile:
73        [[NSBundle mainBundle] pathForResource: @"Defaults" ofType: @"plist"]]];
74}
75
76- (id) init
77{
78    if ((self = [super init]))
79    {
80        fLib = tr_init();
81       
82        fTorrents = [[NSMutableArray alloc] initWithCapacity: 10];
83        fDisplayedTorrents = [[NSMutableArray alloc] initWithCapacity: 10];
84       
85        fDefaults = [NSUserDefaults standardUserDefaults];
86       
87        fMessageController = [[MessageWindowController alloc] initWithWindowNibName: @"MessageWindow"];
88        fInfoController = [[InfoWindowController alloc] initWithWindowNibName: @"InfoWindow"];
89        fPrefsController = [[PrefsController alloc] initWithWindowNibName: @"PrefsWindow" handle: fLib];
90       
91        fBadger = [[Badger alloc] init];
92       
93        fAutoImportedNames = [[NSMutableArray alloc] init];
94       
95        [GrowlApplicationBridge setGrowlDelegate: self];
96    }
97    return self;
98}
99
100- (void) dealloc
101{
102    [[NSNotificationCenter defaultCenter] removeObserver: self];
103
104    [fTorrents release];
105    [fDisplayedTorrents release];
106   
107    [fToolbar release];
108    [fInfoController release];
109    [fPrefsController release];
110    [fBadger release];
111   
112    [fSortType release];
113    [fFilterType release];
114    [fAutoImportedNames release];
115   
116    tr_close(fLib);
117    [super dealloc];
118}
119
120- (void) awakeFromNib
121{
122    [fStatusBar setBackgroundImage: [NSImage imageNamed: @"StatusBarBackground.png"]];
123    [fFilterBar setBackgroundImage: [NSImage imageNamed: @"FilterBarBackground.png"]];
124   
125    [fWindow setAcceptsMouseMovedEvents: YES]; //ensure filter buttons display correctly
126   
127    [fAdvancedBarItem setState: [fDefaults boolForKey: @"UseAdvancedBar"]];
128
129    fToolbar = [[NSToolbar alloc] initWithIdentifier: @"Transmission Toolbar"];
130    [fToolbar setDelegate: self];
131    [fToolbar setAllowsUserCustomization: YES];
132    [fToolbar setAutosavesConfiguration: YES];
133    [fWindow setToolbar: fToolbar];
134    [fWindow setDelegate: self];
135   
136    [fWindow makeFirstResponder: fTableView];
137    [fWindow setExcludedFromWindowsMenu: YES];
138   
139    //set table size
140    if ([fDefaults boolForKey: @"SmallView"])
141    {
142        [fTableView setRowHeight: ROW_HEIGHT_SMALL];
143        [fSmallViewItem setState: NSOnState];
144    }
145   
146    //window min height
147    NSSize contentMinSize = [fWindow contentMinSize];
148    contentMinSize.height = [[fWindow contentView] frame].size.height - [fScrollView frame].size.height
149                                + [fTableView rowHeight] + [fTableView intercellSpacing].height;
150    [fWindow setContentMinSize: contentMinSize];
151   
152    //set info keyboard shortcuts
153    unichar ch = NSRightArrowFunctionKey;
154    [fNextInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & ch length: 1]];
155    ch = NSLeftArrowFunctionKey;
156    [fPrevInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & ch length: 1]];
157   
158    //set up filter bar
159    NSView * contentView = [fWindow contentView];
160    [fFilterBar setHidden: YES];
161   
162    NSRect filterBarFrame = [fFilterBar frame];
163    filterBarFrame.size.width = [fWindow frame].size.width;
164    [fFilterBar setFrame: filterBarFrame];
165   
166    [contentView addSubview: fFilterBar];
167    [fFilterBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
168   
169    [self showFilterBar: [fDefaults boolForKey: @"FilterBar"] animate: NO];
170   
171    //set up status bar
172    [fStatusBar setHidden: YES];
173   
174    NSRect statusBarFrame = [fStatusBar frame];
175    statusBarFrame.size.width = [fWindow frame].size.width;
176    [fStatusBar setFrame: statusBarFrame];
177   
178    [contentView addSubview: fStatusBar];
179    [fStatusBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
180    [self showStatusBar: [fDefaults boolForKey: @"StatusBar"] animate: NO];
181   
182    //set speed limit
183    fSpeedLimitNormalImage = [fSpeedLimitButton image];
184    fSpeedLimitBlueImage = [NSImage imageNamed: @"SpeedLimitButtonBlue.png"];
185    fSpeedLimitGraphiteImage = [NSImage imageNamed: @"SpeedLimitButtonGraphite.png"];
186   
187    [self updateControlTint: nil];
188   
189    if ((fSpeedLimitEnabled = [fDefaults boolForKey: @"SpeedLimit"]))
190    {
191        [fSpeedLimitItem setState: NSOnState];
192        [fSpeedLimitDockItem setState: NSOnState];
193       
194        [fSpeedLimitButton setImage: [NSColor currentControlTint] == NSBlueControlTint
195                                        ? fSpeedLimitBlueImage : fSpeedLimitGraphiteImage];
196    }
197
198    [fActionButton setToolTip: @"Shortcuts for changing global settings."];
199    [fSpeedLimitButton setToolTip: @"Speed Limit overrides the total bandwidth limits with its own limits."];
200
201    [fTableView setTorrents: fDisplayedTorrents];
202    [[fTableView tableColumnWithIdentifier: @"Torrent"] setDataCell: [[TorrentCell alloc] init]];
203
204    [fTableView registerForDraggedTypes: [NSArray arrayWithObjects: NSFilenamesPboardType,
205                                                        TORRENT_TABLE_VIEW_DATA_TYPE, nil]];
206
207    //register for sleep notifications
208    IONotificationPortRef notify;
209    io_object_t iterator;
210    if (fRootPort = IORegisterForSystemPower(self, & notify, sleepCallBack, & iterator))
211        CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify),
212                            kCFRunLoopCommonModes);
213    else
214        NSLog(@"Could not IORegisterForSystemPower");
215
216    //load torrents from history
217    Torrent * torrent;
218    NSDictionary * historyItem;
219    NSEnumerator * enumerator = [[fDefaults arrayForKey: @"History"] objectEnumerator];
220    while ((historyItem = [enumerator nextObject]))
221        if ((torrent = [[Torrent alloc] initWithHistory: historyItem lib: fLib]))
222        {
223            [fTorrents addObject: torrent];
224            [torrent release];
225        }
226   
227    //set sort
228    fSortType = [[fDefaults stringForKey: @"Sort"] retain];
229   
230    NSMenuItem * currentSortItem, * currentSortActionItem;
231    if ([fSortType isEqualToString: @"Name"])
232    {
233        currentSortItem = fNameSortItem;
234        currentSortActionItem = fNameSortActionItem;
235    }
236    else if ([fSortType isEqualToString: @"State"])
237    {
238        currentSortItem = fStateSortItem;
239        currentSortActionItem = fStateSortActionItem;
240    }
241    else if ([fSortType isEqualToString: @"Progress"])
242    {
243        currentSortItem = fProgressSortItem;
244        currentSortActionItem = fProgressSortActionItem;
245    }
246    else if ([fSortType isEqualToString: @"Date"])
247    {
248        currentSortItem = fDateSortItem;
249        currentSortActionItem = fDateSortActionItem;
250    }
251    else
252    {
253        currentSortItem = fOrderSortItem;
254        currentSortActionItem = fOrderSortActionItem;
255    }
256    [currentSortItem setState: NSOnState];
257    [currentSortActionItem setState: NSOnState];
258   
259    //set filter
260    fFilterType = [[fDefaults stringForKey: @"Filter"] retain];
261
262    BarButton * currentFilterButton;
263    if ([fFilterType isEqualToString: @"Pause"])
264        currentFilterButton = fPauseFilterButton;
265    else if ([fFilterType isEqualToString: @"Seed"])
266        currentFilterButton = fSeedFilterButton;
267    else if ([fFilterType isEqualToString: @"Download"])
268        currentFilterButton = fDownloadFilterButton;
269    else
270        currentFilterButton = fNoFilterButton;
271
272    [currentFilterButton setEnabled: YES];
273   
274    //set upload limit action button
275    [fUploadLimitItem setTitle: [NSString stringWithFormat: @"Limit (%d KB/s)",
276                    [fDefaults integerForKey: @"UploadLimit"]]];
277    if ([fDefaults boolForKey: @"CheckUpload"])
278        [fUploadLimitItem setState: NSOnState];
279    else
280        [fUploadNoLimitItem setState: NSOnState];
281
282        //set download limit action menu
283    [fDownloadLimitItem setTitle: [NSString stringWithFormat: @"Limit (%d KB/s)",
284                    [fDefaults integerForKey: @"DownloadLimit"]]];
285    if ([fDefaults boolForKey: @"CheckDownload"])
286        [fDownloadLimitItem setState: NSOnState];
287    else
288        [fDownloadNoLimitItem setState: NSOnState];
289   
290    //set ratio action menu
291    [fRatioSetItem setTitle: [NSString stringWithFormat: @"Stop at Ratio (%.2f)",
292                                [fDefaults floatForKey: @"RatioLimit"]]];
293    if ([fDefaults boolForKey: @"RatioCheck"])
294        [fRatioSetItem setState: NSOnState];
295    else
296        [fRatioNotSetItem setState: NSOnState];
297   
298    //observe notifications
299    NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
300   
301    [nc addObserver: self selector: @selector(torrentFinishedDownloading:)
302                    name: @"TorrentFinishedDownloading" object: nil];
303   
304    [nc addObserver: self selector: @selector(updateControlTint:)
305                    name: NSControlTintDidChangeNotification object: nil];
306   
307    [nc addObserver: self selector: @selector(prepareForUpdate:)
308                    name: SUUpdaterWillRestartNotification object: nil];
309    fUpdateInProgress = NO;
310   
311    [nc addObserver: self selector: @selector(limitGlobalChange:)
312                    name: @"LimitGlobalChange" object: nil];
313   
314    [nc addObserver: self selector: @selector(ratioGlobalChange:)
315                    name: @"RatioGlobalChange" object: nil];
316   
317    [nc addObserver: self selector: @selector(autoSpeedLimitChange:)
318                    name: @"AutoSpeedLimitChange" object: nil];
319   
320    [nc addObserver: self selector: @selector(autoImportChange:)
321                    name: @"AutoImportSettingChange" object: nil];
322   
323    [nc addObserver: self selector: @selector(setWindowSizeToFit)
324                    name: @"AutoSizeSettingChange" object: nil];
325   
326    //check to start another because of stopped torrent
327    [nc addObserver: self selector: @selector(checkWaitingForStopped:)
328                    name: @"StoppedDownloading" object: nil];
329   
330    //check all torrents for starting
331    [nc addObserver: self selector: @selector(globalStartSettingChange:)
332                    name: @"GlobalStartSettingChange" object: nil];
333
334    //check if torrent should now start
335    [nc addObserver: self selector: @selector(torrentStartSettingChange:)
336                    name: @"TorrentStartSettingChange" object: nil];
337   
338    //check if torrent should now start
339    [nc addObserver: self selector: @selector(torrentStoppedForRatio:)
340                    name: @"TorrentStoppedForRatio" object: nil];
341   
342    //change that just impacts the inspector
343    [nc addObserver: self selector: @selector(reloadInspectorSettings:)
344                    name: @"TorrentSettingChange" object: nil];
345   
346    //change that just impacts the dock badge
347    [nc addObserver: self selector: @selector(resetDockBadge:)
348                    name: @"DockBadgeChange" object: nil];
349
350    //timer to update the interface every second
351    fCompleted = 0;
352    [self updateUI: nil];
353    fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_UI_SECONDS target: self
354        selector: @selector(updateUI:) userInfo: nil repeats: YES];
355    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSModalPanelRunLoopMode];
356    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSEventTrackingRunLoopMode];
357   
358    [self applyFilter: nil];
359   
360    [fWindow makeKeyAndOrderFront: nil];
361
362    if ([fDefaults boolForKey: @"InfoVisible"])
363        [self showInfo: nil];
364   
365    //timer to auto toggle speed limit
366    [self autoSpeedLimitChange: nil];
367    fSpeedLimitTimer = [NSTimer scheduledTimerWithTimeInterval: AUTO_SPEED_LIMIT_SECONDS target: self
368        selector: @selector(autoSpeedLimit:) userInfo: nil repeats: YES];
369   
370    //timer to check for auto import every 15 seconds, must do after everything else is set up
371    fAutoImportTimer = [NSTimer scheduledTimerWithTimeInterval: AUTO_IMPORT_SECONDS target: self
372        selector: @selector(checkAutoImportDirectory:) userInfo: nil repeats: YES];
373    [[NSRunLoop currentRunLoop] addTimer: fAutoImportTimer forMode: NSDefaultRunLoopMode];
374}
375
376- (BOOL) applicationShouldHandleReopen: (NSApplication *) app hasVisibleWindows: (BOOL) visibleWindows
377{
378    if (![fWindow isVisible] && ![[fPrefsController window] isVisible])
379        [fWindow makeKeyAndOrderFront: nil];
380    return NO;
381}
382
383- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) sender
384{
385    if (!fUpdateInProgress && [fDefaults boolForKey: @"CheckQuit"])
386    {
387        int active = 0, downloading = 0;
388        Torrent * torrent;
389        NSEnumerator * enumerator = [fTorrents objectEnumerator];
390        while ((torrent = [enumerator nextObject]))
391            if ([torrent isActive])
392            {
393                active++;
394                if (![torrent isSeeding])
395                    downloading++;
396            }
397
398        if ([fDefaults boolForKey: @"CheckQuitDownloading"] ? downloading > 0 : active > 0)
399        {
400            NSString * message = active == 1
401                ? @"There is an active transfer. Do you really want to quit?"
402                : [NSString stringWithFormat:
403                    @"There are %d active transfers. Do you really want to quit?", active];
404
405            NSBeginAlertSheet(@"Confirm Quit", @"Quit", @"Cancel", nil, fWindow, self,
406                                @selector(quitSheetDidEnd:returnCode:contextInfo:), nil, nil, message);
407            return NSTerminateLater;
408        }
409    }
410
411    return NSTerminateNow;
412}
413
414- (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo
415{
416    [NSApp replyToApplicationShouldTerminate: returnCode == NSAlertDefaultReturn];
417}
418
419- (void) applicationWillTerminate: (NSNotification *) notification
420{
421    //stop timers
422    [fAutoImportTimer invalidate];
423    [fSpeedLimitTimer invalidate];
424    [fTimer invalidate];
425   
426    //save history and stop running torrents
427    [self updateTorrentHistory];
428    [fTorrents makeObjectsPerformSelector: @selector(stopTransferForQuit)];
429   
430    //remember window states and close all windows
431    [fDefaults setBool: [[fInfoController window] isVisible] forKey: @"InfoVisible"];
432    [[NSApp windows] makeObjectsPerformSelector: @selector(close)];
433    [self showStatusBar: NO animate: NO];
434    [self showFilterBar: NO animate: NO];
435   
436    //clear badge
437    [fBadger clearBadge];
438
439    //end quickly if the app is updating
440    if (fUpdateInProgress)
441        return;
442
443    //wait for running transfers to stop (5 second timeout)
444    NSDate * start = [NSDate date];
445    BOOL timeUp = NO;
446   
447    NSEnumerator * enumerator = [fTorrents objectEnumerator];
448    Torrent * torrent;
449    while (!timeUp && (torrent = [enumerator nextObject]))
450        while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
451        {
452            usleep(100000);
453            [torrent update];
454        }
455}
456
457- (NSArray *) torrentsAtIndexes: (NSIndexSet *) indexSet
458{
459    if ([fDisplayedTorrents respondsToSelector: @selector(objectsAtIndexes:)])
460        return [fDisplayedTorrents objectsAtIndexes: indexSet];
461    else
462    {
463        NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [indexSet count]];
464        unsigned int i;
465        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
466            [torrents addObject: [fDisplayedTorrents objectAtIndex: i]];
467
468        return torrents;
469    }
470}
471
472- (void) application: (NSApplication *) sender openFiles: (NSArray *) filenames
473{
474    [self openFiles: filenames ignoreDownloadFolder: NO];
475}
476
477- (void) openFiles: (NSArray *) filenames ignoreDownloadFolder: (BOOL) ignore
478{
479    NSString * downloadChoice = [fDefaults stringForKey: @"DownloadChoice"], * torrentPath;
480   
481    if (ignore || [downloadChoice isEqualToString: @"Ask"])
482    {
483        [self openFilesAsk: [filenames mutableCopy]];
484        return;
485    }
486   
487    Torrent * torrent;
488    NSEnumerator * enumerator = [filenames objectEnumerator];
489    while ((torrentPath = [enumerator nextObject]))
490    {
491        if (!(torrent = [[Torrent alloc] initWithPath: torrentPath lib: fLib]))
492            continue;
493
494        //add it to the "File > Open Recent" menu
495        [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
496
497        NSString * folder = [downloadChoice isEqualToString: @"Constant"]
498            ? [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath]
499            : [torrentPath stringByDeletingLastPathComponent];
500       
501        [torrent setDownloadFolder: folder];
502        [torrent update];
503        [self attemptToStartAuto: torrent];
504       
505        [fTorrents addObject: torrent];
506        [torrent release];
507    }
508
509    [self updateUI: nil];
510    [self applyFilter: nil];
511   
512    [self updateTorrentHistory];
513}
514
515//called by the main open method to show sheet for choosing download location
516- (void) openFilesAsk: (NSMutableArray *) files
517{
518    NSString * torrentPath;
519    Torrent * torrent;
520    do
521    {
522        if ([files count] == 0) //recursive base case
523        {
524            [files release];
525           
526            [self updateTorrentHistory];
527            return;
528        }
529   
530        torrentPath = [files objectAtIndex: 0];
531        torrent = [[Torrent alloc] initWithPath: torrentPath lib: fLib];
532       
533        [files removeObjectAtIndex: 0];
534    } while (!torrent);
535
536    //add it to the "File > Open Recent" menu
537    [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
538
539    NSOpenPanel * panel = [NSOpenPanel openPanel];
540
541    [panel setPrompt: @"Select"];
542    [panel setAllowsMultipleSelection: NO];
543    [panel setCanChooseFiles: NO];
544    [panel setCanChooseDirectories: YES];
545
546    [panel setMessage: [NSString stringWithFormat: @"Select the download folder for \"%@\"", [torrent name]]];
547   
548    NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:
549                                    torrent, @"Torrent", files, @"Files", nil];
550
551    [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
552            didEndSelector: @selector(folderChoiceClosed:returnCode:contextInfo:) contextInfo: dictionary];
553}
554
555- (void) folderChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code
556    contextInfo: (NSDictionary *) dictionary
557{
558    Torrent * torrent = [dictionary objectForKey: @"Torrent"];
559
560    if (code == NSOKButton)
561    {
562        [torrent setDownloadFolder: [[openPanel filenames] objectAtIndex: 0]];
563        [torrent update];
564        [self attemptToStartAuto: torrent];
565       
566        [fTorrents addObject: torrent];
567       
568        [self updateUI: nil];
569        [self applyFilter: nil];
570    }
571   
572    [self performSelectorOnMainThread: @selector(openFilesAsk:) withObject: [dictionary objectForKey: @"Files"]
573                        waitUntilDone: NO];
574   
575    [torrent release];
576    [dictionary release];
577}
578
579//called on by applescript
580- (void) open: (NSArray *) files
581{
582    [self performSelectorOnMainThread: @selector(openFiles:) withObject: files waitUntilDone: NO];
583}
584
585- (void) openFiles: (NSArray *) filenames
586{
587    [self openFiles: filenames ignoreDownloadFolder: NO];
588}
589
590- (void) openShowSheet: (id) sender
591{
592    NSOpenPanel * panel = [NSOpenPanel openPanel];
593
594    [panel setAllowsMultipleSelection: YES];
595    [panel setCanChooseFiles: YES];
596    [panel setCanChooseDirectories: NO];
597
598    [panel beginSheetForDirectory: nil file: nil types: [NSArray arrayWithObject: @"torrent"]
599        modalForWindow: fWindow modalDelegate: self didEndSelector: @selector(openSheetClosed:returnCode:contextInfo:)
600        contextInfo: [NSNumber numberWithBool: sender == fOpenIgnoreDownloadFolder]];
601}
602
603- (void) openSheetClosed: (NSOpenPanel *) panel returnCode: (int) code contextInfo: (NSNumber *) ignore
604{
605    if (code == NSOKButton)
606    {
607        NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:
608                                        [panel filenames], @"Files", ignore, @"Ignore", nil];
609        [self performSelectorOnMainThread: @selector(openFromSheet:) withObject: dictionary waitUntilDone: NO];
610    }
611}
612
613- (void) openFromSheet: (NSDictionary *) dictionary
614{
615    [self openFiles: [dictionary objectForKey: @"Files"]
616        ignoreDownloadFolder: [[dictionary objectForKey: @"Ignore"] boolValue]];
617   
618    [dictionary release];
619}
620
621- (void) resumeSelectedTorrents: (id) sender
622{
623    [self resumeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]];
624}
625
626- (void) resumeAllTorrents: (id) sender
627{
628    [self resumeTorrents: fTorrents];
629}
630
631- (void) resumeWaitingTorrents: (id) sender
632{
633    NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [fTorrents count]];
634   
635    NSEnumerator * enumerator = [fTorrents objectEnumerator];
636    Torrent * torrent;
637    while ((torrent = [enumerator nextObject]))
638        if ([torrent waitingToStart])
639            [torrents addObject: torrent];
640   
641    [self resumeTorrents: torrents];
642}
643
644- (void) resumeTorrents: (NSArray *) torrents
645{
646    [torrents makeObjectsPerformSelector: @selector(startTransfer)];
647   
648    [self updateUI: nil];
649    [self applyFilter: nil];
650    [fInfoController updateInfoStatsAndSettings];
651    [self updateTorrentHistory];
652}
653
654- (void) stopSelectedTorrents: (id) sender
655{
656    [self stopTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]];
657}
658
659- (void) stopAllTorrents: (id) sender
660{
661    [self stopTorrents: fTorrents];
662}
663
664- (void) stopTorrents: (NSArray *) torrents
665{
666    //don't want any of these starting then stopping
667    NSEnumerator * enumerator = [torrents objectEnumerator];
668    Torrent * torrent;
669    while ((torrent = [enumerator nextObject]))
670        [torrent setWaitToStart: NO];
671
672    [torrents makeObjectsPerformSelector: @selector(stopTransfer)];
673   
674    [self updateUI: nil];
675    [self applyFilter: nil];
676    [fInfoController updateInfoStatsAndSettings];
677    [self updateTorrentHistory];
678}
679
680- (void) removeTorrents: (NSArray *) torrents
681        deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
682{
683    [torrents retain];
684    int active = 0, downloading = 0;
685
686    if ([fDefaults boolForKey: @"CheckRemove"])
687    {
688        Torrent * torrent;
689        NSEnumerator * enumerator = [torrents objectEnumerator];
690        while ((torrent = [enumerator nextObject]))
691            if ([torrent isActive])
692            {
693                active++;
694                if (![torrent isSeeding])
695                    downloading++;
696            }
697
698        if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? downloading > 0 : active > 0)
699        {
700            NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
701                torrents, @"Torrents",
702                [NSNumber numberWithBool: deleteData], @"DeleteData",
703                [NSNumber numberWithBool: deleteTorrent], @"DeleteTorrent", nil];
704
705            NSString * title, * message;
706           
707            int selected = [fTableView numberOfSelectedRows];
708            if (selected == 1)
709            {
710                title = [NSString stringWithFormat: @"Comfirm Removal of \"%@\"",
711                            [[fDisplayedTorrents objectAtIndex: [fTableView selectedRow]] name]];
712                message = @"This transfer is active."
713                            " Once removed, continuing the transfer will require the torrent file."
714                            " Do you really want to remove it?";
715            }
716            else
717            {
718                title = [NSString stringWithFormat: @"Comfirm Removal of %d Transfers", selected];
719                if (selected == active)
720                    message = [NSString stringWithFormat: @"There are %d active transfers.", active];
721                else
722                    message = [NSString stringWithFormat: @"There are %d transfers (%d active).", selected, active];
723                message = [message stringByAppendingString:
724                    @" Once removed, continuing the transfers will require the torrent files."
725                    " Do you really want to remove them?"];
726            }
727
728            NSBeginAlertSheet(title, @"Remove", @"Cancel", nil, fWindow, self,
729                nil, @selector(removeSheetDidEnd:returnCode:contextInfo:), dict, message);
730            return;
731        }
732    }
733   
734    [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
735}
736
737- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (NSDictionary *) dict
738{
739    NSArray * torrents = [dict objectForKey: @"Torrents"];
740    BOOL deleteData = [[dict objectForKey: @"DeleteData"] boolValue],
741        deleteTorrent = [[dict objectForKey: @"DeleteTorrent"] boolValue];
742    [dict release];
743   
744    if (returnCode == NSAlertDefaultReturn)
745        [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
746    else
747        [torrents release];
748}
749
750- (void) confirmRemoveTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
751{
752    //don't want any of these starting then stopping
753    NSEnumerator * enumerator = [torrents objectEnumerator];
754    Torrent * torrent;
755    while ((torrent = [enumerator nextObject]))
756        [torrent setWaitToStart: NO];
757
758    NSNumber * lowestOrderValue = [NSNumber numberWithInt: [torrents count]], * currentOrderValue;
759
760    enumerator = [torrents objectEnumerator];
761    while ((torrent = [enumerator nextObject]))
762    {
763        [torrent stopTransfer];
764
765        if (deleteData)
766            [torrent trashData];
767        if (deleteTorrent)
768            [torrent trashTorrent];
769       
770        //determine lowest order value
771        currentOrderValue = [torrent orderValue];
772        if ([lowestOrderValue compare: currentOrderValue] == NSOrderedDescending)
773            lowestOrderValue = currentOrderValue;
774
775        [torrent removeForever];
776       
777        [fTorrents removeObject: torrent];
778        [fDisplayedTorrents removeObject: torrent];
779    }
780    [torrents release];
781
782    //reset the order values if necessary
783    if ([lowestOrderValue intValue] < [fTorrents count])
784    {
785        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
786                                                @"orderValue" ascending: YES] autorelease];
787        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
788
789        NSArray * tempTorrents = [fTorrents sortedArrayUsingDescriptors: descriptors];
790        [descriptors release];
791
792        int i;
793        for (i = [lowestOrderValue intValue]; i < [tempTorrents count]; i++)
794            [[tempTorrents objectAtIndex: i] setOrderValue: i];
795    }
796   
797    [fTableView deselectAll: nil];
798   
799    [self updateUI: nil];
800    [self applyFilter: nil];
801   
802    [self updateTorrentHistory];
803}
804
805- (void) removeNoDelete: (id) sender
806{
807    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
808                deleteData: NO deleteTorrent: NO];
809}
810
811- (void) removeDeleteData: (id) sender
812{
813    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
814                deleteData: YES deleteTorrent: NO];
815}
816
817- (void) removeDeleteTorrent: (id) sender
818{
819    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
820                deleteData: NO deleteTorrent: YES];
821}
822
823- (void) removeDeleteDataAndTorrent: (id) sender
824{
825    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
826                deleteData: YES deleteTorrent: YES];
827}
828
829- (void) copyTorrentFile: (id) sender
830{
831    [self copyTorrentFileForTorrents: [[NSMutableArray alloc] initWithArray:
832            [self torrentsAtIndexes: [fTableView selectedRowIndexes]]]];
833}
834
835- (void) copyTorrentFileForTorrents: (NSMutableArray *) torrents
836{
837    if ([torrents count] == 0)
838    {
839        [torrents release];
840        return;
841    }
842
843    Torrent * torrent = [torrents objectAtIndex: 0];
844
845    //warn user if torrent file can't be found
846    if (![[NSFileManager defaultManager] fileExistsAtPath: [torrent torrentLocation]])
847    {
848        NSAlert * alert = [[NSAlert alloc] init];
849        [alert addButtonWithTitle: @"OK"];
850        [alert setMessageText: [NSString stringWithFormat: @"Copy of \"%@\" Cannot Be Created", [torrent name]]];
851        [alert setInformativeText: [NSString stringWithFormat:
852                @"The torrent file (%@) cannot be found.", [torrent torrentLocation]]];
853        [alert setAlertStyle: NSWarningAlertStyle];
854       
855        [alert runModal];
856       
857        [torrents removeObjectAtIndex: 0];
858        [self copyTorrentFileForTorrents: torrents];
859    }
860    else
861    {
862        NSSavePanel * panel = [NSSavePanel savePanel];
863        [panel setRequiredFileType: @"torrent"];
864        [panel setCanSelectHiddenExtension: YES];
865       
866        [panel beginSheetForDirectory: nil file: [torrent name] modalForWindow: fWindow modalDelegate: self
867            didEndSelector: @selector(saveTorrentCopySheetClosed:returnCode:contextInfo:) contextInfo: torrents];
868    }
869}
870
871- (void) saveTorrentCopySheetClosed: (NSSavePanel *) panel returnCode: (int) code contextInfo: (NSMutableArray *) torrents
872{
873    //if save successful, copy torrent to new location with name of data file
874    if (code == NSOKButton)
875        [[NSFileManager defaultManager] copyPath: [[torrents objectAtIndex: 0] torrentLocation]
876            toPath: [panel filename] handler: nil];
877   
878    [torrents removeObjectAtIndex: 0];
879    [self performSelectorOnMainThread: @selector(copyTorrentFileForTorrents:) withObject: torrents waitUntilDone: NO];
880}
881
882- (void) revealFile: (id) sender
883{
884    NSIndexSet * indexSet = [fTableView selectedRowIndexes];
885    unsigned int i;
886    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
887        [[fDisplayedTorrents objectAtIndex: i] revealData];
888}
889
890- (void) showPreferenceWindow: (id) sender
891{
892    NSWindow * window = [fPrefsController window];
893    if (![window isVisible])
894        [window center];
895
896    [window makeKeyAndOrderFront: nil];
897}
898
899- (void) showInfo: (id) sender
900{
901    if ([[fInfoController window] isVisible])
902        [fInfoController close];
903    else
904    {
905        [fInfoController updateInfoStats];
906        [[fInfoController window] orderFront: nil];
907    }
908}
909
910- (void) setInfoTab: (id) sender
911{
912    if (sender == fNextInfoTabItem)
913        [fInfoController setNextTab];
914    else
915        [fInfoController setPreviousTab];
916}
917
918- (void) showMessageWindow: (id) sender
919{
920    [fMessageController showWindow: nil];
921}
922
923- (void) updateControlTint: (NSNotification *) notification
924{
925    if (fSpeedLimitEnabled)
926        [fSpeedLimitButton setImage: [NSColor currentControlTint] == NSBlueControlTint
927                                        ? fSpeedLimitBlueImage : fSpeedLimitGraphiteImage];
928}
929
930- (void) updateUI: (NSTimer *) timer
931{
932    [fTorrents makeObjectsPerformSelector: @selector(update)];
933
934    //resort if necessary or just update the table
935    if ([fSortType isEqualToString: @"Progress"] || [fSortType isEqualToString: @"State"])
936        [self sortTorrents];
937    else
938        [fTableView reloadData];
939   
940    //update the global DL/UL rates
941    float downloadRate, uploadRate;
942    tr_torrentRates(fLib, & downloadRate, & uploadRate);
943    if (![fStatusBar isHidden])
944    {
945        [fTotalDLField setStringValue: [@"Total DL: " stringByAppendingString: [NSString stringForSpeed: downloadRate]]];
946        [fTotalULField setStringValue: [@"Total UL: " stringByAppendingString: [NSString stringForSpeed: uploadRate]]];
947    }
948
949    //update non-constant parts of info window
950    if ([[fInfoController window] isVisible])
951        [fInfoController updateInfoStats];
952
953    //badge dock
954    [fBadger updateBadgeWithCompleted: fCompleted uploadRate: uploadRate downloadRate: downloadRate];
955}
956
957- (void) torrentFinishedDownloading: (NSNotification *) notification
958{
959    Torrent * torrent = [notification object];
960
961    [self applyFilter: nil];
962    [self checkToStartWaiting: torrent];
963
964    if ([fDefaults boolForKey: @"PlayDownloadSound"])
965    {
966        NSSound * sound;
967        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"DownloadSound"]]))
968            [sound play];
969    }
970
971    [GrowlApplicationBridge notifyWithTitle: @"Download Complete" description: [torrent name]
972        notificationName: GROWL_DOWNLOAD_COMPLETE iconData: nil priority: 0 isSticky: NO clickContext: nil];
973
974    if (![fWindow isKeyWindow])
975        fCompleted++;
976}
977
978- (void) updateTorrentHistory
979{
980    NSMutableArray * history = [NSMutableArray arrayWithCapacity: [fTorrents count]];
981
982    NSEnumerator * enumerator = [fTorrents objectEnumerator];
983    Torrent * torrent;
984    while ((torrent = [enumerator nextObject]))
985        [history addObject: [torrent history]];
986
987    [fDefaults setObject: history forKey: @"History"];
988    [fDefaults synchronize];
989}
990
991- (void) sortTorrents
992{
993    //remember selected rows if needed
994    NSArray * selectedTorrents = nil;
995    int numSelected = [fTableView numberOfSelectedRows];
996    if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
997        selectedTorrents = [self torrentsAtIndexes: [fTableView selectedRowIndexes]];
998
999    [self sortTorrentsIgnoreSelected]; //actually sort
1000   
1001    //set selected rows if needed
1002    if (selectedTorrents)
1003    {
1004        Torrent * torrent;
1005        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1006        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1007        while ((torrent = [enumerator nextObject]))
1008            [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1009       
1010        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1011        [indexSet release];
1012    }
1013}
1014
1015//doesn't remember selected rows
1016- (void) sortTorrentsIgnoreSelected
1017{
1018    NSSortDescriptor * nameDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"name"
1019                            ascending: YES selector: @selector(caseInsensitiveCompare:)] autorelease],
1020                    * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"orderValue"
1021                                            ascending: YES] autorelease];
1022
1023    NSArray * descriptors;
1024    if ([fSortType isEqualToString: @"Name"])
1025        descriptors = [[NSArray alloc] initWithObjects: nameDescriptor, orderDescriptor, nil];
1026    else if ([fSortType isEqualToString: @"State"])
1027    {
1028        NSSortDescriptor * stateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1029                                                @"stateSortKey" ascending: NO] autorelease],
1030                        * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1031                                            @"progressSortKey" ascending: NO] autorelease];
1032       
1033        descriptors = [[NSArray alloc] initWithObjects: stateDescriptor, progressDescriptor,
1034                                                            nameDescriptor, orderDescriptor, nil];
1035    }
1036    else if ([fSortType isEqualToString: @"Progress"])
1037    {
1038        NSSortDescriptor * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1039                                            @"progressSortKey" ascending: YES] autorelease];
1040       
1041        descriptors = [[NSArray alloc] initWithObjects: progressDescriptor, nameDescriptor, orderDescriptor, nil];
1042    }
1043    else if ([fSortType isEqualToString: @"Date"])
1044    {
1045        NSSortDescriptor * dateDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"date" ascending: YES] autorelease];
1046   
1047        descriptors = [[NSArray alloc] initWithObjects: dateDescriptor, orderDescriptor, nil];
1048    }
1049    else
1050        descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1051
1052    [fDisplayedTorrents sortUsingDescriptors: descriptors];
1053    [descriptors release];
1054   
1055    [fTableView reloadData];
1056}
1057
1058- (void) setSort: (id) sender
1059{
1060    //get checked items
1061    NSMenuItem * prevSortItem, * prevSortActionItem;
1062    if ([fSortType isEqualToString: @"Name"])
1063    {
1064        prevSortItem = fNameSortItem;
1065        prevSortActionItem = fNameSortActionItem;
1066    }
1067    else if ([fSortType isEqualToString: @"State"])
1068    {
1069        prevSortItem = fStateSortItem;
1070        prevSortActionItem = fStateSortActionItem;
1071    }
1072    else if ([fSortType isEqualToString: @"Progress"])
1073    {
1074        prevSortItem = fProgressSortItem;
1075        prevSortActionItem = fProgressSortActionItem;
1076    }
1077    else if ([fSortType isEqualToString: @"Date"])
1078    {
1079        prevSortItem = fDateSortItem;
1080        prevSortActionItem = fDateSortActionItem;
1081    }
1082    else
1083    {
1084        prevSortItem = fOrderSortItem;
1085        prevSortActionItem = fOrderSortActionItem;
1086    }
1087   
1088    if (sender != prevSortItem && sender != prevSortActionItem)
1089    {
1090        [fSortType release];
1091       
1092        //get new items to check
1093        NSMenuItem * currentSortItem, * currentSortActionItem;
1094        if (sender == fNameSortItem || sender == fNameSortActionItem)
1095        {
1096            currentSortItem = fNameSortItem;
1097            currentSortActionItem = fNameSortActionItem;
1098            fSortType = [[NSString alloc] initWithString: @"Name"];
1099        }
1100        else if (sender == fStateSortItem || sender == fStateSortActionItem)
1101        {
1102            currentSortItem = fStateSortItem;
1103            currentSortActionItem = fStateSortActionItem;
1104            fSortType = [[NSString alloc] initWithString: @"State"];
1105        }
1106        else if (sender == fProgressSortItem || sender == fProgressSortActionItem)
1107        {
1108            currentSortItem = fProgressSortItem;
1109            currentSortActionItem = fProgressSortActionItem;
1110            fSortType = [[NSString alloc] initWithString: @"Progress"];
1111        }
1112        else if (sender == fDateSortItem || sender == fDateSortActionItem)
1113        {
1114            currentSortItem = fDateSortItem;
1115            currentSortActionItem = fDateSortActionItem;
1116            fSortType = [[NSString alloc] initWithString: @"Date"];
1117        }
1118        else
1119        {
1120            currentSortItem = fOrderSortItem;
1121            currentSortActionItem = fOrderSortActionItem;
1122            fSortType = [[NSString alloc] initWithString: @"Order"];
1123        }
1124   
1125        [prevSortItem setState: NSOffState];
1126        [prevSortActionItem setState: NSOffState];
1127        [currentSortItem setState: NSOnState];
1128        [currentSortActionItem setState: NSOnState];
1129       
1130        [fDefaults setObject: fSortType forKey: @"Sort"];
1131    }
1132
1133    [self sortTorrents];
1134}
1135
1136- (void) applyFilter: (id) sender
1137{
1138    //remember selected rows if needed
1139    NSArray * selectedTorrents = [fTableView numberOfSelectedRows] > 0
1140                ? [self torrentsAtIndexes: [fTableView selectedRowIndexes]] : nil;
1141
1142    NSMutableArray * tempTorrents = [[NSMutableArray alloc] initWithCapacity: [fTorrents count]];
1143
1144    BOOL filtering = YES;
1145    if ([fFilterType isEqualToString: @"Pause"])
1146    {
1147        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1148        Torrent * torrent;
1149        while ((torrent = [enumerator nextObject]))
1150            if (![torrent isActive])
1151                [tempTorrents addObject: torrent];
1152    }
1153    else if ([fFilterType isEqualToString: @"Seed"])
1154    {
1155        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1156        Torrent * torrent;
1157        while ((torrent = [enumerator nextObject]))
1158            if ([torrent isActive] && [torrent progress] >= 1.0)
1159                [tempTorrents addObject: torrent];
1160    }
1161    else if ([fFilterType isEqualToString: @"Download"])
1162    {
1163        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1164        Torrent * torrent;
1165        while ((torrent = [enumerator nextObject]))
1166            if ([torrent isActive] && [torrent progress] < 1.0)
1167                [tempTorrents addObject: torrent];
1168    }
1169    else
1170    {
1171        filtering = NO;
1172        [tempTorrents setArray: fTorrents];
1173    }
1174   
1175    NSString * searchString = [fSearchFilterField stringValue];
1176    if ([searchString length] > 0)
1177    {
1178        filtering = YES;
1179       
1180        int i;
1181        for (i = [tempTorrents count] - 1; i >= 0; i--)
1182            if ([[[tempTorrents objectAtIndex: i] name] rangeOfString: searchString
1183                                        options: NSCaseInsensitiveSearch].location == NSNotFound)
1184                [tempTorrents removeObjectAtIndex: i];
1185    }
1186   
1187    [fDisplayedTorrents setArray: tempTorrents];
1188    [tempTorrents release];
1189   
1190    [self sortTorrentsIgnoreSelected];
1191   
1192    //set selected rows if needed
1193    if (selectedTorrents)
1194    {
1195        Torrent * torrent;
1196        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1197        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1198        unsigned index;
1199        while ((torrent = [enumerator nextObject]))
1200            if ((index = [fDisplayedTorrents indexOfObject: torrent]) != NSNotFound)
1201                [indexSet addIndex: index];
1202       
1203        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1204        [indexSet release];
1205    }
1206   
1207    //set status bar torrent count text
1208    NSMutableString * totalTorrentsString = [NSMutableString stringWithString: @""];
1209    if (filtering)
1210        [totalTorrentsString appendFormat: @"%d/", [fDisplayedTorrents count]];
1211   
1212    int totalCount = [fTorrents count];
1213    [totalTorrentsString appendFormat: @"%d Transfer%s", totalCount, totalCount == 1 ? "" : "s"];
1214   
1215    [fTotalTorrentsField setStringValue: totalTorrentsString];
1216
1217    [self setWindowSizeToFit];
1218}
1219
1220//resets filter and sorts torrents
1221- (void) setFilter: (id) sender
1222{
1223    BarButton * prevFilterButton;
1224    if ([fFilterType isEqualToString: @"Pause"])
1225        prevFilterButton = fPauseFilterButton;
1226    else if ([fFilterType isEqualToString: @"Seed"])
1227        prevFilterButton = fSeedFilterButton;
1228    else if ([fFilterType isEqualToString: @"Download"])
1229        prevFilterButton = fDownloadFilterButton;
1230    else
1231        prevFilterButton = fNoFilterButton;
1232   
1233    if (sender != prevFilterButton)
1234    {
1235        [prevFilterButton setEnabled: NO];
1236        [sender setEnabled: YES];
1237
1238        [fFilterType release];
1239        if (sender == fDownloadFilterButton)
1240            fFilterType = [[NSString alloc] initWithString: @"Download"];
1241        else if (sender == fPauseFilterButton)
1242            fFilterType = [[NSString alloc] initWithString: @"Pause"];
1243        else if (sender == fSeedFilterButton)
1244            fFilterType = [[NSString alloc] initWithString: @"Seed"];
1245        else
1246            fFilterType = [[NSString alloc] initWithString: @"None"];
1247
1248        [fDefaults setObject: fFilterType forKey: @"Filter"];
1249    }
1250
1251    [self applyFilter: nil];
1252}
1253
1254- (void) toggleSpeedLimit: (id) sender
1255{
1256    fSpeedLimitEnabled = !fSpeedLimitEnabled;
1257    int state = fSpeedLimitEnabled ? NSOnState : NSOffState;
1258
1259    [fSpeedLimitItem setState: state];
1260    [fSpeedLimitDockItem setState: state];
1261   
1262    [fSpeedLimitButton setImage: !fSpeedLimitEnabled ? fSpeedLimitNormalImage
1263        : ([NSColor currentControlTint] == NSBlueControlTint ? fSpeedLimitBlueImage : fSpeedLimitGraphiteImage)];
1264   
1265    [fPrefsController enableSpeedLimit: fSpeedLimitEnabled];
1266}
1267
1268- (void) autoSpeedLimitChange: (NSNotification *) notification
1269{
1270    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
1271        return;
1272 
1273    int onHour = [fDefaults integerForKey: @"SpeedLimitAutoOnHour"],
1274        offHour = [fDefaults integerForKey: @"SpeedLimitAutoOffHour"];
1275   
1276    //check if should be on if within range
1277    BOOL shouldBeOn;
1278    int hour = [[NSCalendarDate calendarDate] hourOfDay];
1279    if (onHour < offHour)
1280        shouldBeOn = hour >= onHour && hour < offHour;
1281    else
1282        shouldBeOn = hour < offHour || hour >= onHour;
1283   
1284    if (fSpeedLimitEnabled != shouldBeOn)
1285        [self toggleSpeedLimit: nil];
1286}
1287
1288- (void) autoSpeedLimit: (NSTimer *) timer
1289{
1290    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
1291        return;
1292 
1293    //toggle if within first few seconds of hour
1294    NSCalendarDate * currentDate = [NSCalendarDate calendarDate];
1295    if ([currentDate minuteOfHour] == 0 && [currentDate secondOfMinute] < AUTO_SPEED_LIMIT_SECONDS
1296            && [currentDate hourOfDay] == (fSpeedLimitEnabled ? [fDefaults integerForKey: @"SpeedLimitAutoOffHour"]
1297                                            : [fDefaults integerForKey: @"SpeedLimitAutoOnHour"]))
1298    {
1299        [self toggleSpeedLimit: nil];
1300       
1301        [GrowlApplicationBridge notifyWithTitle: [@"Speed Limit Auto " stringByAppendingString:
1302            fSpeedLimitEnabled ? @"Enabled" : @"Disabled"] description: @"Bandwidth settings changed"
1303            notificationName: GROWL_AUTO_SPEED_LIMIT iconData: nil priority: 0 isSticky: NO clickContext: nil];
1304    }
1305}
1306
1307- (void) setLimitGlobalEnabled: (id) sender
1308{
1309    [fPrefsController setLimitEnabled: (sender == fUploadLimitItem || sender == fDownloadLimitItem)
1310        type: (sender == fUploadLimitItem || sender == fUploadNoLimitItem) ? @"Upload" : @"Download"];
1311}
1312
1313- (void) setQuickLimitGlobal: (id) sender
1314{
1315    [fPrefsController setQuickLimit: [[sender title] intValue]
1316        type: [sender menu] == fUploadMenu ? @"Upload" : @"Download"];
1317}
1318
1319- (void) limitGlobalChange: (NSNotification *) notification
1320{
1321    NSDictionary * dict = [notification object];
1322   
1323    NSMenuItem * limitItem, * noLimitItem;
1324    if ([[dict objectForKey: @"Type"] isEqualToString: @"Upload"])
1325    {
1326        limitItem = fUploadLimitItem;
1327        noLimitItem = fUploadNoLimitItem;
1328    }
1329    else
1330    {
1331        limitItem = fDownloadLimitItem;
1332        noLimitItem = fDownloadNoLimitItem;
1333    }
1334   
1335    BOOL enable = [[dict objectForKey: @"Enable"] boolValue];
1336    [limitItem setState: enable ? NSOnState : NSOffState];
1337    [noLimitItem setState: !enable ? NSOnState : NSOffState];
1338   
1339    [limitItem setTitle: [NSString stringWithFormat: @"Limit (%d KB/s)",
1340                            [[dict objectForKey: @"Limit"] intValue]]];
1341
1342    [dict release];
1343}
1344
1345- (void) setRatioGlobalEnabled: (id) sender
1346{
1347    [fPrefsController setRatioEnabled: sender == fRatioSetItem];
1348}
1349
1350- (void) setQuickRatioGlobal: (id) sender
1351{
1352    [fPrefsController setQuickRatio: [[sender title] floatValue]];
1353}
1354
1355- (void) ratioGlobalChange: (NSNotification *) notification
1356{
1357    NSDictionary * dict = [notification object];
1358   
1359    BOOL enable = [[dict objectForKey: @"Enable"] boolValue];
1360    [fRatioSetItem setState: enable ? NSOnState : NSOffState];
1361    [fRatioNotSetItem setState: !enable ? NSOnState : NSOffState];
1362   
1363    [fRatioSetItem setTitle: [NSString stringWithFormat: @"Stop at Ratio (%.2f)",
1364                            [[dict objectForKey: @"Ratio"] floatValue]]];
1365
1366    [dict release];
1367}
1368
1369- (void) checkWaitingForStopped: (NSNotification *) notification
1370{
1371    [self checkToStartWaiting: [notification object]];
1372   
1373    [self updateUI: nil];
1374    [self applyFilter: nil];
1375    [fInfoController updateInfoStatsAndSettings];
1376    [self updateTorrentHistory];
1377}
1378
1379- (void) checkToStartWaiting: (Torrent *) finishedTorrent
1380{
1381    //don't try to start a transfer if there should be none waiting
1382    if (![[fDefaults stringForKey: @"StartSetting"] isEqualToString: @"Wait"])
1383        return;
1384
1385    int desiredActive = [fDefaults integerForKey: @"WaitToStartNumber"];
1386   
1387    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1388    Torrent * torrent, * torrentToStart = nil;
1389    while ((torrent = [enumerator nextObject]))
1390    {
1391        //ignore the torrent just stopped
1392        if (torrent == finishedTorrent)
1393            continue;
1394   
1395        if ([torrent isActive])
1396        {
1397            if (![torrent isSeeding])
1398            {
1399                desiredActive--;
1400                if (desiredActive <= 0)
1401                    return;
1402            }
1403        }
1404        else
1405        {
1406            //use as next if it is waiting to start and either no previous or order value is lower
1407            if ([torrent waitingToStart] && (!torrentToStart
1408                || [[torrentToStart orderValue] compare: [torrent orderValue]] == NSOrderedDescending))
1409                torrentToStart = torrent;
1410        }
1411    }
1412   
1413    //since it hasn't returned, the queue amount has not been met
1414    if (torrentToStart)
1415    {
1416        [torrentToStart startTransfer];
1417       
1418        [self updateUI: nil];
1419        [self applyFilter: nil];
1420        [fInfoController updateInfoStatsAndSettings];
1421        [self updateTorrentHistory];
1422    }
1423}
1424
1425- (void) torrentStartSettingChange: (NSNotification *) notification
1426{
1427    [self attemptToStartMultipleAuto: [notification object]];
1428
1429    [self updateUI: nil];
1430    [self applyFilter: nil];
1431    [fInfoController updateInfoStatsAndSettings];
1432    [self updateTorrentHistory];
1433}
1434
1435- (void) globalStartSettingChange: (NSNotification *) notification
1436{
1437    [self attemptToStartMultipleAuto: fTorrents];
1438   
1439    [self updateUI: nil];
1440    [self applyFilter: nil];
1441    [fInfoController updateInfoStatsAndSettings];
1442    [self updateTorrentHistory];
1443}
1444
1445- (void) torrentStoppedForRatio: (NSNotification *) notification
1446{
1447    [self applyFilter: nil];
1448    [fInfoController updateInfoStatsAndSettings];
1449   
1450    if ([fDefaults boolForKey: @"PlaySeedingSound"])
1451    {
1452        NSSound * sound;
1453        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]]))
1454            [sound play];
1455    }
1456   
1457    [GrowlApplicationBridge notifyWithTitle: @"Seeding Complete" description: [[notification object] name]
1458        notificationName: GROWL_SEEDING_COMPLETE iconData: nil priority: 0 isSticky: NO clickContext: nil];
1459}
1460
1461- (void) attemptToStartAuto: (Torrent *) torrent
1462{
1463    [self attemptToStartMultipleAuto: [NSArray arrayWithObject: torrent]];
1464}
1465
1466//will try to start, taking into consideration the start preference
1467- (void) attemptToStartMultipleAuto: (NSArray *) torrents
1468{
1469    NSString * startSetting = [fDefaults stringForKey: @"StartSetting"];
1470    if ([startSetting isEqualToString: @"Start"])
1471    {
1472        NSEnumerator * enumerator = [torrents objectEnumerator];
1473        Torrent * torrent;
1474        while ((torrent = [enumerator nextObject]))
1475            if ([torrent waitingToStart])
1476                [torrent startTransfer];
1477       
1478        return;
1479    }
1480    else if (![startSetting isEqualToString: @"Wait"])
1481        return;
1482    else;
1483   
1484    //determine the number of downloads needed to start
1485    int desiredActive = [fDefaults integerForKey: @"WaitToStartNumber"];
1486           
1487    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1488    Torrent * torrent;
1489    while ((torrent = [enumerator nextObject]))
1490        if ([torrent isActive] && ![torrent isSeeding])
1491        {
1492            desiredActive--;
1493            if (desiredActive <= 0)
1494                break;
1495        }
1496   
1497    //sort torrents by order value
1498    NSArray * sortedTorrents;
1499    if ([torrents count] > 1 && desiredActive > 0)
1500    {
1501        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1502                                                    @"orderValue" ascending: YES] autorelease];
1503        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1504       
1505        sortedTorrents = [torrents sortedArrayUsingDescriptors: descriptors];
1506        [descriptors release];
1507    }
1508    else
1509        sortedTorrents = torrents;
1510
1511    enumerator = [sortedTorrents objectEnumerator];
1512    while ((torrent = [enumerator nextObject]))
1513    {
1514        if ([torrent waitingToStart])
1515        {
1516            if ([torrent progress] >= 1.0)
1517                [torrent startTransfer];
1518            else if (desiredActive > 0)
1519            {
1520                [torrent startTransfer];
1521                desiredActive--;
1522            }
1523            else
1524                continue;
1525           
1526            [torrent update];
1527        }
1528    }
1529}
1530
1531- (void) reloadInspectorSettings: (NSNotification *) notification
1532{
1533    [fInfoController updateInfoStatsAndSettings];
1534}
1535
1536- (void) checkAutoImportDirectory: (NSTimer *) timer
1537{
1538    if (![fDefaults boolForKey: @"AutoImport"])
1539        return;
1540       
1541    NSString * path = [[fDefaults stringForKey: @"AutoImportDirectory"] stringByExpandingTildeInPath];
1542   
1543    //if folder cannot be found or the contents hasn't changed simply give up
1544    NSArray * allFileNames;
1545    if (!(allFileNames = [[NSFileManager defaultManager] directoryContentsAtPath: path])
1546            || [allFileNames isEqualToArray: fAutoImportedNames])
1547        return;
1548
1549    //try to add files that haven't already been added
1550    NSMutableArray * newFileNames = [NSMutableArray arrayWithArray: allFileNames];
1551    [newFileNames removeObjectsInArray: fAutoImportedNames];
1552   
1553    //save the current list of files
1554    [fAutoImportedNames setArray: allFileNames];
1555   
1556    NSEnumerator * enumerator = [newFileNames objectEnumerator];
1557    NSString * file;
1558    unsigned oldCount;
1559    while ((file = [enumerator nextObject]))
1560        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1561        {
1562            oldCount = [fTorrents count];
1563            [self openFiles: [NSArray arrayWithObject: [path stringByAppendingPathComponent: file]]];
1564           
1565            //import only actually happened if the torrent array is larger
1566            if (oldCount < [fTorrents count])
1567                [GrowlApplicationBridge notifyWithTitle: @"Torrent File Auto Added" description: file
1568                    notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO clickContext: nil];
1569        }
1570}
1571
1572- (void) autoImportChange: (NSNotification *) notification
1573{
1574    [fAutoImportedNames removeAllObjects];
1575    [self checkAutoImportDirectory: nil];
1576}
1577
1578- (int) numberOfRowsInTableView: (NSTableView *) t
1579{
1580    return [fDisplayedTorrents count];
1581}
1582
1583- (void) tableView: (NSTableView *) t willDisplayCell: (id) cell
1584    forTableColumn: (NSTableColumn *) tableColumn row: (int) row
1585{
1586    [cell setTorrent: [fDisplayedTorrents objectAtIndex: row]];
1587}
1588
1589- (BOOL) tableView: (NSTableView *) tableView writeRowsWithIndexes: (NSIndexSet *) indexes
1590    toPasteboard: (NSPasteboard *) pasteboard
1591{
1592    //only allow reordering of rows if sorting by order with no filter
1593    if ([fSortType isEqualToString: @"Order"] && [fFilterType isEqualToString: @"None"]
1594            && [[fSearchFilterField stringValue] length] == 0)
1595    {
1596        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
1597        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexes]
1598                                forType: TORRENT_TABLE_VIEW_DATA_TYPE];
1599        return YES;
1600    }
1601    return NO;
1602}
1603
1604- (NSDragOperation) tableView: (NSTableView *) t validateDrop: (id <NSDraggingInfo>) info
1605    proposedRow: (int) row proposedDropOperation: (NSTableViewDropOperation) operation
1606{
1607    NSPasteboard * pasteboard = [info draggingPasteboard];
1608    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1609    {
1610        //check if any files to add have "torrent" as an extension
1611        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1612        NSString * file;
1613        while ((file = [enumerator nextObject]))
1614            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1615            {
1616                [fTableView setDropRow: -1 dropOperation: NSTableViewDropOn];
1617                return NSDragOperationGeneric;
1618            }
1619    }
1620    else if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
1621    {
1622        [fTableView setDropRow: row dropOperation: NSTableViewDropAbove];
1623        return NSDragOperationGeneric;
1624    }
1625    else
1626        return NSDragOperationNone;
1627}
1628
1629- (BOOL) tableView: (NSTableView *) t acceptDrop: (id <NSDraggingInfo>) info
1630    row: (int) newRow dropOperation: (NSTableViewDropOperation) operation
1631{
1632    NSPasteboard * pasteboard = [info draggingPasteboard];
1633    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1634    {
1635        //create an array of files with the "torrent" extension
1636        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
1637        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1638        NSString * file;
1639        while ((file = [enumerator nextObject]))
1640            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1641                [filesToOpen addObject: file];
1642   
1643        [self application: NSApp openFiles: filesToOpen];
1644        [filesToOpen release];
1645    }
1646    else
1647    {
1648        //remember selected rows if needed
1649        NSArray * selectedTorrents = nil;
1650        int numSelected = [fTableView numberOfSelectedRows];
1651        if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
1652            selectedTorrents = [self torrentsAtIndexes: [fTableView selectedRowIndexes]];
1653   
1654        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData:
1655                                [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
1656       
1657        //move torrent in array
1658        NSArray * movingTorrents = [[self torrentsAtIndexes: indexes] retain];
1659        [fDisplayedTorrents removeObjectsInArray: movingTorrents];
1660       
1661        //determine the insertion index now that transfers to move have been removed
1662        int i, decrease = 0;
1663        for (i = [indexes firstIndex]; i < newRow && i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
1664            decrease++;
1665       
1666        //insert objects at new location
1667        for (i = 0; i < [movingTorrents count]; i++)
1668            [fDisplayedTorrents insertObject: [movingTorrents objectAtIndex: i] atIndex: newRow - decrease + i];
1669       
1670        [movingTorrents release];
1671       
1672        //redo order values
1673        int low = [indexes firstIndex], high = [indexes lastIndex];
1674        if (newRow < low)
1675            low = newRow;
1676        else if (newRow > high + 1)
1677            high = newRow - 1;
1678        else;
1679       
1680        for (i = low; i <= high; i++)
1681            [[fDisplayedTorrents objectAtIndex: i] setOrderValue: i];
1682       
1683        [fTableView reloadData];
1684       
1685        //set selected rows if needed
1686        if (selectedTorrents)
1687        {
1688            Torrent * torrent;
1689            NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1690            NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1691            while ((torrent = [enumerator nextObject]))
1692                [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1693           
1694            [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1695            [indexSet release];
1696        }
1697    }
1698   
1699    return YES;
1700}
1701
1702- (void) tableViewSelectionDidChange: (NSNotification *) notification
1703{
1704    [fInfoController updateInfoForTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]];
1705}
1706
1707- (void) toggleSmallView: (id) sender
1708{
1709    BOOL makeSmall = ![fDefaults boolForKey: @"SmallView"];
1710   
1711    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
1712    [fSmallViewItem setState: makeSmall];
1713   
1714    [fDefaults setBool: makeSmall forKey: @"SmallView"];
1715   
1716    //window min height
1717    NSSize contentMinSize = [fWindow contentMinSize],
1718            contentSize = [[fWindow contentView] frame].size;
1719    contentMinSize.height = contentSize.height - [fScrollView frame].size.height
1720                            + [fTableView rowHeight] + [fTableView intercellSpacing].height;
1721    [fWindow setContentMinSize: contentMinSize];
1722   
1723    //resize for larger min height if not set to auto size
1724    if (![fDefaults boolForKey: @"AutoSize"])
1725    {
1726        if (!makeSmall && contentSize.height < contentMinSize.height)
1727        {
1728            NSRect frame = [fWindow frame];
1729            float heightChange = contentMinSize.height - contentSize.height;
1730            frame.size.height += heightChange;
1731            frame.origin.y -= heightChange;
1732           
1733            [fWindow setFrame: frame display: YES];
1734            [fTableView reloadData];
1735        }
1736    }
1737    else
1738        [self setWindowSizeToFit];
1739}
1740
1741- (void) toggleStatusBar: (id) sender
1742{
1743    [self showStatusBar: [fStatusBar isHidden] animate: YES];
1744    [fDefaults setBool: ![fStatusBar isHidden] forKey: @"StatusBar"];
1745}
1746
1747- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
1748{
1749    if (show != [fStatusBar isHidden])
1750        return;
1751
1752    if (show)
1753        [fStatusBar setHidden: NO];
1754
1755    NSRect frame = [fWindow frame];
1756    float heightChange = [fStatusBar frame].size.height;
1757    if (!show)
1758        heightChange *= -1;
1759
1760    frame.size.height += heightChange;
1761    frame.origin.y -= heightChange;
1762   
1763    [self updateUI: nil];
1764   
1765    //set views to not autoresize
1766    unsigned int statsMask = [fStatusBar autoresizingMask];
1767    unsigned int filterMask = [fFilterBar autoresizingMask];
1768    unsigned int scrollMask = [fScrollView autoresizingMask];
1769    [fStatusBar setAutoresizingMask: 0];
1770    [fFilterBar setAutoresizingMask: 0];
1771    [fScrollView setAutoresizingMask: 0];
1772   
1773    [fWindow setFrame: frame display: YES animate: animate];
1774   
1775    //re-enable autoresize
1776    [fStatusBar setAutoresizingMask: statsMask];
1777    [fFilterBar setAutoresizingMask: filterMask];
1778    [fScrollView setAutoresizingMask: scrollMask];
1779   
1780    //change min size
1781    NSSize minSize = [fWindow contentMinSize];
1782    minSize.height += heightChange;
1783    [fWindow setContentMinSize: minSize];
1784   
1785    if (!show)
1786        [fStatusBar setHidden: YES];
1787}
1788
1789- (void) toggleFilterBar: (id) sender
1790{
1791    //disable filtering when hiding
1792    if (![fFilterBar isHidden])
1793    {
1794        [fSearchFilterField setStringValue: @""];
1795        [self setFilter: fNoFilterButton];
1796    }
1797
1798    [self showFilterBar: [fFilterBar isHidden] animate: YES];
1799    [fDefaults setBool: ![fFilterBar isHidden] forKey: @"FilterBar"];
1800}
1801
1802- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
1803{
1804    if (show != [fFilterBar isHidden])
1805        return;
1806
1807    if (show)
1808        [fFilterBar setHidden: NO];
1809
1810    NSRect frame = [fWindow frame];
1811    float heightChange = [fFilterBar frame].size.height;
1812    if (!show)
1813        heightChange *= -1;
1814
1815    frame.size.height += heightChange;
1816    frame.origin.y -= heightChange;
1817   
1818    //set views to not autoresize
1819    unsigned int filterMask = [fFilterBar autoresizingMask];
1820    unsigned int scrollMask = [fScrollView autoresizingMask];
1821    [fFilterBar setAutoresizingMask: 0];
1822    [fScrollView setAutoresizingMask: 0];
1823   
1824    [fWindow setFrame: frame display: YES animate: animate];
1825   
1826    //re-enable autoresize
1827    [fFilterBar setAutoresizingMask: filterMask];
1828    [fScrollView setAutoresizingMask: scrollMask];
1829   
1830    //change min size
1831    NSSize minSize = [fWindow contentMinSize];
1832    minSize.height += heightChange;
1833    [fWindow setContentMinSize: minSize];
1834   
1835    if (!show)
1836    {
1837        [fFilterBar setHidden: YES];
1838        [fWindow makeFirstResponder: fTableView];
1839    }
1840}
1841
1842- (void) toggleAdvancedBar: (id) sender
1843{
1844    int state = ![fAdvancedBarItem state];
1845    [fAdvancedBarItem setState: state];
1846    [fDefaults setBool: state forKey: @"UseAdvancedBar"];
1847   
1848    [fTableView display];
1849}
1850
1851- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier:
1852    (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
1853{
1854    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
1855
1856    if ([ident isEqualToString: TOOLBAR_OPEN])
1857    {
1858        [item setLabel: @"Open"];
1859        [item setPaletteLabel: @"Open Torrent Files"];
1860        [item setToolTip: @"Open torrent files"];
1861        [item setImage: [NSImage imageNamed: @"Open.png"]];
1862        [item setTarget: self];
1863        [item setAction: @selector(openShowSheet:)];
1864    }
1865    else if ([ident isEqualToString: TOOLBAR_REMOVE])
1866    {
1867        [item setLabel: @"Remove"];
1868        [item setPaletteLabel: @"Remove Selected"];
1869        [item setToolTip: @"Remove selected transfers"];
1870        [item setImage: [NSImage imageNamed: @"Remove.png"]];
1871        [item setTarget: self];
1872        [item setAction: @selector(removeNoDelete:)];
1873    }
1874    else if ([ident isEqualToString: TOOLBAR_INFO])
1875    {
1876        [item setLabel: @"Inspector"];
1877        [item setPaletteLabel: @"Toggle Inspector"];
1878        [item setToolTip: @"Toggle the torrent inspector"];
1879        [item setImage: [NSImage imageNamed: @"Info.png"]];
1880        [item setTarget: self];
1881        [item setAction: @selector(showInfo:)];
1882    }
1883    else if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
1884    {
1885        [item setLabel: @"Pause All"];
1886        [item setPaletteLabel: [item label]];
1887        [item setToolTip: @"Pause all transfers"];
1888        [item setImage: [NSImage imageNamed: @"PauseAll.png"]];
1889        [item setTarget: self];
1890        [item setAction: @selector(stopAllTorrents:)];
1891    }
1892    else if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
1893    {
1894        [item setLabel: @"Resume All"];
1895        [item setPaletteLabel: [item label]];
1896        [item setToolTip: @"Resume all transfers"];
1897        [item setImage: [NSImage imageNamed: @"ResumeAll.png"]];
1898        [item setTarget: self];
1899        [item setAction: @selector(resumeAllTorrents:)];
1900    }
1901    else if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
1902    {
1903        [item setLabel: @"Pause"];
1904        [item setPaletteLabel: @"Pause Selected"];
1905        [item setToolTip: @"Pause selected transfers"];
1906        [item setImage: [NSImage imageNamed: @"PauseSelected.png"]];
1907        [item setTarget: self];
1908        [item setAction: @selector(stopSelectedTorrents:)];
1909    }
1910    else if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
1911    {
1912        [item setLabel: @"Resume"];
1913        [item setPaletteLabel: @"Resume Selected"];
1914        [item setToolTip: @"Resume selected transfers"];
1915        [item setImage: [NSImage imageNamed: @"ResumeSelected.png"]];
1916        [item setTarget: self];
1917        [item setAction: @selector(resumeSelectedTorrents:)];
1918    }
1919    else if ([ident isEqualToString: TOOLBAR_FILTER])
1920    {
1921        [item setLabel: @"Filter"];
1922        [item setPaletteLabel: @"Toggle Filter"];
1923        [item setToolTip: @"Toggle the filter bar"];
1924        [item setImage: [NSImage imageNamed: @"Filter.png"]];
1925        [item setTarget: self];
1926        [item setAction: @selector(toggleFilterBar:)];
1927    }
1928    else
1929    {
1930        [item release];
1931        return nil;
1932    }
1933
1934    return item;
1935}
1936
1937- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t
1938{
1939    return [NSArray arrayWithObjects:
1940            TOOLBAR_OPEN, TOOLBAR_REMOVE,
1941            TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED,
1942            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, TOOLBAR_FILTER, TOOLBAR_INFO,
1943            NSToolbarSeparatorItemIdentifier,
1944            NSToolbarSpaceItemIdentifier,
1945            NSToolbarFlexibleSpaceItemIdentifier,
1946            NSToolbarCustomizeToolbarItemIdentifier, nil];
1947}
1948
1949- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t
1950{
1951    return [NSArray arrayWithObjects:
1952            TOOLBAR_OPEN, TOOLBAR_REMOVE,
1953            NSToolbarSeparatorItemIdentifier,
1954            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
1955            NSToolbarFlexibleSpaceItemIdentifier,
1956            TOOLBAR_FILTER, TOOLBAR_INFO, nil];
1957}
1958
1959- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
1960{
1961    NSString * ident = [toolbarItem itemIdentifier];
1962
1963    //enable remove item
1964    if ([ident isEqualToString: TOOLBAR_REMOVE])
1965        return [fTableView numberOfSelectedRows] > 0;
1966
1967    //enable pause all item
1968    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
1969    {
1970        Torrent * torrent;
1971        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1972        while ((torrent = [enumerator nextObject]))
1973            if ([torrent isActive])
1974                return YES;
1975        return NO;
1976    }
1977
1978    //enable resume all item
1979    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
1980    {
1981        Torrent * torrent;
1982        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1983        while ((torrent = [enumerator nextObject]))
1984            if ([torrent isPaused])
1985                return YES;
1986        return NO;
1987    }
1988
1989    //enable pause item
1990    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
1991    {
1992        Torrent * torrent;
1993        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1994        unsigned int i;
1995       
1996        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1997            if ([[fDisplayedTorrents objectAtIndex: i] isActive])
1998                return YES;
1999        return NO;
2000    }
2001   
2002    //enable resume item
2003    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2004    {
2005        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2006        unsigned int i;
2007       
2008        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2009            if ([[fDisplayedTorrents objectAtIndex: i] isPaused])
2010                return YES;
2011        return NO;
2012    }
2013
2014    return YES;
2015}
2016
2017- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
2018{
2019    SEL action = [menuItem action];
2020
2021    //only enable some items if it is in a context menu or the window is useable
2022    BOOL canUseMenu = [fWindow isKeyWindow] || [[[menuItem menu] title] isEqualToString: @"Context"];
2023
2024    //enable open items
2025    if (action == @selector(openShowSheet:))
2026        return [fWindow attachedSheet] == nil;
2027
2028    //enable show info
2029    if (action == @selector(showInfo:))
2030    {
2031        NSString * title = [[fInfoController window] isVisible] ? @"Hide Inspector" : @"Show Inspector";
2032        if (![[menuItem title] isEqualToString: title])
2033                [menuItem setTitle: title];
2034
2035        return YES;
2036    }
2037   
2038    //enable prev/next inspector tab
2039    if (action == @selector(setInfoTab:))
2040        return [[fInfoController window] isVisible];
2041   
2042    //enable toggle status bar
2043    if (action == @selector(toggleStatusBar:))
2044    {
2045        NSString * title = [fStatusBar isHidden] ? @"Show Status Bar" : @"Hide Status Bar";
2046        if (![[menuItem title] isEqualToString: title])
2047                [menuItem setTitle: title];
2048
2049        return canUseMenu;
2050    }
2051   
2052    //enable toggle filter bar
2053    if (action == @selector(toggleFilterBar:))
2054    {
2055        NSString * title = [fFilterBar isHidden] ? @"Show Filter Bar" : @"Hide Filter Bar";
2056        if (![[menuItem title] isEqualToString: title])
2057                [menuItem setTitle: title];
2058
2059        return canUseMenu;
2060    }
2061
2062    //enable reveal in finder
2063    if (action == @selector(revealFile:))
2064        return canUseMenu && [fTableView numberOfSelectedRows] > 0;
2065
2066    //enable remove items
2067    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
2068        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteDataAndTorrent:))
2069    {
2070        BOOL warning = NO,
2071            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
2072            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteDataAndTorrent:);
2073        Torrent * torrent;
2074        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2075        unsigned int i;
2076       
2077        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2078        {
2079            torrent = [fDisplayedTorrents objectAtIndex: i];
2080            if (!warning && [torrent isActive])
2081            {
2082                warning = onlyDownloading ? ![torrent isSeeding] : YES;
2083                if (warning && canDelete)
2084                    break;
2085            }
2086            if (!canDelete && [torrent publicTorrent])
2087            {
2088                canDelete = YES;
2089                if (warning)
2090                    break;
2091            }
2092        }
2093   
2094        //append or remove ellipsis when needed
2095        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
2096        if (warning && [fDefaults boolForKey: @"CheckRemove"])
2097        {
2098            if (![title hasSuffix: ellipsis])
2099                [menuItem setTitle: [title stringByAppendingEllipsis]];
2100        }
2101        else
2102        {
2103            if ([title hasSuffix: ellipsis])
2104                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
2105        }
2106       
2107        return canUseMenu && canDelete && [fTableView numberOfSelectedRows] > 0;
2108    }
2109
2110    //enable pause all item
2111    if (action == @selector(stopAllTorrents:))
2112    {
2113        Torrent * torrent;
2114        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2115        while ((torrent = [enumerator nextObject]))
2116            if ([torrent isActive])
2117                return YES;
2118        return NO;
2119    }
2120   
2121    //enable resume all item
2122    if (action == @selector(resumeAllTorrents:))
2123    {
2124        Torrent * torrent;
2125        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2126        while ((torrent = [enumerator nextObject]))
2127            if ([torrent isPaused])
2128                return YES;
2129        return NO;
2130    }
2131   
2132    //enable resume waiting item
2133    if (action == @selector(resumeWaitingTorrents:))
2134    {
2135        if (![[fDefaults stringForKey: @"StartSetting"] isEqualToString: @"Wait"])
2136            return NO;
2137   
2138        Torrent * torrent;
2139        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2140        while ((torrent = [enumerator nextObject]))
2141            if ([torrent waitingToStart])
2142                return YES;
2143        return NO;
2144    }
2145
2146    //enable pause item
2147    if (action == @selector(stopSelectedTorrents:))
2148    {
2149        if (!canUseMenu)
2150            return NO;
2151   
2152        Torrent * torrent;
2153        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2154        unsigned int i;
2155       
2156        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2157        {
2158            torrent = [fDisplayedTorrents objectAtIndex: i];
2159            if ([torrent isActive])
2160                return YES;
2161        }
2162        return NO;
2163    }
2164   
2165    //enable resume item
2166    if (action == @selector(resumeSelectedTorrents:))
2167    {
2168        if (!canUseMenu)
2169            return NO;
2170   
2171        Torrent * torrent;
2172        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2173        unsigned int i;
2174       
2175        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2176        {
2177            torrent = [fDisplayedTorrents objectAtIndex: i];
2178            if ([torrent isPaused])
2179                return YES;
2180        }
2181        return NO;
2182    }
2183   
2184    //enable sort and advanced bar items
2185    if (action == @selector(setSort:) || action == @selector(toggleAdvancedBar:)
2186            || action == @selector(toggleSmallView:))
2187        return canUseMenu;
2188   
2189    //enable copy torrent file item
2190    if (action == @selector(copyTorrentFile:))
2191        return canUseMenu && [fTableView numberOfSelectedRows] > 0;
2192
2193    return YES;
2194}
2195
2196- (void) sleepCallBack: (natural_t) messageType argument: (void *) messageArgument
2197{
2198    NSEnumerator * enumerator;
2199    Torrent * torrent;
2200    BOOL active;
2201
2202    switch (messageType)
2203    {
2204        case kIOMessageSystemWillSleep:
2205            //close all connections before going to sleep and remember we should resume when we wake up
2206            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
2207
2208            //wait for running transfers to stop (5 second timeout)
2209            NSDate * start = [NSDate date];
2210            BOOL timeUp = NO;
2211           
2212            NSEnumerator * enumerator = [fTorrents objectEnumerator];
2213            Torrent * torrent;
2214            while (!timeUp && (torrent = [enumerator nextObject]))
2215                while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
2216                {
2217                    usleep(100000);
2218                    [torrent update];
2219                }
2220
2221            IOAllowPowerChange(fRootPort, (long) messageArgument);
2222            break;
2223
2224        case kIOMessageCanSystemSleep:
2225            //pevent idle sleep unless all paused
2226            active = NO;
2227            enumerator = [fTorrents objectEnumerator];
2228            while ((torrent = [enumerator nextObject]))
2229                if ([torrent isActive])
2230                {
2231                    active = YES;
2232                    break;
2233                }
2234
2235            if (active)
2236                IOCancelPowerChange(fRootPort, (long) messageArgument);
2237            else
2238                IOAllowPowerChange(fRootPort, (long) messageArgument);
2239            break;
2240
2241        case kIOMessageSystemHasPoweredOn:
2242            //resume sleeping transfers after we wake up
2243            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
2244            break;
2245    }
2246}
2247
2248- (void) resetDockBadge: (NSNotification *) notification
2249{
2250    float downloadRate, uploadRate;
2251    tr_torrentRates(fLib, & downloadRate, & uploadRate);
2252   
2253    [fBadger updateBadgeWithCompleted: fCompleted uploadRate: uploadRate downloadRate: downloadRate];
2254}
2255
2256- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
2257{
2258    //if auto size is enabled, the current frame shouldn't need to change
2259    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
2260   
2261    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
2262    return frame;
2263}
2264
2265- (void) setWindowSizeToFit
2266{
2267    if ([fDefaults boolForKey: @"AutoSize"])
2268        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
2269}
2270
2271- (NSRect) sizedWindowFrame
2272{
2273    NSRect frame = [fWindow frame];
2274    float newHeight = frame.size.height - [fScrollView frame].size.height
2275        + [fDisplayedTorrents count] * ([fTableView rowHeight] + [fTableView intercellSpacing].height);
2276
2277    float minHeight = [fWindow minSize].height;
2278    if (newHeight < minHeight)
2279        newHeight = minHeight;
2280    else
2281    {
2282        float maxHeight = [[fWindow screen] visibleFrame].size.height;
2283        if ([fStatusBar isHidden])
2284            maxHeight -= [fStatusBar frame].size.height;
2285        if ([fFilterBar isHidden])
2286            maxHeight -= [fFilterBar frame].size.height;
2287       
2288        if (newHeight > maxHeight)
2289            newHeight = maxHeight;
2290    }
2291
2292    frame.origin.y -= (newHeight - frame.size.height);
2293    frame.size.height = newHeight;
2294    return frame;
2295}
2296
2297- (void) showMainWindow: (id) sender
2298{
2299    [fWindow makeKeyAndOrderFront: nil];
2300}
2301
2302- (void) windowDidBecomeKey: (NSNotification *) notification
2303{
2304    //reset dock badge for completed
2305    if (fCompleted > 0)
2306    {
2307        fCompleted = 0;
2308        [self resetDockBadge: nil];
2309    }
2310}
2311
2312- (NSSize) windowWillResize: (NSWindow *) sender toSize: (NSSize) proposedFrameSize
2313{
2314    //only resize horizontally if autosize is enabled
2315    if ([fDefaults boolForKey: @"AutoSize"])
2316        proposedFrameSize.height = [fWindow frame].size.height;
2317    return proposedFrameSize;
2318}
2319
2320- (void) windowDidResize: (NSNotification *) notification
2321{
2322    //hide search filter if it overlaps filter buttons
2323    [fSearchFilterField setHidden: NSMaxX([fPauseFilterButton frame]) + 2.0 > [fSearchFilterField frame].origin.x];
2324}
2325
2326- (void) linkHomepage: (id) sender
2327{
2328    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
2329}
2330
2331- (void) linkForums: (id) sender
2332{
2333    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
2334}
2335
2336- (void) checkUpdate: (id) sender
2337{
2338    [fPrefsController checkUpdate];
2339}
2340
2341- (void) prepareForUpdate: (NSNotification *) notification
2342{
2343    fUpdateInProgress = YES;
2344}
2345
2346- (NSDictionary *) registrationDictionaryForGrowl
2347{
2348    NSArray * notifications = [NSArray arrayWithObjects: GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE,
2349                                                            GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT, nil];
2350    return [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL,
2351                                notifications, GROWL_NOTIFICATIONS_DEFAULT, nil];
2352}
2353
2354@end
Note: See TracBrowser for help on using the repository browser.