source: trunk/macosx/Controller.m @ 847

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

Use UKKQueue for auto import which gets rid of polling and makes the process instantaneous.

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