source: trunk/macosx/Controller.m @ 807

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

Message Log window, accessible from the Transmission menu.

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