source: trunk/macosx/Controller.m @ 961

Last change on this file since 961 was 961, checked in by livings124, 16 years ago

Torrents that have errors aren't counted in the queue, and when a torrent encounters an error the next torrent in the queue will start (even though that torrent might still be active).

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