source: trunk/macosx/Controller.m @ 1009

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

if there's not enough room to allow the window to expand to show the status/filter bars, first make the window smaller.

  • Property svn:keywords set to Date Rev Author Id
File size: 82.9 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 1009 2006-10-15 19:24:08Z 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    //iterate through instead of all at once to ensure no conflicts
659    NSEnumerator * enumerator = [torrents objectEnumerator];
660    Torrent * torrent;
661    while ((torrent = [enumerator nextObject]))
662        [torrent startTransfer];
663   
664    [self updateUI: nil];
665    [self applyFilter: nil];
666    [self updateTorrentHistory];
667}
668
669- (void) stopSelectedTorrents: (id) sender
670{
671    [self stopTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]];
672}
673
674- (void) stopAllTorrents: (id) sender
675{
676    [self stopTorrents: fTorrents];
677}
678
679- (void) stopTorrents: (NSArray *) torrents
680{
681    //don't want any of these starting then stopping
682    NSEnumerator * enumerator = [torrents objectEnumerator];
683    Torrent * torrent;
684    while ((torrent = [enumerator nextObject]))
685        [torrent setWaitToStart: NO];
686
687    [torrents makeObjectsPerformSelector: @selector(stopTransfer)];
688   
689    [self updateUI: nil];
690    [self applyFilter: nil];
691    [self updateTorrentHistory];
692}
693
694- (void) removeTorrents: (NSArray *) torrents
695        deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
696{
697    [torrents retain];
698    int active = 0, downloading = 0;
699
700    if ([fDefaults boolForKey: @"CheckRemove"])
701    {
702        Torrent * torrent;
703        NSEnumerator * enumerator = [torrents objectEnumerator];
704        while ((torrent = [enumerator nextObject]))
705            if ([torrent isActive])
706            {
707                active++;
708                if (![torrent isSeeding])
709                    downloading++;
710            }
711
712        if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? downloading > 0 : active > 0)
713        {
714            NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
715                torrents, @"Torrents",
716                [NSNumber numberWithBool: deleteData], @"DeleteData",
717                [NSNumber numberWithBool: deleteTorrent], @"DeleteTorrent", nil];
718
719            NSString * title, * message;
720           
721            int selected = [fTableView numberOfSelectedRows];
722            if (selected == 1)
723            {
724                title = [NSString stringWithFormat: @"Comfirm Removal of \"%@\"",
725                            [[fDisplayedTorrents objectAtIndex: [fTableView selectedRow]] name]];
726                message = @"This transfer is active."
727                            " Once removed, continuing the transfer will require the torrent file."
728                            " Do you really want to remove it?";
729            }
730            else
731            {
732                title = [NSString stringWithFormat: @"Comfirm Removal of %d Transfers", selected];
733                if (selected == active)
734                    message = [NSString stringWithFormat: @"There are %d active transfers.", active];
735                else
736                    message = [NSString stringWithFormat: @"There are %d transfers (%d active).", selected, active];
737                message = [message stringByAppendingString:
738                    @" Once removed, continuing the transfers will require the torrent files."
739                    " Do you really want to remove them?"];
740            }
741
742            NSBeginAlertSheet(title, @"Remove", @"Cancel", nil, fWindow, self,
743                nil, @selector(removeSheetDidEnd:returnCode:contextInfo:), dict, message);
744            return;
745        }
746    }
747   
748    [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
749}
750
751- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (NSDictionary *) dict
752{
753    NSArray * torrents = [dict objectForKey: @"Torrents"];
754    BOOL deleteData = [[dict objectForKey: @"DeleteData"] boolValue],
755        deleteTorrent = [[dict objectForKey: @"DeleteTorrent"] boolValue];
756    [dict release];
757   
758    if (returnCode == NSAlertDefaultReturn)
759        [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
760    else
761        [torrents release];
762}
763
764- (void) confirmRemoveTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
765{
766    //don't want any of these starting then stopping
767    NSEnumerator * enumerator = [torrents objectEnumerator];
768    Torrent * torrent;
769    while ((torrent = [enumerator nextObject]))
770        [torrent setWaitToStart: NO];
771
772    NSNumber * lowestOrderValue = [NSNumber numberWithInt: [torrents count]], * currentOrderValue;
773
774    enumerator = [torrents objectEnumerator];
775    while ((torrent = [enumerator nextObject]))
776    {
777        [torrent stopTransfer];
778
779        if (deleteData)
780            [torrent trashData];
781        if (deleteTorrent)
782            [torrent trashTorrent];
783       
784        //determine lowest order value
785        currentOrderValue = [torrent orderValue];
786        if ([lowestOrderValue compare: currentOrderValue] == NSOrderedDescending)
787            lowestOrderValue = currentOrderValue;
788
789        [torrent removeForever];
790       
791        [fTorrents removeObject: torrent];
792        [fDisplayedTorrents removeObject: torrent];
793    }
794    [torrents release];
795
796    //reset the order values if necessary
797    if ([lowestOrderValue intValue] < [fTorrents count])
798    {
799        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
800                                                @"orderValue" ascending: YES] autorelease];
801        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
802
803        NSArray * tempTorrents = [fTorrents sortedArrayUsingDescriptors: descriptors];
804        [descriptors release];
805
806        int i;
807        for (i = [lowestOrderValue intValue]; i < [tempTorrents count]; i++)
808            [[tempTorrents objectAtIndex: i] setOrderValue: i];
809    }
810   
811    [fTableView deselectAll: nil];
812   
813    [self updateUI: nil];
814    [self applyFilter: nil];
815   
816    [self updateTorrentHistory];
817}
818
819- (void) removeNoDelete: (id) sender
820{
821    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
822                deleteData: NO deleteTorrent: NO];
823}
824
825- (void) removeDeleteData: (id) sender
826{
827    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
828                deleteData: YES deleteTorrent: NO];
829}
830
831- (void) removeDeleteTorrent: (id) sender
832{
833    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
834                deleteData: NO deleteTorrent: YES];
835}
836
837- (void) removeDeleteDataAndTorrent: (id) sender
838{
839    [self removeTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]
840                deleteData: YES deleteTorrent: YES];
841}
842
843- (void) copyTorrentFile: (id) sender
844{
845    [self copyTorrentFileForTorrents: [[NSMutableArray alloc] initWithArray:
846            [self torrentsAtIndexes: [fTableView selectedRowIndexes]]]];
847}
848
849- (void) copyTorrentFileForTorrents: (NSMutableArray *) torrents
850{
851    if ([torrents count] == 0)
852    {
853        [torrents release];
854        return;
855    }
856
857    Torrent * torrent = [torrents objectAtIndex: 0];
858
859    //warn user if torrent file can't be found
860    if (![[NSFileManager defaultManager] fileExistsAtPath: [torrent torrentLocation]])
861    {
862        NSAlert * alert = [[NSAlert alloc] init];
863        [alert addButtonWithTitle: @"OK"];
864        [alert setMessageText: [NSString stringWithFormat: @"Copy of \"%@\" Cannot Be Created", [torrent name]]];
865        [alert setInformativeText: [NSString stringWithFormat:
866                @"The torrent file (%@) cannot be found.", [torrent torrentLocation]]];
867        [alert setAlertStyle: NSWarningAlertStyle];
868       
869        [alert runModal];
870       
871        [torrents removeObjectAtIndex: 0];
872        [self copyTorrentFileForTorrents: torrents];
873    }
874    else
875    {
876        NSSavePanel * panel = [NSSavePanel savePanel];
877        [panel setRequiredFileType: @"torrent"];
878        [panel setCanSelectHiddenExtension: YES];
879       
880        [panel beginSheetForDirectory: nil file: [torrent name] modalForWindow: fWindow modalDelegate: self
881            didEndSelector: @selector(saveTorrentCopySheetClosed:returnCode:contextInfo:) contextInfo: torrents];
882    }
883}
884
885- (void) saveTorrentCopySheetClosed: (NSSavePanel *) panel returnCode: (int) code contextInfo: (NSMutableArray *) torrents
886{
887    //if save successful, copy torrent to new location with name of data file
888    if (code == NSOKButton)
889        [[NSFileManager defaultManager] copyPath: [[torrents objectAtIndex: 0] torrentLocation]
890            toPath: [panel filename] handler: nil];
891   
892    [torrents removeObjectAtIndex: 0];
893    [self performSelectorOnMainThread: @selector(copyTorrentFileForTorrents:) withObject: torrents waitUntilDone: NO];
894}
895
896- (void) revealFile: (id) sender
897{
898    NSIndexSet * indexSet = [fTableView selectedRowIndexes];
899    unsigned int i;
900    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
901        [[fDisplayedTorrents objectAtIndex: i] revealData];
902}
903
904- (void) showPreferenceWindow: (id) sender
905{
906    NSWindow * window = [fPrefsController window];
907    if (![window isVisible])
908        [window center];
909
910    [window makeKeyAndOrderFront: nil];
911}
912
913- (void) showInfo: (id) sender
914{
915    if ([[fInfoController window] isVisible])
916        [fInfoController close];
917    else
918    {
919        [fInfoController updateInfoStats];
920        [[fInfoController window] orderFront: nil];
921    }
922}
923
924- (void) setInfoTab: (id) sender
925{
926    if (sender == fNextInfoTabItem)
927        [fInfoController setNextTab];
928    else
929        [fInfoController setPreviousTab];
930}
931
932- (void) showMessageWindow: (id) sender
933{
934    [fMessageController showWindow: nil];
935}
936
937- (void) updateControlTint: (NSNotification *) notification
938{
939    if (fSpeedLimitEnabled)
940        [fSpeedLimitButton setImage: [NSColor currentControlTint] == NSBlueControlTint
941                                        ? fSpeedLimitBlueImage : fSpeedLimitGraphiteImage];
942}
943
944- (void) updateUI: (NSTimer *) timer
945{
946    [fTorrents makeObjectsPerformSelector: @selector(update)];
947
948    //resort if necessary or just update the table
949    if ([fSortType isEqualToString: @"Progress"] || [fSortType isEqualToString: @"State"])
950        [self sortTorrents];
951    else
952        [fTableView reloadData];
953   
954    //update the global DL/UL rates
955    float downloadRate, uploadRate;
956    tr_torrentRates(fLib, & downloadRate, & uploadRate);
957    if (![fStatusBar isHidden])
958    {
959        [fTotalDLField setStringValue: [@"Total DL: " stringByAppendingString: [NSString stringForSpeed: downloadRate]]];
960        [fTotalULField setStringValue: [@"Total UL: " stringByAppendingString: [NSString stringForSpeed: uploadRate]]];
961    }
962
963    //update non-constant parts of info window
964    if ([[fInfoController window] isVisible])
965        [fInfoController updateInfoStats];
966
967    //badge dock
968    [fBadger updateBadgeWithCompleted: fCompleted uploadRate: uploadRate downloadRate: downloadRate];
969}
970
971- (void) torrentFinishedDownloading: (NSNotification *) notification
972{
973    Torrent * torrent = [notification object];
974
975    [self applyFilter: nil];
976    [self checkToStartWaiting: torrent];
977
978    if ([fDefaults boolForKey: @"PlayDownloadSound"])
979    {
980        NSSound * sound;
981        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"DownloadSound"]]))
982            [sound play];
983    }
984
985    [GrowlApplicationBridge notifyWithTitle: @"Download Complete" description: [torrent name]
986        notificationName: GROWL_DOWNLOAD_COMPLETE iconData: nil priority: 0 isSticky: NO clickContext: nil];
987
988    if (![fWindow isKeyWindow])
989        fCompleted++;
990}
991
992- (void) updateTorrentHistory
993{
994    NSMutableArray * history = [NSMutableArray arrayWithCapacity: [fTorrents count]];
995
996    NSEnumerator * enumerator = [fTorrents objectEnumerator];
997    Torrent * torrent;
998    while ((torrent = [enumerator nextObject]))
999        [history addObject: [torrent history]];
1000
1001    [fDefaults setObject: history forKey: @"History"];
1002    [fDefaults synchronize];
1003}
1004
1005- (void) sortTorrents
1006{
1007    //remember selected rows if needed
1008    NSArray * selectedTorrents = nil;
1009    int numSelected = [fTableView numberOfSelectedRows];
1010    if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
1011        selectedTorrents = [self torrentsAtIndexes: [fTableView selectedRowIndexes]];
1012
1013    [self sortTorrentsIgnoreSelected]; //actually sort
1014   
1015    //set selected rows if needed
1016    if (selectedTorrents)
1017    {
1018        Torrent * torrent;
1019        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1020        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1021        while ((torrent = [enumerator nextObject]))
1022            [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1023       
1024        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1025        [indexSet release];
1026    }
1027}
1028
1029//doesn't remember selected rows
1030- (void) sortTorrentsIgnoreSelected
1031{
1032    NSSortDescriptor * nameDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"name"
1033                            ascending: YES selector: @selector(caseInsensitiveCompare:)] autorelease],
1034                    * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"orderValue"
1035                                            ascending: YES] autorelease];
1036
1037    NSArray * descriptors;
1038    if ([fSortType isEqualToString: @"Name"])
1039        descriptors = [[NSArray alloc] initWithObjects: nameDescriptor, orderDescriptor, nil];
1040    else if ([fSortType isEqualToString: @"State"])
1041    {
1042        NSSortDescriptor * stateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1043                                                @"stateSortKey" ascending: NO] autorelease],
1044                        * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1045                                            @"progressSortKey" ascending: NO] autorelease];
1046       
1047        descriptors = [[NSArray alloc] initWithObjects: stateDescriptor, progressDescriptor,
1048                                                            nameDescriptor, orderDescriptor, nil];
1049    }
1050    else if ([fSortType isEqualToString: @"Progress"])
1051    {
1052        NSSortDescriptor * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1053                                            @"progressSortKey" ascending: YES] autorelease];
1054       
1055        descriptors = [[NSArray alloc] initWithObjects: progressDescriptor, nameDescriptor, orderDescriptor, nil];
1056    }
1057    else if ([fSortType isEqualToString: @"Date"])
1058    {
1059        NSSortDescriptor * dateDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"date" ascending: YES] autorelease];
1060   
1061        descriptors = [[NSArray alloc] initWithObjects: dateDescriptor, orderDescriptor, nil];
1062    }
1063    else
1064        descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1065
1066    [fDisplayedTorrents sortUsingDescriptors: descriptors];
1067    [descriptors release];
1068   
1069    [fTableView reloadData];
1070}
1071
1072- (void) setSort: (id) sender
1073{
1074    //get checked items
1075    NSMenuItem * prevSortItem, * prevSortActionItem;
1076    if ([fSortType isEqualToString: @"Name"])
1077    {
1078        prevSortItem = fNameSortItem;
1079        prevSortActionItem = fNameSortActionItem;
1080    }
1081    else if ([fSortType isEqualToString: @"State"])
1082    {
1083        prevSortItem = fStateSortItem;
1084        prevSortActionItem = fStateSortActionItem;
1085    }
1086    else if ([fSortType isEqualToString: @"Progress"])
1087    {
1088        prevSortItem = fProgressSortItem;
1089        prevSortActionItem = fProgressSortActionItem;
1090    }
1091    else if ([fSortType isEqualToString: @"Date"])
1092    {
1093        prevSortItem = fDateSortItem;
1094        prevSortActionItem = fDateSortActionItem;
1095    }
1096    else
1097    {
1098        prevSortItem = fOrderSortItem;
1099        prevSortActionItem = fOrderSortActionItem;
1100    }
1101   
1102    if (sender != prevSortItem && sender != prevSortActionItem)
1103    {
1104        [fSortType release];
1105       
1106        //get new items to check
1107        NSMenuItem * currentSortItem, * currentSortActionItem;
1108        if (sender == fNameSortItem || sender == fNameSortActionItem)
1109        {
1110            currentSortItem = fNameSortItem;
1111            currentSortActionItem = fNameSortActionItem;
1112            fSortType = [[NSString alloc] initWithString: @"Name"];
1113        }
1114        else if (sender == fStateSortItem || sender == fStateSortActionItem)
1115        {
1116            currentSortItem = fStateSortItem;
1117            currentSortActionItem = fStateSortActionItem;
1118            fSortType = [[NSString alloc] initWithString: @"State"];
1119        }
1120        else if (sender == fProgressSortItem || sender == fProgressSortActionItem)
1121        {
1122            currentSortItem = fProgressSortItem;
1123            currentSortActionItem = fProgressSortActionItem;
1124            fSortType = [[NSString alloc] initWithString: @"Progress"];
1125        }
1126        else if (sender == fDateSortItem || sender == fDateSortActionItem)
1127        {
1128            currentSortItem = fDateSortItem;
1129            currentSortActionItem = fDateSortActionItem;
1130            fSortType = [[NSString alloc] initWithString: @"Date"];
1131        }
1132        else
1133        {
1134            currentSortItem = fOrderSortItem;
1135            currentSortActionItem = fOrderSortActionItem;
1136            fSortType = [[NSString alloc] initWithString: @"Order"];
1137        }
1138   
1139        [prevSortItem setState: NSOffState];
1140        [prevSortActionItem setState: NSOffState];
1141        [currentSortItem setState: NSOnState];
1142        [currentSortActionItem setState: NSOnState];
1143       
1144        [fDefaults setObject: fSortType forKey: @"Sort"];
1145    }
1146
1147    [self sortTorrents];
1148}
1149
1150- (void) applyFilter: (id) sender
1151{
1152    //remember selected rows if needed
1153    NSArray * selectedTorrents = [fTableView numberOfSelectedRows] > 0
1154                ? [self torrentsAtIndexes: [fTableView selectedRowIndexes]] : nil;
1155
1156    NSMutableArray * tempTorrents = [[NSMutableArray alloc] initWithCapacity: [fTorrents count]];
1157
1158    BOOL filtering = YES;
1159    if ([fFilterType isEqualToString: @"Pause"])
1160    {
1161        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1162        Torrent * torrent;
1163        while ((torrent = [enumerator nextObject]))
1164            if (![torrent isActive])
1165                [tempTorrents addObject: torrent];
1166    }
1167    else if ([fFilterType isEqualToString: @"Seed"])
1168    {
1169        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1170        Torrent * torrent;
1171        while ((torrent = [enumerator nextObject]))
1172            if ([torrent isActive] && [torrent progress] >= 1.0)
1173                [tempTorrents addObject: torrent];
1174    }
1175    else if ([fFilterType isEqualToString: @"Download"])
1176    {
1177        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1178        Torrent * torrent;
1179        while ((torrent = [enumerator nextObject]))
1180            if ([torrent isActive] && [torrent progress] < 1.0)
1181                [tempTorrents addObject: torrent];
1182    }
1183    else
1184    {
1185        filtering = NO;
1186        [tempTorrents setArray: fTorrents];
1187    }
1188   
1189    NSString * searchString = [fSearchFilterField stringValue];
1190    if ([searchString length] > 0)
1191    {
1192        filtering = YES;
1193       
1194        int i;
1195        for (i = [tempTorrents count] - 1; i >= 0; i--)
1196            if ([[[tempTorrents objectAtIndex: i] name] rangeOfString: searchString
1197                                        options: NSCaseInsensitiveSearch].location == NSNotFound)
1198                [tempTorrents removeObjectAtIndex: i];
1199    }
1200   
1201    [fDisplayedTorrents setArray: tempTorrents];
1202    [tempTorrents release];
1203   
1204    [self sortTorrentsIgnoreSelected];
1205   
1206    //set selected rows if needed
1207    if (selectedTorrents)
1208    {
1209        Torrent * torrent;
1210        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1211        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1212        unsigned index;
1213        while ((torrent = [enumerator nextObject]))
1214            if ((index = [fDisplayedTorrents indexOfObject: torrent]) != NSNotFound)
1215                [indexSet addIndex: index];
1216       
1217        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1218        [indexSet release];
1219    }
1220   
1221    //set status bar torrent count text
1222    NSMutableString * totalTorrentsString = [NSMutableString stringWithString: @""];
1223    if (filtering)
1224        [totalTorrentsString appendFormat: @"%d/", [fDisplayedTorrents count]];
1225   
1226    int totalCount = [fTorrents count];
1227    [totalTorrentsString appendFormat: @"%d Transfer%s", totalCount, totalCount == 1 ? "" : "s"];
1228   
1229    [fTotalTorrentsField setStringValue: totalTorrentsString];
1230
1231    [self setWindowSizeToFit];
1232}
1233
1234//resets filter and sorts torrents
1235- (void) setFilter: (id) sender
1236{
1237    BarButton * prevFilterButton;
1238    if ([fFilterType isEqualToString: @"Pause"])
1239        prevFilterButton = fPauseFilterButton;
1240    else if ([fFilterType isEqualToString: @"Seed"])
1241        prevFilterButton = fSeedFilterButton;
1242    else if ([fFilterType isEqualToString: @"Download"])
1243        prevFilterButton = fDownloadFilterButton;
1244    else
1245        prevFilterButton = fNoFilterButton;
1246   
1247    if (sender != prevFilterButton)
1248    {
1249        [prevFilterButton setEnabled: NO];
1250        [sender setEnabled: YES];
1251
1252        [fFilterType release];
1253        if (sender == fDownloadFilterButton)
1254            fFilterType = [[NSString alloc] initWithString: @"Download"];
1255        else if (sender == fPauseFilterButton)
1256            fFilterType = [[NSString alloc] initWithString: @"Pause"];
1257        else if (sender == fSeedFilterButton)
1258            fFilterType = [[NSString alloc] initWithString: @"Seed"];
1259        else
1260            fFilterType = [[NSString alloc] initWithString: @"None"];
1261
1262        [fDefaults setObject: fFilterType forKey: @"Filter"];
1263    }
1264
1265    [self applyFilter: nil];
1266}
1267
1268- (void) switchFilter: (id) sender
1269{
1270    NSButton * button;
1271    if ([fFilterType isEqualToString: @"None"])
1272        button = sender == fNextFilterItem ? fDownloadFilterButton : fPauseFilterButton;
1273    else if ([fFilterType isEqualToString: @"Download"])
1274        button = sender == fNextFilterItem ? fSeedFilterButton : fNoFilterButton;
1275    else if ([fFilterType isEqualToString: @"Seed"])
1276        button = sender == fNextFilterItem ? fPauseFilterButton : fDownloadFilterButton;
1277    else if ([fFilterType isEqualToString: @"Pause"])
1278        button = sender == fNextFilterItem ? fNoFilterButton : fSeedFilterButton;
1279    else
1280        button = fNoFilterButton;
1281   
1282    [self setFilter: button];
1283}
1284
1285- (void) toggleSpeedLimit: (id) sender
1286{
1287    fSpeedLimitEnabled = !fSpeedLimitEnabled;
1288    int state = fSpeedLimitEnabled ? NSOnState : NSOffState;
1289
1290    [fSpeedLimitItem setState: state];
1291    [fSpeedLimitDockItem setState: state];
1292   
1293    [fSpeedLimitButton setImage: !fSpeedLimitEnabled ? fSpeedLimitNormalImage
1294        : ([NSColor currentControlTint] == NSBlueControlTint ? fSpeedLimitBlueImage : fSpeedLimitGraphiteImage)];
1295   
1296    [fPrefsController enableSpeedLimit: fSpeedLimitEnabled];
1297}
1298
1299- (void) autoSpeedLimitChange: (NSNotification *) notification
1300{
1301    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
1302        return;
1303 
1304    int onHour = [fDefaults integerForKey: @"SpeedLimitAutoOnHour"],
1305        offHour = [fDefaults integerForKey: @"SpeedLimitAutoOffHour"];
1306   
1307    //check if should be on if within range
1308    BOOL shouldBeOn;
1309    int hour = [[NSCalendarDate calendarDate] hourOfDay];
1310    if (onHour < offHour)
1311        shouldBeOn = hour >= onHour && hour < offHour;
1312    else
1313        shouldBeOn = hour < offHour || hour >= onHour;
1314   
1315    if (fSpeedLimitEnabled != shouldBeOn)
1316        [self toggleSpeedLimit: nil];
1317}
1318
1319- (void) autoSpeedLimit: (NSTimer *) timer
1320{
1321    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
1322        return;
1323 
1324    //toggle if within first few seconds of hour
1325    NSCalendarDate * currentDate = [NSCalendarDate calendarDate];
1326    if ([currentDate minuteOfHour] == 0 && [currentDate secondOfMinute] < AUTO_SPEED_LIMIT_SECONDS
1327            && [currentDate hourOfDay] == (fSpeedLimitEnabled ? [fDefaults integerForKey: @"SpeedLimitAutoOffHour"]
1328                                            : [fDefaults integerForKey: @"SpeedLimitAutoOnHour"]))
1329    {
1330        [self toggleSpeedLimit: nil];
1331       
1332        [GrowlApplicationBridge notifyWithTitle: [@"Speed Limit Auto " stringByAppendingString:
1333            fSpeedLimitEnabled ? @"Enabled" : @"Disabled"] description: @"Bandwidth settings changed"
1334            notificationName: GROWL_AUTO_SPEED_LIMIT iconData: nil priority: 0 isSticky: NO clickContext: nil];
1335    }
1336}
1337
1338- (void) setLimitGlobalEnabled: (id) sender
1339{
1340    [fPrefsController setLimitEnabled: (sender == fUploadLimitItem || sender == fDownloadLimitItem)
1341        type: (sender == fUploadLimitItem || sender == fUploadNoLimitItem) ? @"Upload" : @"Download"];
1342}
1343
1344- (void) setQuickLimitGlobal: (id) sender
1345{
1346    [fPrefsController setQuickLimit: [[sender title] intValue]
1347        type: [sender menu] == fUploadMenu ? @"Upload" : @"Download"];
1348}
1349
1350- (void) limitGlobalChange: (NSNotification *) notification
1351{
1352    NSDictionary * dict = [notification object];
1353   
1354    NSMenuItem * limitItem, * noLimitItem;
1355    if ([[dict objectForKey: @"Type"] isEqualToString: @"Upload"])
1356    {
1357        limitItem = fUploadLimitItem;
1358        noLimitItem = fUploadNoLimitItem;
1359    }
1360    else
1361    {
1362        limitItem = fDownloadLimitItem;
1363        noLimitItem = fDownloadNoLimitItem;
1364    }
1365   
1366    BOOL enable = [[dict objectForKey: @"Enable"] boolValue];
1367    [limitItem setState: enable ? NSOnState : NSOffState];
1368    [noLimitItem setState: !enable ? NSOnState : NSOffState];
1369   
1370    [limitItem setTitle: [NSString stringWithFormat: @"Limit (%d KB/s)",
1371                            [[dict objectForKey: @"Limit"] intValue]]];
1372
1373    [dict release];
1374}
1375
1376- (void) setRatioGlobalEnabled: (id) sender
1377{
1378    [fPrefsController setRatioEnabled: sender == fRatioSetItem];
1379}
1380
1381- (void) setQuickRatioGlobal: (id) sender
1382{
1383    [fPrefsController setQuickRatio: [[sender title] floatValue]];
1384}
1385
1386- (void) ratioGlobalChange: (NSNotification *) notification
1387{
1388    NSDictionary * dict = [notification object];
1389   
1390    BOOL enable = [[dict objectForKey: @"Enable"] boolValue];
1391    [fRatioSetItem setState: enable ? NSOnState : NSOffState];
1392    [fRatioNotSetItem setState: !enable ? NSOnState : NSOffState];
1393   
1394    [fRatioSetItem setTitle: [NSString stringWithFormat: @"Stop at Ratio (%.2f)",
1395                            [[dict objectForKey: @"Ratio"] floatValue]]];
1396
1397    [dict release];
1398}
1399
1400- (void) checkWaitingForStopped: (NSNotification *) notification
1401{
1402    [self checkToStartWaiting: [notification object]];
1403   
1404    [self updateUI: nil];
1405    [self applyFilter: nil];
1406    [self updateTorrentHistory];
1407}
1408
1409- (void) checkToStartWaiting: (Torrent *) finishedTorrent
1410{
1411    //don't try to start a transfer if there should be none waiting
1412    if (![fDefaults boolForKey: @"Queue"])
1413        return;
1414
1415    int desiredActive = [fDefaults integerForKey: @"QueueDownloadNumber"];
1416   
1417    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1418    Torrent * torrent, * torrentToStart = nil;
1419    while ((torrent = [enumerator nextObject]))
1420    {
1421        //ignore the torrent just stopped
1422        if (torrent == finishedTorrent)
1423            continue;
1424   
1425        if ([torrent isActive])
1426        {
1427            if (![torrent isSeeding] && ![torrent isError])
1428            {
1429                desiredActive--;
1430                if (desiredActive <= 0)
1431                    return;
1432            }
1433        }
1434        else
1435        {
1436            //use as next if it is waiting to start and either no previous or order value is lower
1437            if ([torrent waitingToStart] && (!torrentToStart
1438                || [[torrentToStart orderValue] compare: [torrent orderValue]] == NSOrderedDescending))
1439                torrentToStart = torrent;
1440        }
1441    }
1442   
1443    //since it hasn't returned, the queue amount has not been met
1444    if (torrentToStart)
1445    {
1446        [torrentToStart startTransfer];
1447       
1448        [self updateUI: nil];
1449        [self applyFilter: nil];
1450        [self updateTorrentHistory];
1451    }
1452}
1453
1454- (void) torrentStartSettingChange: (NSNotification *) notification
1455{
1456    [self attemptToStartMultipleAuto: [notification object]];
1457
1458    [self updateUI: nil];
1459    [self applyFilter: nil];
1460    [self updateTorrentHistory];
1461}
1462
1463- (void) globalStartSettingChange: (NSNotification *) notification
1464{
1465    [self attemptToStartMultipleAuto: fTorrents];
1466   
1467    [self updateUI: nil];
1468    [self applyFilter: nil];
1469    [self updateTorrentHistory];
1470}
1471
1472- (void) torrentStoppedForRatio: (NSNotification *) notification
1473{
1474    [self applyFilter: nil];
1475    [fInfoController updateInfoStats];
1476    [fInfoController updateInfoSettings];
1477   
1478    if ([fDefaults boolForKey: @"PlaySeedingSound"])
1479    {
1480        NSSound * sound;
1481        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]]))
1482            [sound play];
1483    }
1484   
1485    [GrowlApplicationBridge notifyWithTitle: @"Seeding Complete" description: [[notification object] name]
1486        notificationName: GROWL_SEEDING_COMPLETE iconData: nil priority: 0 isSticky: NO clickContext: nil];
1487}
1488
1489- (void) attemptToStartAuto: (Torrent *) torrent
1490{
1491    [self attemptToStartMultipleAuto: [NSArray arrayWithObject: torrent]];
1492}
1493
1494//will try to start, taking into consideration the start preference
1495- (void) attemptToStartMultipleAuto: (NSArray *) torrents
1496{
1497    if (![fDefaults boolForKey: @"Queue"])
1498    {
1499        NSEnumerator * enumerator = [torrents objectEnumerator];
1500        Torrent * torrent;
1501        while ((torrent = [enumerator nextObject]))
1502            if ([torrent waitingToStart])
1503                [torrent startTransfer];
1504       
1505        return;
1506    }
1507   
1508    //determine the number of downloads needed to start
1509    int desiredActive = [fDefaults integerForKey: @"QueueDownloadNumber"];
1510           
1511    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1512    Torrent * torrent;
1513    while ((torrent = [enumerator nextObject]))
1514        if ([torrent isActive] && ![torrent isSeeding] && ![torrent isError])
1515        {
1516            desiredActive--;
1517            if (desiredActive <= 0)
1518                break;
1519        }
1520   
1521    //sort torrents by order value
1522    NSArray * sortedTorrents;
1523    if ([torrents count] > 1 && desiredActive > 0)
1524    {
1525        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1526                                                    @"orderValue" ascending: YES] autorelease];
1527        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1528       
1529        sortedTorrents = [torrents sortedArrayUsingDescriptors: descriptors];
1530        [descriptors release];
1531    }
1532    else
1533        sortedTorrents = torrents;
1534
1535    enumerator = [sortedTorrents objectEnumerator];
1536    while ((torrent = [enumerator nextObject]))
1537    {
1538        if ([torrent waitingToStart])
1539        {
1540            if ([torrent progress] >= 1.0)
1541                [torrent startTransfer];
1542            else if (desiredActive > 0)
1543            {
1544                [torrent startTransfer];
1545                desiredActive--;
1546            }
1547            else
1548                continue;
1549           
1550            [torrent update];
1551        }
1552    }
1553}
1554
1555-(void) watcher: (id<UKFileWatcher>) watcher receivedNotification: (NSString *) notification forPath: (NSString *) path
1556{
1557    if ([notification isEqualToString: UKFileWatcherWriteNotification])
1558        [self checkAutoImportDirectory];
1559}
1560
1561- (void) checkAutoImportDirectory
1562{
1563    if (![fDefaults boolForKey: @"AutoImport"])
1564        return;
1565       
1566    NSString * path = [[fDefaults stringForKey: @"AutoImportDirectory"] stringByExpandingTildeInPath];
1567   
1568    NSArray * importedNames;
1569    if (!(importedNames = [[NSFileManager defaultManager] directoryContentsAtPath: path]))
1570        return;
1571   
1572    NSEnumerator * enumerator = [importedNames objectEnumerator];
1573    NSString * file;
1574    unsigned oldCount;
1575    while ((file = [enumerator nextObject]))
1576        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1577        {
1578            oldCount = [fTorrents count];
1579            [self openFiles: [NSArray arrayWithObject: [path stringByAppendingPathComponent: file]]];
1580           
1581            //import only actually happened if the torrent array is larger
1582            if (oldCount < [fTorrents count])
1583                [GrowlApplicationBridge notifyWithTitle: @"Torrent File Auto Added" description: file
1584                    notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO clickContext: nil];
1585        }
1586}
1587
1588- (int) numberOfRowsInTableView: (NSTableView *) t
1589{
1590    return [fDisplayedTorrents count];
1591}
1592
1593- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (int) row
1594{
1595    return [[fDisplayedTorrents objectAtIndex: row] infoForCurrentView];
1596}
1597
1598- (BOOL) tableView: (NSTableView *) tableView writeRowsWithIndexes: (NSIndexSet *) indexes
1599    toPasteboard: (NSPasteboard *) pasteboard
1600{
1601    //only allow reordering of rows if sorting by order with no filter
1602    if ([fSortType isEqualToString: @"Order"] && [fFilterType isEqualToString: @"None"]
1603            && [[fSearchFilterField stringValue] length] == 0)
1604    {
1605        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
1606        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexes]
1607                                forType: TORRENT_TABLE_VIEW_DATA_TYPE];
1608        return YES;
1609    }
1610    return NO;
1611}
1612
1613- (NSDragOperation) tableView: (NSTableView *) t validateDrop: (id <NSDraggingInfo>) info
1614    proposedRow: (int) row proposedDropOperation: (NSTableViewDropOperation) operation
1615{
1616    NSPasteboard * pasteboard = [info draggingPasteboard];
1617    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1618    {
1619        //check if any files to add have "torrent" as an extension
1620        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1621        NSString * file;
1622        while ((file = [enumerator nextObject]))
1623            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1624            {
1625                [fTableView setDropRow: -1 dropOperation: NSTableViewDropOn];
1626                return NSDragOperationGeneric;
1627            }
1628    }
1629    else if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
1630    {
1631        [fTableView setDropRow: row dropOperation: NSTableViewDropAbove];
1632        return NSDragOperationGeneric;
1633    }
1634    else
1635        return NSDragOperationNone;
1636}
1637
1638- (BOOL) tableView: (NSTableView *) t acceptDrop: (id <NSDraggingInfo>) info
1639    row: (int) newRow dropOperation: (NSTableViewDropOperation) operation
1640{
1641    NSPasteboard * pasteboard = [info draggingPasteboard];
1642    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1643    {
1644        //create an array of files with the "torrent" extension
1645        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
1646        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1647        NSString * file;
1648        while ((file = [enumerator nextObject]))
1649            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1650                [filesToOpen addObject: file];
1651   
1652        [self application: NSApp openFiles: filesToOpen];
1653        [filesToOpen release];
1654    }
1655    else
1656    {
1657        //remember selected rows if needed
1658        NSArray * selectedTorrents = nil;
1659        int numSelected = [fTableView numberOfSelectedRows];
1660        if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
1661            selectedTorrents = [self torrentsAtIndexes: [fTableView selectedRowIndexes]];
1662   
1663        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData:
1664                                [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
1665       
1666        //move torrent in array
1667        NSArray * movingTorrents = [[self torrentsAtIndexes: indexes] retain];
1668        [fDisplayedTorrents removeObjectsInArray: movingTorrents];
1669       
1670        //determine the insertion index now that transfers to move have been removed
1671        int i, decrease = 0;
1672        for (i = [indexes firstIndex]; i < newRow && i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
1673            decrease++;
1674       
1675        //insert objects at new location
1676        for (i = 0; i < [movingTorrents count]; i++)
1677            [fDisplayedTorrents insertObject: [movingTorrents objectAtIndex: i] atIndex: newRow - decrease + i];
1678       
1679        [movingTorrents release];
1680       
1681        //redo order values
1682        int low = [indexes firstIndex], high = [indexes lastIndex];
1683        if (newRow < low)
1684            low = newRow;
1685        else if (newRow > high + 1)
1686            high = newRow - 1;
1687        else;
1688       
1689        for (i = low; i <= high; i++)
1690            [[fDisplayedTorrents objectAtIndex: i] setOrderValue: i];
1691       
1692        [fTableView reloadData];
1693       
1694        //set selected rows if needed
1695        if (selectedTorrents)
1696        {
1697            Torrent * torrent;
1698            NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1699            NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1700            while ((torrent = [enumerator nextObject]))
1701                [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1702           
1703            [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1704            [indexSet release];
1705        }
1706    }
1707   
1708    return YES;
1709}
1710
1711- (void) tableViewSelectionDidChange: (NSNotification *) notification
1712{
1713    [fInfoController updateInfoForTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]];
1714}
1715
1716- (void) toggleSmallView: (id) sender
1717{
1718    BOOL makeSmall = ![fDefaults boolForKey: @"SmallView"];
1719   
1720    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
1721    [fSmallViewItem setState: makeSmall];
1722   
1723    [fDefaults setBool: makeSmall forKey: @"SmallView"];
1724   
1725    //window min height
1726    NSSize contentMinSize = [fWindow contentMinSize],
1727            contentSize = [[fWindow contentView] frame].size;
1728    contentMinSize.height = contentSize.height - [fScrollView frame].size.height
1729                            + [fTableView rowHeight] + [fTableView intercellSpacing].height;
1730    [fWindow setContentMinSize: contentMinSize];
1731   
1732    //resize for larger min height if not set to auto size
1733    if (![fDefaults boolForKey: @"AutoSize"])
1734    {
1735        if (!makeSmall && contentSize.height < contentMinSize.height)
1736        {
1737            NSRect frame = [fWindow frame];
1738            float heightChange = contentMinSize.height - contentSize.height;
1739            frame.size.height += heightChange;
1740            frame.origin.y -= heightChange;
1741           
1742            [fWindow setFrame: frame display: YES];
1743            [fTableView reloadData];
1744        }
1745    }
1746    else
1747        [self setWindowSizeToFit];
1748}
1749
1750- (void) toggleStatusBar: (id) sender
1751{
1752    [self showStatusBar: [fStatusBar isHidden] animate: YES];
1753    [fDefaults setBool: ![fStatusBar isHidden] forKey: @"StatusBar"];
1754}
1755
1756- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
1757{
1758    if (show != [fStatusBar isHidden])
1759        return;
1760
1761    if (show)
1762        [fStatusBar setHidden: NO];
1763
1764    NSRect frame = [fWindow frame];
1765    float heightChange = [fStatusBar frame].size.height;
1766    if (!show)
1767        heightChange *= -1;
1768   
1769    //allow bar to show even if not enough room
1770    if (show && ![fDefaults boolForKey: @"AutoSize"])
1771    {
1772        float maxHeight = [[fWindow screen] visibleFrame].size.height - heightChange;
1773        if (frame.size.height > maxHeight)
1774        {
1775            float change = maxHeight - frame.size.height;
1776            frame.size.height += change;
1777            frame.origin.y -= change;
1778           
1779            [fWindow setFrame: frame display: NO animate: NO];
1780        }
1781    }
1782
1783    frame.size.height += heightChange;
1784    frame.origin.y -= heightChange;
1785   
1786    [self updateUI: nil];
1787   
1788    //set views to not autoresize
1789    unsigned int statsMask = [fStatusBar autoresizingMask];
1790    unsigned int filterMask = [fFilterBar autoresizingMask];
1791    unsigned int scrollMask = [fScrollView autoresizingMask];
1792    [fStatusBar setAutoresizingMask: 0];
1793    [fFilterBar setAutoresizingMask: 0];
1794    [fScrollView setAutoresizingMask: 0];
1795   
1796    [fWindow setFrame: frame display: YES animate: animate];
1797   
1798    //re-enable autoresize
1799    [fStatusBar setAutoresizingMask: statsMask];
1800    [fFilterBar setAutoresizingMask: filterMask];
1801    [fScrollView setAutoresizingMask: scrollMask];
1802   
1803    //change min size
1804    NSSize minSize = [fWindow contentMinSize];
1805    minSize.height += heightChange;
1806    [fWindow setContentMinSize: minSize];
1807   
1808    if (!show)
1809        [fStatusBar setHidden: YES];
1810}
1811
1812- (void) toggleFilterBar: (id) sender
1813{
1814    //disable filtering when hiding
1815    if (![fFilterBar isHidden])
1816    {
1817        [fSearchFilterField setStringValue: @""];
1818        [self setFilter: fNoFilterButton];
1819    }
1820
1821    [self showFilterBar: [fFilterBar isHidden] animate: YES];
1822    [fDefaults setBool: ![fFilterBar isHidden] forKey: @"FilterBar"];
1823}
1824
1825- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
1826{
1827    if (show != [fFilterBar isHidden])
1828        return;
1829
1830    if (show)
1831        [fFilterBar setHidden: NO];
1832
1833    NSRect frame = [fWindow frame];
1834    float heightChange = [fFilterBar frame].size.height;
1835    if (!show)
1836        heightChange *= -1;
1837   
1838    //allow bar to show even if not enough room
1839    if (show && ![fDefaults boolForKey: @"AutoSize"])
1840    {
1841        float maxHeight = [[fWindow screen] visibleFrame].size.height - heightChange;
1842        if (frame.size.height > maxHeight)
1843        {
1844            float change = maxHeight - frame.size.height;
1845            frame.size.height += change;
1846            frame.origin.y -= change;
1847           
1848            [fWindow setFrame: frame display: NO animate: NO];
1849        }
1850    }
1851
1852    frame.size.height += heightChange;
1853    frame.origin.y -= heightChange;
1854   
1855    //set views to not autoresize
1856    unsigned int filterMask = [fFilterBar autoresizingMask];
1857    unsigned int scrollMask = [fScrollView autoresizingMask];
1858    [fFilterBar setAutoresizingMask: 0];
1859    [fScrollView setAutoresizingMask: 0];
1860   
1861    [fWindow setFrame: frame display: YES animate: animate];
1862   
1863    //re-enable autoresize
1864    [fFilterBar setAutoresizingMask: filterMask];
1865    [fScrollView setAutoresizingMask: scrollMask];
1866   
1867    //change min size
1868    NSSize minSize = [fWindow contentMinSize];
1869    minSize.height += heightChange;
1870    [fWindow setContentMinSize: minSize];
1871   
1872    if (!show)
1873    {
1874        [fFilterBar setHidden: YES];
1875        [fWindow makeFirstResponder: fTableView];
1876    }
1877}
1878
1879- (void) toggleAdvancedBar: (id) sender
1880{
1881    int state = ![fAdvancedBarItem state];
1882    [fAdvancedBarItem setState: state];
1883    [fDefaults setBool: state forKey: @"UseAdvancedBar"];
1884   
1885    [fTableView display];
1886}
1887
1888- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier:
1889    (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
1890{
1891    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
1892
1893    if ([ident isEqualToString: TOOLBAR_OPEN])
1894    {
1895        [item setLabel: @"Open"];
1896        [item setPaletteLabel: @"Open Torrent Files"];
1897        [item setToolTip: @"Open torrent files"];
1898        [item setImage: [NSImage imageNamed: @"Open.png"]];
1899        [item setTarget: self];
1900        [item setAction: @selector(openShowSheet:)];
1901    }
1902    else if ([ident isEqualToString: TOOLBAR_REMOVE])
1903    {
1904        [item setLabel: @"Remove"];
1905        [item setPaletteLabel: @"Remove Selected"];
1906        [item setToolTip: @"Remove selected transfers"];
1907        [item setImage: [NSImage imageNamed: @"Remove.png"]];
1908        [item setTarget: self];
1909        [item setAction: @selector(removeNoDelete:)];
1910    }
1911    else if ([ident isEqualToString: TOOLBAR_INFO])
1912    {
1913        [item setLabel: @"Inspector"];
1914        [item setPaletteLabel: @"Toggle Inspector"];
1915        [item setToolTip: @"Toggle the torrent inspector"];
1916        [item setImage: [NSImage imageNamed: @"Info.png"]];
1917        [item setTarget: self];
1918        [item setAction: @selector(showInfo:)];
1919    }
1920    else if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
1921    {
1922        [item setLabel: @"Pause All"];
1923        [item setPaletteLabel: [item label]];
1924        [item setToolTip: @"Pause all transfers"];
1925        [item setImage: [NSImage imageNamed: @"PauseAll.png"]];
1926        [item setTarget: self];
1927        [item setAction: @selector(stopAllTorrents:)];
1928    }
1929    else if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
1930    {
1931        [item setLabel: @"Resume All"];
1932        [item setPaletteLabel: [item label]];
1933        [item setToolTip: @"Resume all transfers"];
1934        [item setImage: [NSImage imageNamed: @"ResumeAll.png"]];
1935        [item setTarget: self];
1936        [item setAction: @selector(resumeAllTorrents:)];
1937    }
1938    else if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
1939    {
1940        [item setLabel: @"Pause"];
1941        [item setPaletteLabel: @"Pause Selected"];
1942        [item setToolTip: @"Pause selected transfers"];
1943        [item setImage: [NSImage imageNamed: @"PauseSelected.png"]];
1944        [item setTarget: self];
1945        [item setAction: @selector(stopSelectedTorrents:)];
1946    }
1947    else if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
1948    {
1949        [item setLabel: @"Resume"];
1950        [item setPaletteLabel: @"Resume Selected"];
1951        [item setToolTip: @"Resume selected transfers"];
1952        [item setImage: [NSImage imageNamed: @"ResumeSelected.png"]];
1953        [item setTarget: self];
1954        [item setAction: @selector(resumeSelectedTorrents:)];
1955    }
1956    else if ([ident isEqualToString: TOOLBAR_FILTER])
1957    {
1958        [item setLabel: @"Filter"];
1959        [item setPaletteLabel: @"Toggle Filter"];
1960        [item setToolTip: @"Toggle the filter bar"];
1961        [item setImage: [NSImage imageNamed: @"Filter.png"]];
1962        [item setTarget: self];
1963        [item setAction: @selector(toggleFilterBar:)];
1964    }
1965    else
1966    {
1967        [item release];
1968        return nil;
1969    }
1970
1971    return item;
1972}
1973
1974- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t
1975{
1976    return [NSArray arrayWithObjects:
1977            TOOLBAR_OPEN, TOOLBAR_REMOVE,
1978            TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED,
1979            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, TOOLBAR_FILTER, TOOLBAR_INFO,
1980            NSToolbarSeparatorItemIdentifier,
1981            NSToolbarSpaceItemIdentifier,
1982            NSToolbarFlexibleSpaceItemIdentifier,
1983            NSToolbarCustomizeToolbarItemIdentifier, nil];
1984}
1985
1986- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t
1987{
1988    return [NSArray arrayWithObjects:
1989            TOOLBAR_OPEN, TOOLBAR_REMOVE,
1990            NSToolbarSeparatorItemIdentifier,
1991            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
1992            NSToolbarFlexibleSpaceItemIdentifier,
1993            TOOLBAR_FILTER, TOOLBAR_INFO, nil];
1994}
1995
1996- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
1997{
1998    NSString * ident = [toolbarItem itemIdentifier];
1999
2000    //enable remove item
2001    if ([ident isEqualToString: TOOLBAR_REMOVE])
2002        return [fTableView numberOfSelectedRows] > 0;
2003
2004    //enable pause all item
2005    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
2006    {
2007        Torrent * torrent;
2008        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2009        while ((torrent = [enumerator nextObject]))
2010            if ([torrent isActive] || [torrent waitingToStart])
2011                return YES;
2012        return NO;
2013    }
2014
2015    //enable resume all item
2016    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
2017    {
2018        Torrent * torrent;
2019        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2020        while ((torrent = [enumerator nextObject]))
2021            if ([torrent isPaused] && ![torrent waitingToStart])
2022                return YES;
2023        return NO;
2024    }
2025
2026    //enable pause item
2027    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
2028    {
2029        Torrent * torrent;
2030        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2031        unsigned int i;
2032       
2033        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2034        {
2035            torrent = [fDisplayedTorrents objectAtIndex: i];
2036            if ([torrent isActive] || [torrent waitingToStart])
2037                return YES;
2038        }
2039        return NO;
2040    }
2041   
2042    //enable resume item
2043    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2044    {
2045        Torrent * torrent;
2046        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2047        unsigned int i;
2048       
2049        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2050        {
2051            torrent = [fDisplayedTorrents objectAtIndex: i];
2052            if ([torrent isPaused] && ![torrent waitingToStart])
2053                return YES;
2054        }
2055        return NO;
2056    }
2057
2058    return YES;
2059}
2060
2061- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
2062{
2063    SEL action = [menuItem action];
2064
2065    //only enable some items if it is in a context menu or the window is useable
2066    BOOL canUseTable = [fWindow isKeyWindow] || [[[menuItem menu] title] isEqualToString: @"Context"];
2067
2068    //enable open items
2069    if (action == @selector(openShowSheet:))
2070        return [fWindow attachedSheet] == nil;
2071   
2072    //enable sort and advanced bar items
2073    if (action == @selector(setSort:) || action == @selector(toggleAdvancedBar:) || action == @selector(toggleSmallView:))
2074        return [fWindow isVisible];
2075
2076    //enable show info
2077    if (action == @selector(showInfo:))
2078    {
2079        NSString * title = [[fInfoController window] isVisible] ? @"Hide Inspector" : @"Show Inspector";
2080        if (![[menuItem title] isEqualToString: title])
2081                [menuItem setTitle: title];
2082
2083        return YES;
2084    }
2085   
2086    //enable prev/next inspector tab
2087    if (action == @selector(setInfoTab:))
2088        return [[fInfoController window] isVisible];
2089   
2090    //enable toggle status bar
2091    if (action == @selector(toggleStatusBar:))
2092    {
2093        NSString * title = [fStatusBar isHidden] ? @"Show Status Bar" : @"Hide Status Bar";
2094        if (![[menuItem title] isEqualToString: title])
2095            [menuItem setTitle: title];
2096
2097        return [fWindow isVisible];
2098    }
2099   
2100    //enable toggle filter bar
2101    if (action == @selector(toggleFilterBar:))
2102    {
2103        NSString * title = [fFilterBar isHidden] ? @"Show Filter Bar" : @"Hide Filter Bar";
2104        if (![[menuItem title] isEqualToString: title])
2105            [menuItem setTitle: title];
2106
2107        return [fWindow isVisible];
2108    }
2109   
2110    //enable prev/next filter button
2111    if (action == @selector(switchFilter:))
2112        return [fWindow isVisible] && ![fFilterBar isHidden];
2113
2114    //enable reveal in finder
2115    if (action == @selector(revealFile:))
2116        return canUseTable && [fTableView numberOfSelectedRows] > 0;
2117
2118    //enable remove items
2119    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
2120        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteDataAndTorrent:))
2121    {
2122        BOOL warning = NO,
2123            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
2124            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteDataAndTorrent:);
2125        Torrent * torrent;
2126        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2127        unsigned int i;
2128       
2129        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2130        {
2131            torrent = [fDisplayedTorrents objectAtIndex: i];
2132            if (!warning && [torrent isActive])
2133            {
2134                warning = onlyDownloading ? ![torrent isSeeding] : YES;
2135                if (warning && canDelete)
2136                    break;
2137            }
2138            if (!canDelete && [torrent publicTorrent])
2139            {
2140                canDelete = YES;
2141                if (warning)
2142                    break;
2143            }
2144        }
2145   
2146        //append or remove ellipsis when needed
2147        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
2148        if (warning && [fDefaults boolForKey: @"CheckRemove"])
2149        {
2150            if (![title hasSuffix: ellipsis])
2151                [menuItem setTitle: [title stringByAppendingEllipsis]];
2152        }
2153        else
2154        {
2155            if ([title hasSuffix: ellipsis])
2156                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
2157        }
2158       
2159        return canUseTable && canDelete && [fTableView numberOfSelectedRows] > 0;
2160    }
2161
2162    //enable pause all item
2163    if (action == @selector(stopAllTorrents:))
2164    {
2165        Torrent * torrent;
2166        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2167        while ((torrent = [enumerator nextObject]))
2168            if ([torrent isActive] || [torrent waitingToStart])
2169                return YES;
2170        return NO;
2171    }
2172   
2173    //enable resume all item
2174    if (action == @selector(resumeAllTorrents:))
2175    {
2176        Torrent * torrent;
2177        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2178        while ((torrent = [enumerator nextObject]))
2179            if ([torrent isPaused] && ![torrent waitingToStart])
2180                return YES;
2181        return NO;
2182    }
2183   
2184    //enable resume all waiting item
2185    if (action == @selector(resumeWaitingTorrents:))
2186    {
2187        if (![fDefaults boolForKey: @"Queue"])
2188            return NO;
2189   
2190        Torrent * torrent;
2191        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2192        while ((torrent = [enumerator nextObject]))
2193            if ([torrent waitingToStart])
2194                return YES;
2195        return NO;
2196    }
2197   
2198    //enable resume selected waiting item
2199    if (action == @selector(resumeSelectedTorrentsNoWait:))
2200    {
2201        if (![fDefaults boolForKey: @"Queue"])
2202            return NO;
2203   
2204        Torrent * torrent;
2205        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2206        unsigned int i;
2207       
2208        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2209        {
2210            torrent = [fDisplayedTorrents objectAtIndex: i];
2211            if ([torrent isPaused] && [torrent progress] < 1.0)
2212                return YES;
2213        }
2214        return NO;
2215    }
2216
2217    //enable pause item
2218    if (action == @selector(stopSelectedTorrents:))
2219    {
2220        if (!canUseTable)
2221            return NO;
2222   
2223        Torrent * torrent;
2224        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2225        unsigned int i;
2226       
2227        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2228        {
2229            torrent = [fDisplayedTorrents objectAtIndex: i];
2230            if ([torrent isActive] || [torrent waitingToStart])
2231                return YES;
2232        }
2233        return NO;
2234    }
2235   
2236    //enable resume item
2237    if (action == @selector(resumeSelectedTorrents:))
2238    {
2239        if (!canUseTable)
2240            return NO;
2241   
2242        Torrent * torrent;
2243        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
2244        unsigned int i;
2245       
2246        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
2247        {
2248            torrent = [fDisplayedTorrents objectAtIndex: i];
2249            if ([torrent isPaused] && ![torrent waitingToStart])
2250                return YES;
2251        }
2252        return NO;
2253    }
2254   
2255    //enable copy torrent file item
2256    if (action == @selector(copyTorrentFile:))
2257        return canUseTable && [fTableView numberOfSelectedRows] > 0;
2258
2259    return YES;
2260}
2261
2262- (void) sleepCallBack: (natural_t) messageType argument: (void *) messageArgument
2263{
2264    NSEnumerator * enumerator;
2265    Torrent * torrent;
2266    BOOL active;
2267
2268    switch (messageType)
2269    {
2270        case kIOMessageSystemWillSleep:
2271            //close all connections before going to sleep and remember we should resume when we wake up
2272            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
2273
2274            //wait for running transfers to stop (5 second timeout)
2275            NSDate * start = [NSDate date];
2276            BOOL timeUp = NO;
2277           
2278            NSEnumerator * enumerator = [fTorrents objectEnumerator];
2279            Torrent * torrent;
2280            while (!timeUp && (torrent = [enumerator nextObject]))
2281                while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
2282                {
2283                    usleep(100000);
2284                    [torrent update];
2285                }
2286
2287            IOAllowPowerChange(fRootPort, (long) messageArgument);
2288            break;
2289
2290        case kIOMessageCanSystemSleep:
2291            //pevent idle sleep unless all paused
2292            active = NO;
2293            enumerator = [fTorrents objectEnumerator];
2294            while ((torrent = [enumerator nextObject]))
2295                if ([torrent isActive])
2296                {
2297                    active = YES;
2298                    break;
2299                }
2300
2301            if (active)
2302                IOCancelPowerChange(fRootPort, (long) messageArgument);
2303            else
2304                IOAllowPowerChange(fRootPort, (long) messageArgument);
2305            break;
2306
2307        case kIOMessageSystemHasPoweredOn:
2308            //resume sleeping transfers after we wake up
2309            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
2310            break;
2311    }
2312}
2313
2314- (void) resetDockBadge: (NSNotification *) notification
2315{
2316    float downloadRate, uploadRate;
2317    tr_torrentRates(fLib, & downloadRate, & uploadRate);
2318   
2319    [fBadger updateBadgeWithCompleted: fCompleted uploadRate: uploadRate downloadRate: downloadRate];
2320}
2321
2322- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
2323{
2324    //if auto size is enabled, the current frame shouldn't need to change
2325    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
2326   
2327    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
2328    return frame;
2329}
2330
2331- (void) setWindowSizeToFit
2332{
2333    if ([fDefaults boolForKey: @"AutoSize"])
2334    {
2335        [fScrollView setHasVerticalScroller: NO];
2336        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
2337        [fScrollView setHasVerticalScroller: YES];
2338    }
2339}
2340
2341- (NSRect) sizedWindowFrame
2342{
2343    NSRect frame = [fWindow frame];
2344    float newHeight = frame.size.height - [fScrollView frame].size.height
2345        + [fDisplayedTorrents count] * ([fTableView rowHeight] + [fTableView intercellSpacing].height);
2346
2347    float minHeight = [fWindow minSize].height;
2348    if (newHeight < minHeight)
2349        newHeight = minHeight;
2350    else
2351    {
2352        float maxHeight = [[fWindow screen] visibleFrame].size.height;
2353        if ([fStatusBar isHidden])
2354            maxHeight -= [fStatusBar frame].size.height;
2355        if ([fFilterBar isHidden])
2356            maxHeight -= [fFilterBar frame].size.height;
2357       
2358        if (newHeight > maxHeight)
2359            newHeight = maxHeight;
2360    }
2361
2362    frame.origin.y -= (newHeight - frame.size.height);
2363    frame.size.height = newHeight;
2364    return frame;
2365}
2366
2367- (void) showMainWindow: (id) sender
2368{
2369    [fWindow makeKeyAndOrderFront: nil];
2370}
2371
2372- (void) windowDidBecomeKey: (NSNotification *) notification
2373{
2374    //reset dock badge for completed
2375    if (fCompleted > 0)
2376    {
2377        fCompleted = 0;
2378        [self resetDockBadge: nil];
2379    }
2380}
2381
2382- (NSSize) windowWillResize: (NSWindow *) sender toSize: (NSSize) proposedFrameSize
2383{
2384    //only resize horizontally if autosize is enabled
2385    if ([fDefaults boolForKey: @"AutoSize"])
2386        proposedFrameSize.height = [fWindow frame].size.height;
2387    return proposedFrameSize;
2388}
2389
2390- (void) windowDidResize: (NSNotification *) notification
2391{
2392    //hide search filter if it overlaps filter buttons
2393    [fSearchFilterField setHidden: NSMaxX([fPauseFilterButton frame]) + 2.0 > [fSearchFilterField frame].origin.x];
2394}
2395
2396- (void) linkHomepage: (id) sender
2397{
2398    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
2399}
2400
2401- (void) linkForums: (id) sender
2402{
2403    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
2404}
2405
2406- (void) checkUpdate: (id) sender
2407{
2408    [fPrefsController checkUpdate];
2409}
2410
2411- (void) prepareForUpdate: (NSNotification *) notification
2412{
2413    fUpdateInProgress = YES;
2414}
2415
2416- (NSDictionary *) registrationDictionaryForGrowl
2417{
2418    NSArray * notifications = [NSArray arrayWithObjects: GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE,
2419                                                            GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT, nil];
2420    return [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL,
2421                                notifications, GROWL_NOTIFICATIONS_DEFAULT, nil];
2422}
2423
2424@end
Note: See TracBrowser for help on using the repository browser.