source: trunk/macosx/Controller.m @ 8371

Last change on this file since 8371 was 8371, checked in by livings124, 13 years ago

#2041 bounce Downloads stack when downloads are complete

  • Property svn:keywords set to Date Rev Author Id
File size: 152.9 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 8371 2009-05-09 17:11:21Z livings124 $
3 *
4 * Copyright (c) 2005-2009 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#import <IOKit/pwr_mgt/IOPMLib.h>
27
28#import "Controller.h"
29#import "Torrent.h"
30#import "TorrentGroup.h"
31#import "TorrentCell.h"
32#import "TorrentTableView.h"
33#import "CreatorWindowController.h"
34#import "StatsWindowController.h"
35#import "QuickLookController.h"
36#import "GroupsController.h"
37#import "AboutWindowController.h"
38#import "ButtonToolbarItem.h"
39#import "GroupToolbarItem.h"
40#import "ToolbarSegmentedCell.h"
41#import "BlocklistDownloader.h"
42#import "StatusBarView.h"
43#import "FilterButton.h"
44#import "BonjourController.h"
45#import "NSStringAdditions.h"
46#import "ExpandedPathToPathTransformer.h"
47#import "ExpandedPathToIconTransformer.h"
48#import "bencode.h"
49#import "utils.h"
50
51#import "UKKQueue.h"
52#import <Sparkle/Sparkle.h>
53
54#define TOOLBAR_CREATE                  @"Toolbar Create"
55#define TOOLBAR_OPEN_FILE               @"Toolbar Open"
56#define TOOLBAR_OPEN_WEB                @"Toolbar Open Web"
57#define TOOLBAR_REMOVE                  @"Toolbar Remove"
58#define TOOLBAR_INFO                    @"Toolbar Info"
59#define TOOLBAR_PAUSE_ALL               @"Toolbar Pause All"
60#define TOOLBAR_RESUME_ALL              @"Toolbar Resume All"
61#define TOOLBAR_PAUSE_RESUME_ALL        @"Toolbar Pause / Resume All"
62#define TOOLBAR_PAUSE_SELECTED          @"Toolbar Pause Selected"
63#define TOOLBAR_RESUME_SELECTED         @"Toolbar Resume Selected"
64#define TOOLBAR_PAUSE_RESUME_SELECTED   @"Toolbar Pause / Resume Selected"
65#define TOOLBAR_FILTER                  @"Toolbar Toggle Filter"
66#define TOOLBAR_QUICKLOOK               @"Toolbar QuickLook"
67
68typedef enum
69{
70    TOOLBAR_PAUSE_TAG = 0,
71    TOOLBAR_RESUME_TAG = 1
72} toolbarGroupTag;
73
74#define SORT_DATE       @"Date"
75#define SORT_NAME       @"Name"
76#define SORT_STATE      @"State"
77#define SORT_PROGRESS   @"Progress"
78#define SORT_TRACKER    @"Tracker"
79#define SORT_ORDER      @"Order"
80#define SORT_ACTIVITY   @"Activity"
81
82typedef enum
83{
84    SORT_ORDER_TAG = 0,
85    SORT_DATE_TAG = 1,
86    SORT_NAME_TAG = 2,
87    SORT_PROGRESS_TAG = 3,
88    SORT_STATE_TAG = 4,
89    SORT_TRACKER_TAG = 5,
90    SORT_ACTIVITY_TAG = 6
91} sortTag;
92
93#define FILTER_NONE     @"None"
94#define FILTER_ACTIVE   @"Active"
95#define FILTER_DOWNLOAD @"Download"
96#define FILTER_SEED     @"Seed"
97#define FILTER_PAUSE    @"Pause"
98
99#define FILTER_TYPE_NAME    @"Name"
100#define FILTER_TYPE_TRACKER @"Tracker"
101
102#define FILTER_TYPE_TAG_NAME    401
103#define FILTER_TYPE_TAG_TRACKER 402
104
105#define GROUP_FILTER_ALL_TAG    -2
106
107#define STATUS_RATIO_TOTAL      @"RatioTotal"
108#define STATUS_RATIO_SESSION    @"RatioSession"
109#define STATUS_TRANSFER_TOTAL   @"TransferTotal"
110#define STATUS_TRANSFER_SESSION @"TransferSession"
111
112typedef enum
113{
114    STATUS_RATIO_TOTAL_TAG = 0,
115    STATUS_RATIO_SESSION_TAG = 1,
116    STATUS_TRANSFER_TOTAL_TAG = 2,
117    STATUS_TRANSFER_SESSION_TAG = 3
118} statusTag;
119
120#define GROWL_DOWNLOAD_COMPLETE @"Download Complete"
121#define GROWL_SEEDING_COMPLETE  @"Seeding Complete"
122#define GROWL_AUTO_ADD          @"Torrent Auto Added"
123#define GROWL_AUTO_SPEED_LIMIT  @"Speed Limit Auto Changed"
124
125#define TORRENT_TABLE_VIEW_DATA_TYPE    @"TorrentTableViewDataType"
126
127#define ROW_HEIGHT_REGULAR      62.0
128#define ROW_HEIGHT_SMALL        38.0
129#define WINDOW_REGULAR_WIDTH    468.0
130
131#define SEARCH_FILTER_MIN_WIDTH 48.0
132#define SEARCH_FILTER_MAX_WIDTH 95.0
133
134#define UPDATE_UI_SECONDS   1.0
135
136#define DOCK_SEEDING_TAG        101
137#define DOCK_DOWNLOADING_TAG    102
138
139#define SUPPORT_FOLDER  @"/Library/Application Support/Transmission/Transfers.plist"
140
141#define WEBSITE_URL @"http://www.transmissionbt.com/"
142#define FORUM_URL   @"http://forum.transmissionbt.com/"
143#define TRAC_URL   @"http://trac.transmissionbt.com/"
144#define DONATE_URL  @"http://www.transmissionbt.com/donate.php"
145
146static void altSpeedToggledCallback(tr_session * handle UNUSED, tr_bool active, tr_bool byUser, void * controller)
147{
148    NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: [[NSNumber alloc] initWithBool: active], @"Active",
149                                [[NSNumber alloc] initWithBool: byUser], @"ByUser", nil];
150    [(Controller *)controller performSelectorOnMainThread: @selector(altSpeedToggledCallbackIsLimited:)
151        withObject: dict waitUntilDone: NO];
152}
153
154static tr_rpc_callback_status rpcCallback(tr_session * handle UNUSED, tr_rpc_callback_type type, struct tr_torrent * torrentStruct, void * controller)
155{
156    [(Controller *)controller rpcCallback: type forTorrentStruct: torrentStruct];
157    return TR_RPC_NOREMOVE; //we'll do the remove manually
158}
159
160static void sleepCallback(void * controller, io_service_t y, natural_t messageType, void * messageArgument)
161{
162    [(Controller *)controller sleepCallback: messageType argument: messageArgument];
163}
164
165@implementation Controller
166
167+ (void) initialize
168{
169    //make sure another Transmission.app isn't running already
170    NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
171    int processIdentifier = [[NSProcessInfo processInfo] processIdentifier];
172
173    for (NSDictionary * dic in [[NSWorkspace sharedWorkspace] launchedApplications])
174    {
175        if ([[dic objectForKey: @"NSApplicationBundleIdentifier"] isEqualToString: bundleIdentifier]
176            && [[dic objectForKey: @"NSApplicationProcessIdentifier"] intValue] != processIdentifier)
177        {
178            NSAlert * alert = [[NSAlert alloc] init];
179            [alert addButtonWithTitle: NSLocalizedString(@"Quit", "Transmission already running alert -> button")];
180            [alert setMessageText: NSLocalizedString(@"Transmission is already running.",
181                                                    "Transmission already running alert -> title")];
182            [alert setInformativeText: NSLocalizedString(@"There is already a copy of Transmission running. "
183                "This copy cannot be opened until that instance is quit.", "Transmission already running alert -> message")];
184            [alert setAlertStyle: NSWarningAlertStyle];
185           
186            [alert runModal];
187            [alert release];
188           
189            //kill ourselves right away
190            exit(0);
191        }
192    }
193   
194    [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithContentsOfFile:
195        [[NSBundle mainBundle] pathForResource: @"Defaults" ofType: @"plist"]]];
196   
197    //set custom value transformers
198    ExpandedPathToPathTransformer * pathTransformer = [[[ExpandedPathToPathTransformer alloc] init] autorelease];
199    [NSValueTransformer setValueTransformer: pathTransformer forName: @"ExpandedPathToPathTransformer"];
200   
201    ExpandedPathToIconTransformer * iconTransformer = [[[ExpandedPathToIconTransformer alloc] init] autorelease];
202    [NSValueTransformer setValueTransformer: iconTransformer forName: @"ExpandedPathToIconTransformer"];
203}
204
205- (id) init
206{
207    if ((self = [super init]))
208    {
209        fDefaults = [NSUserDefaults standardUserDefaults];
210       
211        //checks for old version speeds of -1
212        if ([fDefaults integerForKey: @"UploadLimit"] < 0)
213        {
214            [fDefaults removeObjectForKey: @"UploadLimit"];
215            [fDefaults setBool: NO forKey: @"CheckUpload"];
216        }
217        if ([fDefaults integerForKey: @"DownloadLimit"] < 0)
218        {
219            [fDefaults removeObjectForKey: @"DownloadLimit"];
220            [fDefaults setBool: NO forKey: @"CheckDownload"];
221        }
222       
223        tr_benc settings;
224        tr_bencInitDict(&settings, 33);
225        tr_sessionGetDefaultSettings(&settings);
226       
227        const BOOL usesSpeedLimitSched = [fDefaults boolForKey: @"SpeedLimitAuto"];
228        if (!usesSpeedLimitSched)
229            tr_bencDictAddBool(&settings, TR_PREFS_KEY_ALT_SPEED_ENABLED, [fDefaults boolForKey: @"SpeedLimit"]);
230       
231        tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_UP, [fDefaults integerForKey: @"SpeedLimitUploadLimit"]);
232        tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_DOWN, [fDefaults integerForKey: @"SpeedLimitDownloadLimit"]);
233       
234        tr_bencDictAddBool(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED, [fDefaults boolForKey: @"SpeedLimitAuto"]);
235        tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_BEGIN, [PrefsController dateToTimeSum:
236                                                                            [fDefaults objectForKey: @"SpeedLimitAutoOnDate"]]);
237        tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_END, [PrefsController dateToTimeSum:
238                                                                            [fDefaults objectForKey: @"SpeedLimitAutoOffDate"]]);
239        tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_DAY, [fDefaults integerForKey: @"SpeedLimitAutoDay"]);
240       
241        tr_bencDictAddInt(&settings, TR_PREFS_KEY_DSPEED, [fDefaults integerForKey: @"DownloadLimit"]);
242        tr_bencDictAddBool(&settings, TR_PREFS_KEY_DSPEED_ENABLED, [fDefaults boolForKey: @"CheckDownload"]);
243        tr_bencDictAddInt(&settings, TR_PREFS_KEY_USPEED, [fDefaults integerForKey: @"UploadLimit"]);
244        tr_bencDictAddBool(&settings, TR_PREFS_KEY_USPEED_ENABLED, [fDefaults boolForKey: @"CheckUpload"]);
245       
246        tr_bencDictAddBool(&settings, TR_PREFS_KEY_BLOCKLIST_ENABLED, [fDefaults boolForKey: @"Blocklist"]);
247       
248        tr_bencDictAddStr(&settings, TR_PREFS_KEY_DOWNLOAD_DIR, [[[fDefaults stringForKey: @"DownloadFolder"]
249                                                                    stringByExpandingTildeInPath] UTF8String]);
250       
251        tr_bencDictAddInt(&settings, TR_PREFS_KEY_MSGLEVEL, [fDefaults integerForKey: @"MessageLevel"]);
252        tr_bencDictAddInt(&settings, TR_PREFS_KEY_PEER_LIMIT_GLOBAL, [fDefaults integerForKey: @"PeersTotal"]);
253        tr_bencDictAddInt(&settings, TR_PREFS_KEY_PEER_LIMIT_TORRENT, [fDefaults integerForKey: @"PeersTorrent"]);
254       
255        const BOOL randomPort = [fDefaults boolForKey: @"RandomPort"];
256        tr_bencDictAddBool(&settings, TR_PREFS_KEY_PEER_PORT_RANDOM_ON_START, randomPort);
257        if (!randomPort)
258            tr_bencDictAddInt(&settings, TR_PREFS_KEY_PEER_PORT, [fDefaults integerForKey: @"BindPort"]);
259       
260        //hidden pref
261        if ([fDefaults objectForKey: @"PeerSocketTOS"])
262            tr_bencDictAddInt(&settings, TR_PREFS_KEY_PEER_SOCKET_TOS, [fDefaults integerForKey: @"PeerSocketTOS"]);
263       
264        tr_bencDictAddBool(&settings, TR_PREFS_KEY_PEX_ENABLED, [fDefaults boolForKey: @"PEXGlobal"]);
265        tr_bencDictAddBool(&settings, TR_PREFS_KEY_PORT_FORWARDING, [fDefaults boolForKey: @"NatTraversal"]);
266        tr_bencDictAddBool(&settings, TR_PREFS_KEY_PROXY_AUTH_ENABLED, [fDefaults boolForKey: @"ProxyAuthorize"]);
267        tr_bencDictAddBool(&settings, TR_PREFS_KEY_PROXY_ENABLED, [fDefaults boolForKey: @"Proxy"]);
268        tr_bencDictAddInt(&settings, TR_PREFS_KEY_PROXY_PORT, [fDefaults integerForKey: @"ProxyPort"]);
269        tr_bencDictAddStr(&settings, TR_PREFS_KEY_PROXY, [[fDefaults stringForKey: @"ProxyAddress"] UTF8String]);
270        tr_bencDictAddStr(&settings, TR_PREFS_KEY_PROXY_USERNAME,  [[fDefaults stringForKey: @"ProxyUsername"] UTF8String]);
271        tr_bencDictAddBool(&settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED,  [fDefaults boolForKey: @"RPCAuthorize"]);
272        tr_bencDictAddReal(&settings, TR_PREFS_KEY_RATIO, [fDefaults floatForKey: @"RatioLimit"]);
273        tr_bencDictAddBool(&settings, TR_PREFS_KEY_RATIO_ENABLED, [fDefaults boolForKey: @"RatioCheck"]);
274        tr_bencDictAddBool(&settings, TR_PREFS_KEY_RPC_ENABLED,  [fDefaults boolForKey: @"RPC"]);
275        tr_bencDictAddInt(&settings, TR_PREFS_KEY_RPC_PORT,  [fDefaults integerForKey: @"RPCPort"]);
276        tr_bencDictAddStr(&settings, TR_PREFS_KEY_RPC_USERNAME,  [[fDefaults stringForKey: @"RPCUsername"] UTF8String]);
277        tr_bencDictAddBool(&settings, TR_PREFS_KEY_RPC_WHITELIST_ENABLED,  [fDefaults boolForKey: @"RPCUseWhitelist"]);
278       
279        fLib = tr_sessionInit("macosx", tr_getDefaultConfigDir("Transmission"), YES, &settings);
280        tr_bencFree(&settings);
281       
282        [NSApp setDelegate: self];
283       
284        fTorrents = [[NSMutableArray alloc] init];
285        fDisplayedTorrents = [[NSMutableArray alloc] init];
286       
287        fMessageController = [[MessageWindowController alloc] init];
288        fInfoController = [[InfoWindowController alloc] init];
289       
290        [PrefsController setHandle: fLib];
291        fPrefsController = [[PrefsController alloc] init];
292       
293        [QuickLookController quickLookControllerInitializeWithController: self infoController: fInfoController];
294       
295        fSoundPlaying = NO;
296       
297        tr_sessionSetAltSpeedFunc(fLib, altSpeedToggledCallback, self);
298        if (usesSpeedLimitSched)
299            [fDefaults setBool: tr_sessionUsesAltSpeed(fLib) forKey: @"SpeedLimit"];
300       
301        tr_sessionSetRPCCallback(fLib, rpcCallback, self);
302       
303        [GrowlApplicationBridge setGrowlDelegate: self];
304       
305        [[UKKQueue sharedFileWatcher] setDelegate: self];
306       
307        [[SUUpdater sharedUpdater] setDelegate: self];
308        fUpdateInProgress = NO;
309    }
310    return self;
311}
312
313- (void) awakeFromNib
314{
315    NSToolbar * toolbar = [[NSToolbar alloc] initWithIdentifier: @"TRMainToolbar"];
316    [toolbar setDelegate: self];
317    [toolbar setAllowsUserCustomization: YES];
318    [toolbar setAutosavesConfiguration: YES];
319    [toolbar setDisplayMode: NSToolbarDisplayModeIconOnly];
320    [fWindow setToolbar: toolbar];
321    [toolbar release];
322   
323    [fWindow setDelegate: self]; //do manually to avoid placement issue
324   
325    [fWindow makeFirstResponder: fTableView];
326    [fWindow setExcludedFromWindowsMenu: YES];
327   
328    //set table size
329    if ([fDefaults boolForKey: @"SmallView"])
330        [fTableView setRowHeight: ROW_HEIGHT_SMALL];
331   
332    //window min height
333    NSSize contentMinSize = [fWindow contentMinSize];
334    contentMinSize.height = [[fWindow contentView] frame].size.height - [[fTableView enclosingScrollView] frame].size.height
335                                + [fTableView rowHeight] + [fTableView intercellSpacing].height;
336    [fWindow setContentMinSize: contentMinSize];
337    [fWindow setContentBorderThickness: [[fTableView enclosingScrollView] frame].origin.y forEdge: NSMinYEdge];
338       
339    [[fTotalTorrentsField cell] setBackgroundStyle: NSBackgroundStyleRaised];
340   
341    [self updateGroupsFilterButton];
342   
343    //set up filter bar
344    NSView * contentView = [fWindow contentView];
345    NSSize windowSize = [contentView convertSize: [fWindow frame].size fromView: nil];
346    [fFilterBar setHidden: YES];
347   
348    NSRect filterBarFrame = [fFilterBar frame];
349    filterBarFrame.size.width = windowSize.width;
350    [fFilterBar setFrame: filterBarFrame];
351   
352    [contentView addSubview: fFilterBar];
353    [fFilterBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
354
355    [self showFilterBar: [fDefaults boolForKey: @"FilterBar"] animate: NO];
356   
357    //set up status bar
358    [fStatusBar setHidden: YES];
359   
360    [self updateSpeedFieldsToolTips];
361   
362    NSRect statusBarFrame = [fStatusBar frame];
363    statusBarFrame.size.width = windowSize.width;
364    [fStatusBar setFrame: statusBarFrame];
365   
366    [contentView addSubview: fStatusBar];
367    [fStatusBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
368    [self showStatusBar: [fDefaults boolForKey: @"StatusBar"] animate: NO];
369   
370    [fActionButton setToolTip: NSLocalizedString(@"Shortcuts for changing global settings.",
371                                "Main window -> 1st bottom left button (action) tooltip")];
372    [fSpeedLimitButton setToolTip: NSLocalizedString(@"Speed Limit overrides the total bandwidth limits with its own limits.",
373                                "Main window -> 2nd bottom left button (turtle) tooltip")];
374   
375    [fTableView registerForDraggedTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE]];
376    [fWindow registerForDraggedTypes: [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]];
377
378    //register for sleep notifications
379    IONotificationPortRef notify;
380    io_object_t iterator;
381    if ((fRootPort = IORegisterForSystemPower(self, & notify, sleepCallback, &iterator)))
382        CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify), kCFRunLoopCommonModes);
383    else
384        NSLog(@"Could not IORegisterForSystemPower");
385   
386    //load previous transfers
387    NSArray * history = [[NSArray alloc] initWithContentsOfFile: [NSHomeDirectory() stringByAppendingPathComponent: SUPPORT_FOLDER]];
388   
389    //old version saved transfer info in prefs file
390    if (!history)
391    {
392        if ((history = [fDefaults arrayForKey: @"History"]))
393        {
394            [history retain];
395            [fDefaults removeObjectForKey: @"History"];
396        }
397    }
398   
399    if (history)
400    {
401        for (NSDictionary * historyItem in history)
402        {
403            Torrent * torrent;
404            if ((torrent = [[Torrent alloc] initWithHistory: historyItem lib: fLib]))
405            {
406                [fTorrents addObject: torrent];
407                [torrent release];
408            }
409        }
410        [history release];
411    }
412   
413    //set filter
414    NSString * filterType = [fDefaults stringForKey: @"Filter"];
415   
416    NSButton * currentFilterButton;
417    if ([filterType isEqualToString: FILTER_ACTIVE])
418        currentFilterButton = fActiveFilterButton;
419    else if ([filterType isEqualToString: FILTER_PAUSE])
420        currentFilterButton = fPauseFilterButton;
421    else if ([filterType isEqualToString: FILTER_SEED])
422        currentFilterButton = fSeedFilterButton;
423    else if ([filterType isEqualToString: FILTER_DOWNLOAD])
424        currentFilterButton = fDownloadFilterButton;
425    else
426    {
427        //safety
428        if (![filterType isEqualToString: FILTER_NONE])
429            [fDefaults setObject: FILTER_NONE forKey: @"Filter"];
430        currentFilterButton = fNoFilterButton;
431    }
432    [currentFilterButton setState: NSOnState];
433   
434    //set filter search type
435    NSString * filterSearchType = [fDefaults stringForKey: @"FilterSearchType"];
436   
437    NSMenu * filterSearchMenu = [[fSearchFilterField cell] searchMenuTemplate];
438    NSString * filterSearchTypeTitle;
439    if ([filterSearchType isEqualToString: FILTER_TYPE_TRACKER])
440        filterSearchTypeTitle = [[filterSearchMenu itemWithTag: FILTER_TYPE_TAG_TRACKER] title];
441    else
442    {
443        //safety
444        if (![filterType isEqualToString: FILTER_TYPE_NAME])
445            [fDefaults setObject: FILTER_TYPE_NAME forKey: @"FilterSearchType"];
446        filterSearchTypeTitle = [[filterSearchMenu itemWithTag: FILTER_TYPE_TAG_NAME] title];
447    }
448    [[fSearchFilterField cell] setPlaceholderString: filterSearchTypeTitle];
449   
450    fBadger = [[Badger alloc] initWithLib: fLib];
451   
452    //observe notifications
453    NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
454   
455    [nc addObserver: self selector: @selector(updateUI)
456                    name: @"UpdateUI" object: nil];
457   
458    [nc addObserver: self selector: @selector(torrentFinishedDownloading:)
459                    name: @"TorrentFinishedDownloading" object: nil];
460   
461    [nc addObserver: self selector: @selector(torrentRestartedDownloading:)
462                    name: @"TorrentRestartedDownloading" object: nil];
463   
464    //avoids need of setting delegate
465    [nc addObserver: self selector: @selector(torrentTableViewSelectionDidChange:)
466                    name: NSOutlineViewSelectionDidChangeNotification object: fTableView];
467   
468    [nc addObserver: self selector: @selector(changeAutoImport)
469                    name: @"AutoImportSettingChange" object: nil];
470   
471    [nc addObserver: self selector: @selector(setWindowSizeToFit)
472                    name: @"AutoSizeSettingChange" object: nil];
473   
474    [nc addObserver: self selector: @selector(updateForExpandCollape)
475                    name: @"OutlineExpandCollapse" object: nil];
476   
477    [nc addObserver: fWindow selector: @selector(makeKeyWindow)
478                    name: @"MakeWindowKey" object: nil];
479   
480    //check if torrent should now start
481    [nc addObserver: self selector: @selector(torrentStoppedForRatio:)
482                    name: @"TorrentStoppedForRatio" object: nil];
483   
484    [nc addObserver: self selector: @selector(updateTorrentsInQueue)
485                    name: @"UpdateQueue" object: nil];
486   
487    //open newly created torrent file
488    [nc addObserver: self selector: @selector(beginCreateFile:)
489                    name: @"BeginCreateTorrentFile" object: nil];
490   
491    //open newly created torrent file
492    [nc addObserver: self selector: @selector(openCreatedFile:)
493                    name: @"OpenCreatedTorrentFile" object: nil];
494   
495    //update when groups change
496    [nc addObserver: self selector: @selector(updateGroupsFilters:)
497                    name: @"UpdateGroups" object: nil];
498   
499    //update when speed limits are changed
500    [nc addObserver: self selector: @selector(updateSpeedFieldsToolTips)
501                    name: @"SpeedLimitUpdate" object: nil];
502
503    //timer to update the interface every second
504    [self updateUI];
505    fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_UI_SECONDS target: self
506                selector: @selector(updateUI) userInfo: nil repeats: YES];
507    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSModalPanelRunLoopMode];
508    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSEventTrackingRunLoopMode];
509   
510    [self applyFilter: nil];
511   
512    [fWindow makeKeyAndOrderFront: nil];
513   
514    if ([fDefaults boolForKey: @"InfoVisible"])
515        [self showInfo: nil];
516}
517
518- (void) applicationDidFinishLaunching: (NSNotification *) notification
519{
520    [NSApp setServicesProvider: self];
521   
522    //register for dock icon drags
523    [[NSAppleEventManager sharedAppleEventManager] setEventHandler: self andSelector: @selector(handleOpenContentsEvent:replyEvent:)
524        forEventClass: kCoreEventClass andEventID: kAEOpenContents];
525   
526    //auto importing
527    [self checkAutoImportDirectory];
528   
529    //registering the Web UI to Bonjour
530    if ([fDefaults boolForKey: @"RPC"] && [fDefaults boolForKey: @"RPCWebDiscovery"])
531        [[BonjourController defaultController] startWithPort: [fDefaults integerForKey: @"RPCPort"]];
532}
533
534- (BOOL) applicationShouldHandleReopen: (NSApplication *) app hasVisibleWindows: (BOOL) visibleWindows
535{
536    if (![fWindow isVisible] && ![[fPrefsController window] isVisible])
537        [fWindow makeKeyAndOrderFront: nil];
538    return NO;
539}
540
541- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) sender
542{
543    if (!fUpdateInProgress && [fDefaults boolForKey: @"CheckQuit"])
544    {
545        NSInteger active = 0, downloading = 0;
546        for (Torrent * torrent  in fTorrents)
547            if ([torrent isActive] && ![torrent isStalled])
548            {
549                active++;
550                if (![torrent allDownloaded])
551                    downloading++;
552            }
553       
554        if ([fDefaults boolForKey: @"CheckQuitDownloading"] ? downloading > 0 : active > 0)
555        {
556            NSString * message = active == 1
557                ? NSLocalizedString(@"There is an active transfer that will be paused on quit."
558                    " The transfer will automatically resume on the next launch.", "Confirm Quit panel -> message")
559                : [NSString stringWithFormat: NSLocalizedString(@"There are %d active transfers that will be paused on quit."
560                    " The transfers will automatically resume on the next launch.", "Confirm Quit panel -> message"), active];
561
562            NSBeginAlertSheet(NSLocalizedString(@"Are you sure you want to quit?", "Confirm Quit panel -> title"),
563                                NSLocalizedString(@"Quit", "Confirm Quit panel -> button"),
564                                NSLocalizedString(@"Cancel", "Confirm Quit panel -> button"), nil, fWindow, self,
565                                @selector(quitSheetDidEnd:returnCode:contextInfo:), nil, nil, message);
566            return NSTerminateLater;
567        }
568    }
569   
570    return NSTerminateNow;
571}
572
573- (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (NSInteger) returnCode contextInfo: (void *) contextInfo
574{
575    [NSApp replyToApplicationShouldTerminate: returnCode == NSAlertDefaultReturn];
576}
577
578- (void) applicationWillTerminate: (NSNotification *) notification
579{
580    //stop the Bonjour service
581    [[BonjourController defaultController] stop];
582
583    //stop blocklist download
584    if ([BlocklistDownloader isRunning])
585        [[BlocklistDownloader downloader] cancelDownload];
586   
587    //stop timers and notification checking
588    [[NSNotificationCenter defaultCenter] removeObserver: self];
589   
590    [fTimer invalidate];
591   
592    if (fAutoImportTimer)
593    {   
594        if ([fAutoImportTimer isValid])
595            [fAutoImportTimer invalidate];
596        [fAutoImportTimer release];
597    }
598   
599    [fBadger setQuitting];
600   
601    //remove all torrent downloads
602    if (fPendingTorrentDownloads)
603    {
604        for (NSDictionary * downloadDict in fPendingTorrentDownloads)
605        {
606            NSURLDownload * download = [downloadDict objectForKey: @"Download"];
607            [download cancel];
608            [download release];
609        }
610        [fPendingTorrentDownloads removeAllObjects];
611    }
612   
613    //remember window states and close all windows
614    [fDefaults setBool: [[fInfoController window] isVisible] forKey: @"InfoVisible"];
615    [[NSApp windows] makeObjectsPerformSelector: @selector(close)];
616    [self showStatusBar: NO animate: NO];
617    [self showFilterBar: NO animate: NO];
618   
619    //save history
620    [self updateTorrentHistory];
621    [fTableView saveCollapsedGroups];
622   
623    //remaining calls the same as dealloc
624    [fInfoController release];
625    [fMessageController release];
626    [fPrefsController release];
627   
628    [fTorrents release];
629    [fDisplayedTorrents release];
630   
631    [fOverlayWindow release];
632    [fBadger release];
633   
634    [fAutoImportedNames release];
635    [fPendingTorrentDownloads release];
636   
637    //complete cleanup
638    tr_sessionClose(fLib);
639}
640
641- (void) handleOpenContentsEvent: (NSAppleEventDescriptor *) event replyEvent: (NSAppleEventDescriptor *) replyEvent
642{
643    NSString * urlString = nil;
644
645    NSAppleEventDescriptor * directObject = [event paramDescriptorForKeyword: keyDirectObject];
646    if ([directObject descriptorType] == typeAEList)
647    {
648        for (NSUInteger i = 1; i <= [directObject numberOfItems]; i++)
649            if ((urlString = [[directObject descriptorAtIndex: i] stringValue]))
650                break;
651    }
652    else
653        urlString = [directObject stringValue];
654   
655    if (urlString)
656        [self openURL: [NSURL URLWithString: urlString]];
657}
658
659- (void) download: (NSURLDownload *) download decideDestinationWithSuggestedFilename: (NSString *) suggestedName
660{
661    if ([[suggestedName pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
662    {
663        [download cancel];
664       
665        NSRunAlertPanel(NSLocalizedString(@"Torrent download failed", "Download not a torrent -> title"),
666            [NSString stringWithFormat: NSLocalizedString(@"It appears that the file \"%@\" from %@ is not a torrent file.",
667            "Download not a torrent -> message"), suggestedName,
668            [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]],
669            NSLocalizedString(@"OK", "Download not a torrent -> button"), nil, nil);
670       
671        [download release];
672    }
673    else
674        [download setDestination: [NSTemporaryDirectory() stringByAppendingPathComponent: [suggestedName lastPathComponent]]
675                    allowOverwrite: NO];
676}
677
678-(void) download: (NSURLDownload *) download didCreateDestination: (NSString *) path
679{
680    if (!fPendingTorrentDownloads)
681        fPendingTorrentDownloads = [[NSMutableDictionary alloc] init];
682   
683    [fPendingTorrentDownloads setObject: [NSDictionary dictionaryWithObjectsAndKeys:
684                    path, @"Path", download, @"Download", nil] forKey: [[download request] URL]];
685}
686
687- (void) download: (NSURLDownload *) download didFailWithError: (NSError *) error
688{
689    NSRunAlertPanel(NSLocalizedString(@"Torrent download failed", "Torrent download error -> title"),
690        [NSString stringWithFormat: NSLocalizedString(@"The torrent could not be downloaded from %@: %@.",
691        "Torrent download failed -> message"),
692        [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding],
693        [error localizedDescription]], NSLocalizedString(@"OK", "Torrent download failed -> button"), nil, nil);
694   
695    [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]];
696    if ([fPendingTorrentDownloads count] == 0)
697    {
698        [fPendingTorrentDownloads release];
699        fPendingTorrentDownloads = nil;
700    }
701   
702    [download release];
703}
704
705- (void) downloadDidFinish: (NSURLDownload *) download
706{
707    NSString * path = [[fPendingTorrentDownloads objectForKey: [[download request] URL]] objectForKey: @"Path"];
708   
709    [self openFiles: [NSArray arrayWithObject: path] addType: ADD_URL forcePath: nil];
710   
711    //delete the torrent file after opening
712    [[NSFileManager defaultManager] removeItemAtPath: path error: NULL];
713   
714    [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]];
715    if ([fPendingTorrentDownloads count] == 0)
716    {
717        [fPendingTorrentDownloads release];
718        fPendingTorrentDownloads = nil;
719    }
720   
721    [download release];
722}
723
724- (void) application: (NSApplication *) app openFiles: (NSArray *) filenames
725{
726    [self openFiles: filenames addType: ADD_MANUAL forcePath: nil];
727}
728
729- (void) openFiles: (NSArray *) filenames addType: (addType) type forcePath: (NSString *) path
730{
731    #warning checks could probably be removed, since location is checked when starting
732    if (!path && [fDefaults boolForKey: @"UseIncompleteDownloadFolder"]
733        && access([[[fDefaults stringForKey: @"IncompleteDownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0))
734    {
735        NSOpenPanel * panel = [NSOpenPanel openPanel];
736       
737        [panel setPrompt: NSLocalizedString(@"Select", "Default incomplete folder cannot be used alert -> prompt")];
738        [panel setAllowsMultipleSelection: NO];
739        [panel setCanChooseFiles: NO];
740        [panel setCanChooseDirectories: YES];
741        [panel setCanCreateDirectories: YES];
742
743        [panel setMessage: NSLocalizedString(@"The incomplete folder cannot be used. Choose a new location or cancel for none.",
744                                        "Default incomplete folder cannot be used alert -> message")];
745       
746        NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: filenames, @"Filenames",
747                                                        [NSNumber numberWithInt: type], @"AddType", nil];
748       
749        [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
750                didEndSelector: @selector(incompleteChoiceClosed:returnCode:contextInfo:) contextInfo: dict];
751        return;
752    }
753   
754    if (!path && [fDefaults boolForKey: @"DownloadLocationConstant"]
755        && access([[[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0))
756    {
757        NSOpenPanel * panel = [NSOpenPanel openPanel];
758       
759        [panel setPrompt: NSLocalizedString(@"Select", "Default folder cannot be used alert -> prompt")];
760        [panel setAllowsMultipleSelection: NO];
761        [panel setCanChooseFiles: NO];
762        [panel setCanChooseDirectories: YES];
763        [panel setCanCreateDirectories: YES];
764
765        [panel setMessage: NSLocalizedString(@"The download folder cannot be used. Choose a new location.",
766                                        "Default folder cannot be used alert -> message")];
767       
768        NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: filenames, @"Filenames",
769                                                        [NSNumber numberWithInt: type], @"AddType", nil];
770       
771        [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
772                didEndSelector: @selector(downloadChoiceClosed:returnCode:contextInfo:) contextInfo: dict];
773        return;
774    }
775   
776    torrentFileState deleteTorrentFile;
777    switch (type)
778    {
779        case ADD_CREATED:
780            deleteTorrentFile = TORRENT_FILE_SAVE;
781            break;
782        case ADD_URL:
783            deleteTorrentFile = TORRENT_FILE_DELETE;
784            break;
785        default:
786            deleteTorrentFile = TORRENT_FILE_DEFAULT;
787    }
788   
789    tr_info info;
790    for (NSString * torrentPath in filenames)
791    {
792        //ensure torrent doesn't already exist
793        tr_ctor * ctor = tr_ctorNew(fLib);
794        tr_ctorSetMetainfoFromFile(ctor, [torrentPath UTF8String]);
795        int result = tr_torrentParse(ctor, &info);
796        if (result != TR_OK)
797        {
798            if (result == TR_EDUPLICATE)
799                [self duplicateOpenAlert: [NSString stringWithUTF8String: info.name]];
800            else if (result == TR_EINVALID)
801            {
802                if (type != ADD_AUTO)
803                    [self invalidOpenAlert: [torrentPath lastPathComponent]];
804            }
805            else //this shouldn't happen
806                NSLog(@"Unknown error code (%d) when attempting to open \"%@\"", result, torrentPath);
807           
808            tr_ctorFree(ctor);
809            tr_metainfoFree(&info);
810            continue;
811        }
812       
813        tr_ctorFree(ctor);
814       
815        //determine download location
816        NSString * location;
817        BOOL lockDestination = NO; //don't override the location with a group location if it has a hardcoded path
818        if (path)
819        {
820            location = [path stringByExpandingTildeInPath];
821            lockDestination = YES;
822        }
823        else if ([fDefaults boolForKey: @"DownloadLocationConstant"])
824            location = [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath];
825        else if (type != ADD_URL)
826            location = [torrentPath stringByDeletingLastPathComponent];
827        else
828            location = nil;
829       
830        //determine to show the options window
831        BOOL showWindow = type == ADD_SHOW_OPTIONS || ([fDefaults boolForKey: @"DownloadAsk"]
832                            && (info.isMultifile || ![fDefaults boolForKey: @"DownloadAskMulti"])
833                            && (type != ADD_AUTO || ![fDefaults boolForKey: @"DownloadAskManual"]));
834        tr_metainfoFree(&info);
835       
836        Torrent * torrent;
837        if (!(torrent = [[Torrent alloc] initWithPath: torrentPath location: location
838                            deleteTorrentFile: showWindow ? TORRENT_FILE_SAVE : deleteTorrentFile lib: fLib]))
839            continue;
840       
841        //change the location if the group calls for it (this has to wait until after the torrent is create)
842        if (!lockDestination && [[GroupsController groups] usesCustomDownloadLocationForIndex: [torrent groupValue]])
843        {
844            location = [[GroupsController groups] customDownloadLocationForIndex: [torrent groupValue]];
845            [torrent changeDownloadFolder: location];
846        }
847       
848        //verify the data right away if it was newly created
849        if (type == ADD_CREATED)
850            [torrent resetCache];
851       
852        //add it to the "File -> Open Recent" menu
853        [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
854       
855        //show the add window or add directly
856        if (showWindow || !location)
857        {
858            AddWindowController * addController = [[AddWindowController alloc] initWithTorrent: torrent destination: location
859                                                    lockDestination: lockDestination controller: self deleteTorrent: deleteTorrentFile];
860            [addController showWindow: self];
861        }
862        else
863        {
864            [torrent setWaitToStart: [fDefaults boolForKey: @"AutoStartDownload"]];
865           
866            [torrent update];
867            [fTorrents addObject: torrent];
868            [torrent release];
869        }
870    }
871
872    [self updateTorrentsInQueue];
873}
874
875- (void) askOpenConfirmed: (AddWindowController *) addController add: (BOOL) add
876{
877    Torrent * torrent = [addController torrent];
878    [addController release];
879   
880    if (add)
881    {
882        [torrent update];
883        [fTorrents addObject: torrent];
884        [torrent release];
885       
886        [self updateTorrentsInQueue];
887    }
888    else
889    {
890        [torrent closeRemoveTorrent];
891        [torrent release];
892    }
893}
894
895- (void) openCreatedFile: (NSNotification *) notification
896{
897    NSDictionary * dict = [notification userInfo];
898    [self openFiles: [NSArray arrayWithObject: [dict objectForKey: @"File"]] addType: ADD_CREATED
899                        forcePath: [dict objectForKey: @"Path"]];
900    [dict release];
901}
902
903- (void) incompleteChoiceClosed: (NSOpenPanel *) openPanel returnCode: (NSInteger) code contextInfo: (NSDictionary *) dictionary
904{
905    if (code == NSOKButton)
906        [fDefaults setObject: [[openPanel filenames] objectAtIndex: 0] forKey: @"IncompleteDownloadFolder"];
907    else
908        [fDefaults setBool: NO forKey: @"UseIncompleteDownloadFolder"];
909   
910    [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO];
911}
912
913- (void) downloadChoiceClosed: (NSOpenPanel *) openPanel returnCode: (NSInteger) code contextInfo: (NSDictionary *) dictionary
914{
915    if (code == NSOKButton)
916    {
917        [fDefaults setObject: [[openPanel filenames] objectAtIndex: 0] forKey: @"DownloadFolder"];
918        [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO];
919    }
920    else
921        [dictionary release];
922}
923
924- (void) openFilesWithDict: (NSDictionary *) dictionary
925{
926    [self openFiles: [dictionary objectForKey: @"Filenames"] addType: [[dictionary objectForKey: @"AddType"] intValue] forcePath: nil];
927   
928    [dictionary release];
929}
930
931//called on by applescript
932- (void) open: (NSArray *) files
933{
934    NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: files, @"Filenames",
935                                [NSNumber numberWithInt: ADD_MANUAL], @"AddType", nil];
936    [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dict waitUntilDone: NO];
937}
938
939- (void) openShowSheet: (id) sender
940{
941    NSOpenPanel * panel = [NSOpenPanel openPanel];
942
943    [panel setAllowsMultipleSelection: YES];
944    [panel setCanChooseFiles: YES];
945    [panel setCanChooseDirectories: NO];
946
947    [panel beginSheetForDirectory: nil file: nil types: [NSArray arrayWithObject: @"org.bittorrent.torrent"]
948        modalForWindow: fWindow modalDelegate: self didEndSelector: @selector(openSheetClosed:returnCode:contextInfo:)
949        contextInfo: [NSNumber numberWithBool: sender == fOpenIgnoreDownloadFolder]];
950}
951
952- (void) openSheetClosed: (NSOpenPanel *) panel returnCode: (NSInteger) code contextInfo: (NSNumber *) useOptions
953{
954    if (code == NSOKButton)
955    {
956        NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys: [panel filenames], @"Filenames",
957            [NSNumber numberWithInt: [useOptions boolValue] ? ADD_SHOW_OPTIONS : ADD_MANUAL], @"AddType", nil];
958        [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO];
959    }
960}
961
962- (void) invalidOpenAlert: (NSString *) filename
963{
964    if (![fDefaults boolForKey: @"WarningInvalidOpen"])
965        return;
966   
967    NSAlert * alert = [[NSAlert alloc] init];
968    [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"\"%@\" is not a valid torrent file.",
969                            "Open invalid alert -> title"), filename]];
970    [alert setInformativeText:
971            NSLocalizedString(@"The torrent file cannot be opened because it contains invalid data.",
972                            "Open invalid alert -> message")];
973    [alert setAlertStyle: NSWarningAlertStyle];
974    [alert addButtonWithTitle: NSLocalizedString(@"OK", "Open invalid alert -> button")];
975   
976    [alert runModal];
977    if ([[alert suppressionButton] state] == NSOnState)
978        [fDefaults setBool: NO forKey: @"WarningInvalidOpen"];
979    [alert release];
980}
981
982- (void) duplicateOpenAlert: (NSString *) name
983{
984    if (![fDefaults boolForKey: @"WarningDuplicate"])
985        return;
986   
987    NSAlert * alert = [[NSAlert alloc] init];
988    [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"A transfer of \"%@\" already exists.",
989                            "Open duplicate alert -> title"), name]];
990    [alert setInformativeText:
991            NSLocalizedString(@"The torrent file cannot be opened because it is a duplicate of an already added transfer.",
992                            "Open duplicate alert -> message")];
993    [alert setAlertStyle: NSWarningAlertStyle];
994    [alert addButtonWithTitle: NSLocalizedString(@"OK", "Open duplicate alert -> button")];
995    [alert setShowsSuppressionButton: YES];
996   
997    [alert runModal];
998    if ([[alert suppressionButton] state])
999        [fDefaults setBool: NO forKey: @"WarningDuplicate"];
1000    [alert release];
1001}
1002
1003- (void) openURL: (NSURL *) url
1004{
1005    [[NSURLDownload alloc] initWithRequest: [NSURLRequest requestWithURL: url] delegate: self];
1006}
1007
1008- (void) openURLShowSheet: (id) sender
1009{
1010    [NSApp beginSheet: fURLSheetWindow modalForWindow: fWindow modalDelegate: self
1011            didEndSelector: @selector(urlSheetDidEnd:returnCode:contextInfo:) contextInfo: nil];
1012}
1013
1014- (void) openURLEndSheet: (id) sender
1015{
1016    [fURLSheetWindow orderOut: sender];
1017    [NSApp endSheet: fURLSheetWindow returnCode: 1];
1018}
1019
1020- (void) openURLCancelEndSheet: (id) sender
1021{
1022    [fURLSheetWindow orderOut: sender];
1023    [NSApp endSheet: fURLSheetWindow returnCode: 0];
1024}
1025
1026- (void) controlTextDidChange: (NSNotification *) notification
1027{
1028    if ([notification object] != fURLSheetTextField)
1029        return;
1030   
1031    NSString * string = [fURLSheetTextField stringValue];
1032    BOOL enable = YES;
1033    if ([string isEqualToString: @""])
1034        enable = NO;
1035    else
1036    {
1037        NSRange prefixRange = [string rangeOfString: @"://"];
1038        if (prefixRange.location != NSNotFound && [string length] == NSMaxRange(prefixRange))
1039            enable = NO;
1040    }
1041   
1042    [fURLSheetOpenButton setEnabled: enable];
1043}
1044
1045- (void) urlSheetDidEnd: (NSWindow *) sheet returnCode: (NSInteger) returnCode contextInfo: (void *) contextInfo
1046{
1047    [fURLSheetTextField selectText: self];
1048    if (returnCode != 1)
1049        return;
1050   
1051    NSString * urlString = [fURLSheetTextField stringValue];
1052    if ([urlString rangeOfString: @"://"].location == NSNotFound)
1053    {
1054        if ([urlString rangeOfString: @"."].location == NSNotFound)
1055        {
1056            NSInteger beforeCom;
1057            if ((beforeCom = [urlString rangeOfString: @"/"].location) != NSNotFound)
1058                urlString = [NSString stringWithFormat: @"http://www.%@.com/%@",
1059                                [urlString substringToIndex: beforeCom],
1060                                [urlString substringFromIndex: beforeCom + 1]];
1061            else
1062                urlString = [NSString stringWithFormat: @"http://www.%@.com/", urlString];
1063        }
1064        else
1065            urlString = [@"http://" stringByAppendingString: urlString];
1066    }
1067   
1068    NSURL * url = [NSURL URLWithString: urlString];
1069    [self performSelectorOnMainThread: @selector(openURL:) withObject: url waitUntilDone: NO];
1070}
1071
1072- (void) createFile: (id) sender
1073{
1074    [CreatorWindowController createTorrentFile: fLib];
1075}
1076
1077- (void) resumeSelectedTorrents: (id) sender
1078{
1079    [self resumeTorrents: [fTableView selectedTorrents]];
1080}
1081
1082- (void) resumeAllTorrents: (id) sender
1083{
1084    [self resumeTorrents: fTorrents];
1085}
1086
1087- (void) resumeTorrents: (NSArray *) torrents
1088{
1089    for (Torrent * torrent in torrents)
1090        [torrent setWaitToStart: YES];
1091   
1092    [self updateTorrentsInQueue];
1093}
1094
1095- (void) resumeSelectedTorrentsNoWait:  (id) sender
1096{
1097    [self resumeTorrentsNoWait: [fTableView selectedTorrents]];
1098}
1099
1100- (void) resumeWaitingTorrents: (id) sender
1101{
1102    NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [fTorrents count]];
1103   
1104    for (Torrent * torrent in fTorrents)
1105        if (![torrent isActive] && [torrent waitingToStart])
1106            [torrents addObject: torrent];
1107   
1108    [self resumeTorrentsNoWait: torrents];
1109}
1110
1111- (void) resumeTorrentsNoWait: (NSArray *) torrents
1112{
1113    //iterate through instead of all at once to ensure no conflicts
1114    for (Torrent * torrent in torrents)
1115        [torrent startTransfer];
1116   
1117    [self updateUI];
1118    [self applyFilter: nil];
1119    [self updateTorrentHistory];
1120}
1121
1122- (void) stopSelectedTorrents: (id) sender
1123{
1124    [self stopTorrents: [fTableView selectedTorrents]];
1125}
1126
1127- (void) stopAllTorrents: (id) sender
1128{
1129    [self stopTorrents: fTorrents];
1130}
1131
1132- (void) stopTorrents: (NSArray *) torrents
1133{
1134    //don't want any of these starting then stopping
1135    for (Torrent * torrent in torrents)
1136        [torrent setWaitToStart: NO];
1137
1138    [torrents makeObjectsPerformSelector: @selector(stopTransfer)];
1139   
1140    [self updateUI];
1141    [self applyFilter: nil];
1142    [self updateTorrentHistory];
1143}
1144
1145- (void) removeTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
1146{
1147    [torrents retain];
1148    NSInteger active = 0, downloading = 0;
1149
1150    if ([fDefaults boolForKey: @"CheckRemove"])
1151    {
1152        for (Torrent * torrent in torrents)
1153            if ([torrent isActive])
1154            {
1155                active++;
1156                if (![torrent isSeeding])
1157                    downloading++;
1158            }
1159
1160        if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? downloading > 0 : active > 0)
1161        {
1162            NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
1163                                    torrents, @"Torrents",
1164                                    [NSNumber numberWithBool: deleteData], @"DeleteData",
1165                                    [NSNumber numberWithBool: deleteTorrent], @"DeleteTorrent", nil];
1166           
1167            NSString * title, * message;
1168           
1169            NSInteger selected = [torrents count];
1170            if (selected == 1)
1171            {
1172                NSString * torrentName = [[torrents objectAtIndex: 0] name];
1173               
1174                if (!deleteData && !deleteTorrent)
1175                    title = [NSString stringWithFormat:
1176                                NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list?",
1177                                "Removal confirm panel -> title"), torrentName];
1178                else if (deleteData && !deleteTorrent)
1179                    title = [NSString stringWithFormat:
1180                                NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list"
1181                                " and trash the data file?", "Removal confirm panel -> title"), torrentName];
1182                else if (!deleteData && deleteTorrent)
1183                    title = [NSString stringWithFormat:
1184                                NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list"
1185                                " and trash the torrent file?", "Removal confirm panel -> title"), torrentName];
1186                else
1187                    title = [NSString stringWithFormat:
1188                                NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list"
1189                                " and trash both the data and torrent files?", "Removal confirm panel -> title"), torrentName];
1190               
1191                message = NSLocalizedString(@"This transfer is active."
1192                            " Once removed, continuing the transfer will require the torrent file.",
1193                            "Removal confirm panel -> message");
1194            }
1195            else
1196            {
1197                if (!deleteData && !deleteTorrent)
1198                    title = [NSString stringWithFormat:
1199                                NSLocalizedString(@"Are you sure you want to remove %d transfers from the transfer list?",
1200                                "Removal confirm panel -> title"), selected];
1201                else if (deleteData && !deleteTorrent)
1202                    title = [NSString stringWithFormat:
1203                                NSLocalizedString(@"Are you sure you want to remove %d transfers from the transfer list"
1204                                " and trash the data file?", "Removal confirm panel -> title"), selected];
1205                else if (!deleteData && deleteTorrent)
1206                    title = [NSString stringWithFormat:
1207                                NSLocalizedString(@"Are you sure you want to remove %d transfers from the transfer list"
1208                                " and trash the torrent file?", "Removal confirm panel -> title"), selected];
1209                else
1210                    title = [NSString stringWithFormat:
1211                                NSLocalizedString(@"Are you sure you want to remove %d transfers from the transfer list"
1212                                " and trash both the data and torrent files?", "Removal confirm panel -> title"), selected];
1213               
1214                if (selected == active)
1215                    message = [NSString stringWithFormat: NSLocalizedString(@"There are %d active transfers.",
1216                                "Removal confirm panel -> message part 1"), active];
1217                else
1218                    message = [NSString stringWithFormat: NSLocalizedString(@"There are %d transfers (%d active).",
1219                                "Removal confirm panel -> message part 1"), selected, active];
1220                message = [message stringByAppendingFormat: @" %@",
1221                                NSLocalizedString(@"Once removed, continuing the transfers will require the torrent files.",
1222                                "Removal confirm panel -> message part 2")];
1223            }
1224           
1225            NSBeginAlertSheet(title, NSLocalizedString(@"Remove", "Removal confirm panel -> button"),
1226                NSLocalizedString(@"Cancel", "Removal confirm panel -> button"), nil, fWindow, self,
1227                nil, @selector(removeSheetDidEnd:returnCode:contextInfo:), dict, message);
1228            return;
1229        }
1230    }
1231   
1232    [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
1233}
1234
1235- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (NSInteger) returnCode contextInfo: (NSDictionary *) dict
1236{
1237    NSArray * torrents = [dict objectForKey: @"Torrents"];
1238    if (returnCode == NSAlertDefaultReturn)
1239        [self confirmRemoveTorrents: torrents deleteData: [[dict objectForKey: @"DeleteData"] boolValue]
1240                deleteTorrent: [[dict objectForKey: @"DeleteTorrent"] boolValue]];
1241    else
1242        [torrents release];
1243   
1244    [dict release];
1245}
1246
1247- (void) confirmRemoveTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
1248{
1249    //don't want any of these starting then stopping
1250    for (Torrent * torrent in torrents)
1251        [torrent setWaitToStart: NO];
1252   
1253    [fTorrents removeObjectsInArray: torrents];
1254   
1255    for (Torrent * torrent in torrents)
1256    {
1257        //let's expand all groups that have removed items - they either don't exist anymore, are already expanded, or are collapsed (rpc)
1258        [fTableView removeCollapsedGroup: [torrent groupValue]];
1259       
1260        if (deleteData)
1261            [torrent trashData];
1262        if (deleteTorrent)
1263            [torrent trashTorrent];
1264       
1265        [torrent closeRemoveTorrent];
1266    }
1267   
1268    [torrents release];
1269   
1270    [fTableView deselectAll: nil];
1271   
1272    [self updateTorrentsInQueue];
1273}
1274
1275- (void) removeNoDelete: (id) sender
1276{
1277    [self removeTorrents: [fTableView selectedTorrents] deleteData: NO deleteTorrent: NO];
1278}
1279
1280- (void) removeDeleteData: (id) sender
1281{
1282    [self removeTorrents: [fTableView selectedTorrents] deleteData: YES deleteTorrent: NO];
1283}
1284
1285- (void) removeDeleteTorrent: (id) sender
1286{
1287    [self removeTorrents: [fTableView selectedTorrents] deleteData: NO deleteTorrent: YES];
1288}
1289
1290- (void) removeDeleteDataAndTorrent: (id) sender
1291{
1292    [self removeTorrents: [fTableView selectedTorrents] deleteData: YES deleteTorrent: YES];
1293}
1294
1295- (void) moveDataFilesSelected: (id) sender
1296{
1297    [self moveDataFiles: [fTableView selectedTorrents]];
1298}
1299
1300- (void) moveDataFiles: (NSArray *) torrents
1301{
1302    NSOpenPanel * panel = [NSOpenPanel openPanel];
1303    [panel setPrompt: NSLocalizedString(@"Select", "Move torrent -> prompt")];
1304    [panel setAllowsMultipleSelection: NO];
1305    [panel setCanChooseFiles: NO];
1306    [panel setCanChooseDirectories: YES];
1307    [panel setCanCreateDirectories: YES];
1308   
1309    torrents = [torrents retain];
1310    NSInteger count = [torrents count];
1311    if (count == 1)
1312        [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the new folder for \"%@\".",
1313                            "Move torrent -> select destination folder"), [[torrents objectAtIndex: 0] name]]];
1314    else
1315        [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the new folder for %d data files.",
1316                            "Move torrent -> select destination folder"), count]];
1317       
1318    [panel beginSheetForDirectory: nil file: nil modalForWindow: fWindow modalDelegate: self
1319        didEndSelector: @selector(moveDataFileChoiceClosed:returnCode:contextInfo:) contextInfo: torrents];
1320}
1321
1322- (void) moveDataFileChoiceClosed: (NSOpenPanel *) panel returnCode: (NSInteger) code contextInfo: (NSArray *) torrents
1323{
1324    if (code == NSOKButton)
1325    {
1326        for (Torrent * torrent in torrents)
1327            [torrent moveTorrentDataFileTo: [[panel filenames] objectAtIndex: 0]];
1328    }
1329   
1330    [torrents release];
1331}
1332
1333- (void) copyTorrentFiles: (id) sender
1334{
1335    [self copyTorrentFileForTorrents: [[NSMutableArray alloc] initWithArray: [fTableView selectedTorrents]]];
1336}
1337
1338- (void) copyTorrentFileForTorrents: (NSMutableArray *) torrents
1339{
1340    if ([torrents count] <= 0)
1341    {
1342        [torrents release];
1343        return;
1344    }
1345
1346    Torrent * torrent = [torrents objectAtIndex: 0];
1347
1348    //warn user if torrent file can't be found
1349    if (![[NSFileManager defaultManager] fileExistsAtPath: [torrent torrentLocation]])
1350    {
1351        NSAlert * alert = [[NSAlert alloc] init];
1352        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent file copy alert -> button")];
1353        [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Copy of \"%@\" Cannot Be Created",
1354                                "Torrent file copy alert -> title"), [torrent name]]];
1355        [alert setInformativeText: [NSString stringWithFormat:
1356                NSLocalizedString(@"The torrent file (%@) cannot be found.", "Torrent file copy alert -> message"),
1357                                    [torrent torrentLocation]]];
1358        [alert setAlertStyle: NSWarningAlertStyle];
1359       
1360        [alert runModal];
1361        [alert release];
1362       
1363        [torrents removeObjectAtIndex: 0];
1364        [self copyTorrentFileForTorrents: torrents];
1365    }
1366    else
1367    {
1368        NSSavePanel * panel = [NSSavePanel savePanel];
1369        [panel setRequiredFileType: @"org.bittorrent.torrent"];
1370        [panel setCanSelectHiddenExtension: YES];
1371       
1372        [panel beginSheetForDirectory: nil file: [torrent name] modalForWindow: fWindow modalDelegate: self
1373            didEndSelector: @selector(saveTorrentCopySheetClosed:returnCode:contextInfo:) contextInfo: torrents];
1374    }
1375}
1376
1377- (void) saveTorrentCopySheetClosed: (NSSavePanel *) panel returnCode: (NSInteger) code contextInfo: (NSMutableArray *) torrents
1378{
1379    //copy torrent to new location with name of data file
1380    if (code == NSOKButton)
1381        [[torrents objectAtIndex: 0] copyTorrentFileTo: [panel filename]];
1382   
1383    [torrents removeObjectAtIndex: 0];
1384    [self performSelectorOnMainThread: @selector(copyTorrentFileForTorrents:) withObject: torrents waitUntilDone: NO];
1385}
1386
1387- (void) revealFile: (id) sender
1388{
1389    for (Torrent * torrent in [fTableView selectedTorrents])
1390        [torrent revealData];
1391}
1392
1393- (void) announceSelectedTorrents: (id) sender
1394{
1395    for (Torrent * torrent in [fTableView selectedTorrents])
1396    {
1397        if ([torrent canManualAnnounce])
1398            [torrent manualAnnounce];
1399    }
1400}
1401
1402- (void) verifySelectedTorrents: (id) sender
1403{
1404    [self verifyTorrents: [fTableView selectedTorrents]];
1405}
1406
1407- (void) verifyTorrents: (NSArray *) torrents
1408{
1409    for (Torrent * torrent in torrents)
1410        [torrent resetCache];
1411   
1412    [self applyFilter: nil];
1413}
1414
1415- (void) showPreferenceWindow: (id) sender
1416{
1417    NSWindow * window = [fPrefsController window];
1418    if (![window isVisible])
1419        [window center];
1420
1421    [window makeKeyAndOrderFront: nil];
1422}
1423
1424- (void) showAboutWindow: (id) sender
1425{
1426    [[AboutWindowController aboutController] showWindow: nil];
1427}
1428
1429- (void) showInfo: (id) sender
1430{
1431    if ([[fInfoController window] isVisible])
1432        [fInfoController close];
1433    else
1434    {
1435        [fInfoController updateInfoStats];
1436        [[fInfoController window] orderFront: nil];
1437    }
1438}
1439
1440- (void) resetInfo
1441{
1442    [fInfoController setInfoForTorrents: [fTableView selectedTorrents]];
1443    [[QuickLookController quickLook] updateQuickLook];
1444}
1445
1446- (void) setInfoTab: (id) sender
1447{
1448    if (sender == fNextInfoTabItem)
1449        [fInfoController setNextTab];
1450    else
1451        [fInfoController setPreviousTab];
1452}
1453
1454- (void) showMessageWindow: (id) sender
1455{
1456    [fMessageController showWindow: nil];
1457}
1458
1459- (void) showStatsWindow: (id) sender
1460{
1461    [[StatsWindowController statsWindow: fLib] showWindow: nil];
1462}
1463
1464- (void) updateUI
1465{
1466    [fTorrents makeObjectsPerformSelector: @selector(update)];
1467   
1468    if (![NSApp isHidden])
1469    {
1470        if ([fWindow isVisible])
1471        {
1472            [self sortTorrents];
1473           
1474            //update status bar
1475            if (![fStatusBar isHidden])
1476            {
1477                //set rates
1478                [fTotalDLField setStringValue: [NSString stringForSpeed: tr_sessionGetPieceSpeed(fLib, TR_DOWN)]];
1479                [fTotalULField setStringValue: [NSString stringForSpeed: tr_sessionGetPieceSpeed(fLib, TR_UP)]];
1480               
1481                //set status button text
1482                NSString * statusLabel = [fDefaults stringForKey: @"StatusLabel"], * statusString;
1483                BOOL total;
1484                if ((total = [statusLabel isEqualToString: STATUS_RATIO_TOTAL]) || [statusLabel isEqualToString: STATUS_RATIO_SESSION])
1485                {
1486                    tr_session_stats stats;
1487                    if (total)
1488                        tr_sessionGetCumulativeStats(fLib, &stats);
1489                    else
1490                        tr_sessionGetStats(fLib, &stats);
1491                   
1492                    statusString = [NSLocalizedString(@"Ratio", "status bar -> status label") stringByAppendingFormat: @": %@",
1493                                    [NSString stringForRatio: stats.ratio]];
1494                }
1495                else //STATUS_TRANSFER_TOTAL or STATUS_TRANSFER_SESSION
1496                {
1497                    total = [statusLabel isEqualToString: STATUS_TRANSFER_TOTAL];
1498                   
1499                    tr_session_stats stats;
1500                    if (total)
1501                        tr_sessionGetCumulativeStats(fLib, &stats);
1502                    else
1503                        tr_sessionGetStats(fLib, &stats);
1504                   
1505                    statusString = [NSString stringWithFormat: @"%@: %@  %@: %@",
1506                            NSLocalizedString(@"DL", "status bar -> status label"), [NSString stringForFileSize: stats.downloadedBytes],
1507                            NSLocalizedString(@"UL", "status bar -> status label"), [NSString stringForFileSize: stats.uploadedBytes]];
1508                }
1509               
1510                [fStatusButton setTitle: statusString];
1511                [self resizeStatusButton];
1512            }
1513        }
1514
1515        //update non-constant parts of info window
1516        if ([[fInfoController window] isVisible])
1517            [fInfoController updateInfoStats];
1518    }
1519   
1520    //badge dock
1521    [fBadger updateBadge];
1522}
1523
1524- (void) resizeStatusButton
1525{
1526    [fStatusButton sizeToFit];
1527   
1528    //width ends up being too long
1529    NSRect statusFrame = [fStatusButton frame];
1530    statusFrame.size.width -= 25.0;
1531   
1532    CGFloat difference = NSMaxX(statusFrame) + 5.0 - [fTotalDLImageView frame].origin.x;
1533    if (difference > 0)
1534        statusFrame.size.width -= difference;
1535   
1536    [fStatusButton setFrame: statusFrame];
1537}
1538
1539- (void) setBottomCountText: (BOOL) filtering
1540{
1541    NSString * totalTorrentsString;
1542    NSInteger totalCount = [fTorrents count];
1543    if (totalCount != 1)
1544        totalTorrentsString = [NSString stringWithFormat: NSLocalizedString(@"%d transfers", "Status bar transfer count"), totalCount];
1545    else
1546        totalTorrentsString = NSLocalizedString(@"1 transfer", "Status bar transfer count");
1547   
1548    if (filtering)
1549    {
1550        NSInteger count = [fTableView numberOfRows]; //have to factor in collapsed rows
1551        if (count > 0 && ![[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [Torrent class]])
1552            count -= [fDisplayedTorrents count];
1553       
1554        totalTorrentsString = [NSString stringWithFormat: NSLocalizedString(@"%d of %@", "Status bar transfer count"),
1555                                count, totalTorrentsString];
1556    }
1557   
1558    [fTotalTorrentsField setStringValue: totalTorrentsString];
1559}
1560
1561- (void) updateSpeedFieldsToolTips
1562{
1563    NSString * uploadText, * downloadText;
1564   
1565    if ([fDefaults boolForKey: @"SpeedLimit"])
1566    {
1567        NSString * speedString = [NSString stringWithFormat: @"%@ (%@)", NSLocalizedString(@"%d KB/s", "Status Bar -> speed tooltip"),
1568                                    NSLocalizedString(@"Speed Limit", "Status Bar -> speed tooltip")];
1569       
1570        uploadText = [NSString stringWithFormat: speedString, [fDefaults integerForKey: @"SpeedLimitUploadLimit"]];
1571        downloadText = [NSString stringWithFormat: speedString, [fDefaults integerForKey: @"SpeedLimitDownloadLimit"]];
1572    }
1573    else
1574    {
1575        if ([fDefaults boolForKey: @"CheckUpload"])
1576            uploadText = [NSString stringWithFormat: NSLocalizedString(@"%d KB/s", "Status Bar -> speed tooltip"),
1577                            [fDefaults integerForKey: @"UploadLimit"]];
1578        else
1579            uploadText = NSLocalizedString(@"unlimited", "Status Bar -> speed tooltip");
1580       
1581        if ([fDefaults boolForKey: @"CheckDownload"])
1582            downloadText = [NSString stringWithFormat: NSLocalizedString(@"%d KB/s", "Status Bar -> speed tooltip"),
1583                            [fDefaults integerForKey: @"DownloadLimit"]];
1584        else
1585            downloadText = NSLocalizedString(@"unlimited", "Status Bar -> speed tooltip");
1586    }
1587   
1588    uploadText = [NSLocalizedString(@"Total upload rate", "Status Bar -> speed tooltip")
1589                    stringByAppendingFormat: @": %@", uploadText];
1590    downloadText = [NSLocalizedString(@"Total download rate", "Status Bar -> speed tooltip")
1591                    stringByAppendingFormat: @": %@", downloadText];
1592   
1593    [fTotalULField setToolTip: uploadText];
1594    [fTotalDLField setToolTip: downloadText];
1595}
1596
1597- (void) updateTorrentsInQueue
1598{
1599    BOOL download = [fDefaults boolForKey: @"Queue"],
1600        seed = [fDefaults boolForKey: @"QueueSeed"];
1601   
1602    NSInteger desiredDownloadActive = [self numToStartFromQueue: YES],
1603        desiredSeedActive = [self numToStartFromQueue: NO];
1604   
1605    for (Torrent * torrent in fTorrents)
1606    {
1607        if (![torrent isActive] && ![torrent isChecking] && [torrent waitingToStart])
1608        {
1609            if (![torrent allDownloaded])
1610            {
1611                if (!download || desiredDownloadActive > 0)
1612                {
1613                    [torrent startTransfer];
1614                    if ([torrent isActive])
1615                        desiredDownloadActive--;
1616                    [torrent update];
1617                }
1618            }
1619            else
1620            {
1621                if (!seed || desiredSeedActive > 0)
1622                {
1623                    [torrent startTransfer];
1624                    if ([torrent isActive])
1625                        desiredSeedActive--;
1626                    [torrent update];
1627                }
1628            }
1629        }
1630    }
1631   
1632    [self updateUI];
1633    [self applyFilter: nil];
1634    [self updateTorrentHistory];
1635}
1636
1637- (NSInteger) numToStartFromQueue: (BOOL) downloadQueue
1638{
1639    if (![fDefaults boolForKey: downloadQueue ? @"Queue" : @"QueueSeed"])
1640        return 0;
1641   
1642    NSInteger desired = [fDefaults integerForKey: downloadQueue ? @"QueueDownloadNumber" : @"QueueSeedNumber"];
1643       
1644    for (Torrent * torrent in fTorrents)
1645    {
1646        if ([torrent isChecking])
1647        {
1648            desired--;
1649            if (desired <= 0)
1650                return 0;
1651        }
1652        else if ([torrent isActive] && ![torrent isStalled] && ![torrent isError])
1653        {
1654            if ([torrent allDownloaded] != downloadQueue)
1655            {
1656                desired--;
1657                if (desired <= 0)
1658                    return 0;
1659            }
1660        }
1661        else;
1662    }
1663   
1664    return desired;
1665}
1666
1667- (void) torrentFinishedDownloading: (NSNotification *) notification
1668{
1669    Torrent * torrent = [notification object];
1670   
1671    if ([torrent isActive])
1672    {
1673        if (!fSoundPlaying && [fDefaults boolForKey: @"PlayDownloadSound"])
1674        {
1675            NSSound * sound;
1676            if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"DownloadSound"]]))
1677            {
1678                [sound setDelegate: self];
1679                fSoundPlaying = YES;
1680                [sound play];
1681            }
1682        }
1683       
1684        NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_DOWNLOAD_COMPLETE, @"Type",
1685                                        [torrent dataLocation] , @"Location", nil];
1686        [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Download Complete", "Growl notification title")
1687                                    description: [torrent name] notificationName: GROWL_DOWNLOAD_COMPLETE
1688                                    iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
1689       
1690        if (![fWindow isMainWindow])
1691            [fBadger incrementCompleted];
1692       
1693        //bounce download stack
1694        [[NSDistributedNotificationCenter defaultCenter] postNotificationName: @"com.apple.DownloadFileFinished"
1695            object: [[torrent downloadFolder] stringByAppendingPathComponent: [torrent name]]];
1696       
1697        if ([fDefaults boolForKey: @"QueueSeed"] && [self numToStartFromQueue: NO] <= 0)
1698        {
1699            [torrent stopTransfer];
1700            [torrent setWaitToStart: YES];
1701        }
1702    }
1703   
1704    [self updateTorrentsInQueue];
1705}
1706
1707- (void) torrentRestartedDownloading: (NSNotification *) notification
1708{
1709    Torrent * torrent = [notification object];
1710    if ([torrent isActive])
1711    {
1712        if ([fDefaults boolForKey: @"Queue"] && [self numToStartFromQueue: YES] <= 0)
1713        {
1714            [torrent stopTransfer];
1715            [torrent setWaitToStart: YES];
1716        }
1717    }
1718   
1719    [self updateTorrentsInQueue];
1720}
1721
1722- (void) torrentStoppedForRatio: (NSNotification *) notification
1723{
1724    Torrent * torrent = [notification object];
1725   
1726    [self updateTorrentsInQueue];
1727   
1728    if ([[fTableView selectedTorrents] containsObject: torrent])
1729    {
1730        [fInfoController updateInfoStats];
1731        [fInfoController updateOptions];
1732    }
1733   
1734    if (!fSoundPlaying && [fDefaults boolForKey: @"PlaySeedingSound"])
1735    {
1736        NSSound * sound;
1737        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]]))
1738        {
1739            [sound setDelegate: self];
1740            fSoundPlaying = YES;
1741            [sound play];
1742        }
1743    }
1744   
1745    NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_SEEDING_COMPLETE, @"Type",
1746                                    [torrent dataLocation], @"Location", nil];
1747    [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Seeding Complete", "Growl notification title")
1748                        description: [torrent name] notificationName: GROWL_SEEDING_COMPLETE
1749                        iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
1750}
1751
1752- (void) updateTorrentHistory
1753{
1754    NSMutableArray * history = [NSMutableArray arrayWithCapacity: [fTorrents count]];
1755   
1756    for (Torrent * torrent in fTorrents)
1757        [history addObject: [torrent history]];
1758   
1759    [history writeToFile: [NSHomeDirectory() stringByAppendingPathComponent: SUPPORT_FOLDER] atomically: YES];
1760}
1761
1762- (void) setSort: (id) sender
1763{
1764    NSString * sortType;
1765    switch ([sender tag])
1766    {
1767        case SORT_ORDER_TAG:
1768            sortType = SORT_ORDER;
1769            [fDefaults setBool: NO forKey: @"SortReverse"];
1770            break;
1771        case SORT_DATE_TAG:
1772            sortType = SORT_DATE;
1773            break;
1774        case SORT_NAME_TAG:
1775            sortType = SORT_NAME;
1776            break;
1777        case SORT_PROGRESS_TAG:
1778            sortType = SORT_PROGRESS;
1779            break;
1780        case SORT_STATE_TAG:
1781            sortType = SORT_STATE;
1782            break;
1783        case SORT_TRACKER_TAG:
1784            sortType = SORT_TRACKER;
1785            break;
1786        case SORT_ACTIVITY_TAG:
1787            sortType = SORT_ACTIVITY;
1788            break;
1789        default:
1790            return;
1791    }
1792   
1793    [fDefaults setObject: sortType forKey: @"Sort"];
1794    [self sortTorrents];
1795}
1796
1797- (void) setSortByGroup: (id) sender
1798{
1799    BOOL sortByGroup = ![fDefaults boolForKey: @"SortByGroup"];
1800    [fDefaults setBool: sortByGroup forKey: @"SortByGroup"];
1801   
1802    //expand all groups
1803    if (sortByGroup)
1804        [fTableView removeAllCollapsedGroups];
1805   
1806    [self applyFilter: nil];
1807}
1808
1809- (void) setSortReverse: (id) sender
1810{
1811    [fDefaults setBool: ![fDefaults boolForKey: @"SortReverse"] forKey: @"SortReverse"];
1812    [self sortTorrents];
1813}
1814
1815- (void) sortTorrents
1816{
1817    NSArray * selectedValues = [fTableView selectedValues];
1818   
1819    [self sortTorrentsIgnoreSelected]; //actually sort
1820   
1821    [fTableView selectValues: selectedValues];
1822}
1823
1824- (void) sortTorrentsIgnoreSelected
1825{
1826    NSString * sortType = [fDefaults stringForKey: @"Sort"];
1827   
1828    if (![sortType isEqualToString: SORT_ORDER])
1829    {
1830        const BOOL asc = ![fDefaults boolForKey: @"SortReverse"];
1831       
1832        NSArray * descriptors;
1833        NSSortDescriptor * nameDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"name" ascending: asc
1834                                                selector: @selector(compareFinder:)] autorelease];
1835       
1836        if ([sortType isEqualToString: SORT_STATE])
1837        {
1838            NSSortDescriptor * stateDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"stateSortKey" ascending: !asc] autorelease],
1839                            * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"progress" ascending: !asc] autorelease],
1840                            * ratioDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"ratio" ascending: !asc] autorelease];
1841           
1842            descriptors = [[NSArray alloc] initWithObjects: stateDescriptor, progressDescriptor, ratioDescriptor,
1843                                                                nameDescriptor, nil];
1844        }
1845        else if ([sortType isEqualToString: SORT_PROGRESS])
1846        {
1847            NSSortDescriptor * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"progress" ascending: asc] autorelease],
1848                            * ratioProgressDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"progressStopRatio"
1849                                                            ascending: asc] autorelease],
1850                            * ratioDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"ratio" ascending: asc] autorelease];
1851           
1852            descriptors = [[NSArray alloc] initWithObjects: progressDescriptor, ratioProgressDescriptor, ratioDescriptor,
1853                                                                nameDescriptor, nil];
1854        }
1855        else if ([sortType isEqualToString: SORT_TRACKER])
1856        {
1857            NSSortDescriptor * trackerDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"trackerAddressAnnounce" ascending: asc
1858                                                    selector: @selector(localizedCaseInsensitiveCompare:)] autorelease];
1859           
1860            descriptors = [[NSArray alloc] initWithObjects: trackerDescriptor, nameDescriptor, nil];
1861        }
1862        else if ([sortType isEqualToString: SORT_ACTIVITY])
1863        {
1864            NSSortDescriptor * rateDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"totalRate" ascending: !asc] autorelease];
1865            NSSortDescriptor * activityDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"dateActivityOrAdd" ascending: !asc]
1866                                                        autorelease];
1867           
1868            descriptors = [[NSArray alloc] initWithObjects: rateDescriptor, activityDescriptor, nameDescriptor, nil];
1869        }
1870        else if ([sortType isEqualToString: SORT_DATE])
1871        {
1872            NSSortDescriptor * dateDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"dateAdded" ascending: asc] autorelease];
1873       
1874            descriptors = [[NSArray alloc] initWithObjects: dateDescriptor, nameDescriptor, nil];
1875        }
1876        else
1877            descriptors = [[NSArray alloc] initWithObjects: nameDescriptor, nil];
1878       
1879        //actually sort
1880        if ([fDefaults boolForKey: @"SortByGroup"])
1881        {
1882            for (TorrentGroup * group in fDisplayedTorrents)
1883                [[group torrents] sortUsingDescriptors: descriptors];
1884        }
1885        else
1886            [fDisplayedTorrents sortUsingDescriptors: descriptors];
1887       
1888        [descriptors release];
1889    }
1890   
1891    [fTableView reloadData];
1892}
1893
1894- (void) applyFilter: (id) sender
1895{
1896    //get all the torrents in the table
1897    NSMutableArray * previousTorrents;
1898    if ([fDisplayedTorrents count] > 0 && [[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [TorrentGroup class]])
1899    {
1900        previousTorrents = [NSMutableArray array];
1901       
1902        for (TorrentGroup * group in fDisplayedTorrents)
1903            [previousTorrents addObjectsFromArray: [group torrents]];
1904    }
1905    else
1906        previousTorrents = fDisplayedTorrents;
1907   
1908    NSArray * selectedValues = [fTableView selectedValues];
1909   
1910    NSUInteger active = 0, downloading = 0, seeding = 0, paused = 0;
1911    NSString * filterType = [fDefaults stringForKey: @"Filter"];
1912    BOOL filterActive = NO, filterDownload = NO, filterSeed = NO, filterPause = NO, filterStatus = YES;
1913    if ([filterType isEqualToString: FILTER_ACTIVE])
1914        filterActive = YES;
1915    else if ([filterType isEqualToString: FILTER_DOWNLOAD])
1916        filterDownload = YES;
1917    else if ([filterType isEqualToString: FILTER_SEED])
1918        filterSeed = YES;
1919    else if ([filterType isEqualToString: FILTER_PAUSE])
1920        filterPause = YES;
1921    else
1922        filterStatus = NO;
1923   
1924    const NSInteger groupFilterValue = [fDefaults integerForKey: @"FilterGroup"];
1925    const BOOL filterGroup = groupFilterValue != GROUP_FILTER_ALL_TAG;
1926   
1927    NSString * searchString = [fSearchFilterField stringValue];
1928    const BOOL filterText = [searchString length] > 0,
1929            filterTracker = filterText && [[fDefaults stringForKey: @"FilterSearchType"] isEqualToString: FILTER_TYPE_TRACKER];
1930   
1931    NSMutableArray * allTorrents = [NSMutableArray arrayWithCapacity: [fTorrents count]];
1932   
1933    //get count of each type
1934    for (Torrent * torrent in fTorrents)
1935    {
1936        //check status
1937        if ([torrent isActive] && ![torrent isCheckingWaiting])
1938        {
1939            if ([torrent isSeeding])
1940            {
1941                seeding++;
1942                BOOL isActive = ![torrent isStalled];
1943                if (isActive)
1944                    active++;
1945               
1946                if (filterStatus && !((filterActive && isActive) || filterSeed))
1947                    continue;
1948            }
1949            else
1950            {
1951                downloading++;
1952                BOOL isActive = ![torrent isStalled];
1953                if (isActive)
1954                    active++;
1955               
1956                if (filterStatus && !((filterActive && isActive) || filterDownload))
1957                    continue;
1958            }
1959        }
1960        else
1961        {
1962            paused++;
1963            if (filterStatus && !filterPause)
1964                continue;
1965        }
1966       
1967        //checkGroup
1968        if (filterGroup)
1969            if ([torrent groupValue] != groupFilterValue)
1970                continue;
1971       
1972        //check text field
1973        if (filterText)
1974        {
1975            if (filterTracker)
1976            {
1977                BOOL removeTextField = YES;
1978                for (NSString * tracker in [torrent allTrackers: NO])
1979                {
1980                    if ([tracker rangeOfString: searchString options: NSCaseInsensitiveSearch].location != NSNotFound)
1981                    {
1982                        removeTextField = NO;
1983                        break;
1984                    }
1985                }
1986               
1987                if (removeTextField)
1988                    continue;
1989            }
1990            else
1991            {
1992                if ([[torrent name] rangeOfString: searchString options: NSCaseInsensitiveSearch].location == NSNotFound)
1993                    continue;
1994            }
1995        }
1996       
1997        [allTorrents addObject: torrent];
1998    }
1999   
2000    //set button tooltips
2001    [fNoFilterButton setCount: [fTorrents count]];
2002    [fActiveFilterButton setCount: active];
2003    [fDownloadFilterButton setCount: downloading];
2004    [fSeedFilterButton setCount: seeding];
2005    [fPauseFilterButton setCount: paused];
2006   
2007    //clear display cache for not-shown torrents
2008    [previousTorrents removeObjectsInArray: allTorrents];
2009    for (Torrent * torrent in previousTorrents)
2010        [torrent setPreviousFinishedPieces: nil];
2011   
2012    //place torrents into groups
2013    const BOOL groupRows = [fDefaults boolForKey: @"SortByGroup"];
2014    if (groupRows)
2015    {
2016        NSMutableArray * oldTorrentGroups = [NSMutableArray array];
2017        if ([fDisplayedTorrents count] > 0 && [[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [TorrentGroup class]])
2018            [oldTorrentGroups addObjectsFromArray: fDisplayedTorrents];
2019       
2020        [fDisplayedTorrents removeAllObjects];
2021       
2022        NSSortDescriptor * groupDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"groupOrderValue" ascending: YES] autorelease];
2023        [allTorrents sortUsingDescriptors: [NSArray arrayWithObject: groupDescriptor]];
2024       
2025        NSMutableArray * groupTorrents;
2026        NSInteger lastGroupValue = -2, currentOldGroupIndex = 0;
2027        for (Torrent * torrent in allTorrents)
2028        {
2029            NSInteger groupValue = [torrent groupValue];
2030            if (groupValue != lastGroupValue)
2031            {
2032                lastGroupValue = groupValue;
2033               
2034                TorrentGroup * group = nil;
2035               
2036                //try to see if the group already exists
2037                for (; currentOldGroupIndex < [oldTorrentGroups count]; currentOldGroupIndex++)
2038                {
2039                    TorrentGroup * currentGroup = [oldTorrentGroups objectAtIndex: currentOldGroupIndex];
2040                    const NSInteger currentGroupValue = [currentGroup groupIndex];
2041                    if (currentGroupValue == groupValue)
2042                    {
2043                        group = currentGroup;
2044                        [[currentGroup torrents] removeAllObjects];
2045                       
2046                        currentOldGroupIndex++;
2047                    }
2048                   
2049                    if (currentGroupValue >= groupValue)
2050                        break;
2051                }
2052               
2053                if (!group)
2054                    group = [[[TorrentGroup alloc] initWithGroup: groupValue] autorelease];
2055                [fDisplayedTorrents addObject: group];
2056               
2057                groupTorrents = [group torrents];
2058            }
2059           
2060            [groupTorrents addObject: torrent];
2061        }
2062    }
2063    else
2064        [fDisplayedTorrents setArray: allTorrents];
2065   
2066    //actually sort
2067    [self sortTorrentsIgnoreSelected];
2068   
2069    //reset expanded/collapsed rows
2070    if (groupRows)
2071    {
2072        for (TorrentGroup * group in fDisplayedTorrents)
2073        {
2074            if ([fTableView isGroupCollapsed: [group groupIndex]])
2075                [fTableView collapseItem: group];
2076            else
2077                [fTableView expandItem: group];
2078        }
2079    }
2080   
2081    [fTableView selectValues: selectedValues];
2082    [self resetInfo]; //if group is already selected, but the torrents in it change
2083   
2084    [self setBottomCountText: groupRows || filterStatus || filterGroup || filterText];
2085   
2086    [self setWindowSizeToFit];
2087}
2088
2089//resets filter and sorts torrents
2090- (void) setFilter: (id) sender
2091{
2092    NSString * oldFilterType = [fDefaults stringForKey: @"Filter"];
2093   
2094    NSButton * prevFilterButton;
2095    if ([oldFilterType isEqualToString: FILTER_PAUSE])
2096        prevFilterButton = fPauseFilterButton;
2097    else if ([oldFilterType isEqualToString: FILTER_ACTIVE])
2098        prevFilterButton = fActiveFilterButton;
2099    else if ([oldFilterType isEqualToString: FILTER_SEED])
2100        prevFilterButton = fSeedFilterButton;
2101    else if ([oldFilterType isEqualToString: FILTER_DOWNLOAD])
2102        prevFilterButton = fDownloadFilterButton;
2103    else
2104        prevFilterButton = fNoFilterButton;
2105   
2106    if (sender != prevFilterButton)
2107    {
2108        [prevFilterButton setState: NSOffState];
2109        [sender setState: NSOnState];
2110
2111        NSString * filterType;
2112        if (sender == fActiveFilterButton)
2113            filterType = FILTER_ACTIVE;
2114        else if (sender == fDownloadFilterButton)
2115            filterType = FILTER_DOWNLOAD;
2116        else if (sender == fPauseFilterButton)
2117            filterType = FILTER_PAUSE;
2118        else if (sender == fSeedFilterButton)
2119            filterType = FILTER_SEED;
2120        else
2121            filterType = FILTER_NONE;
2122
2123        [fDefaults setObject: filterType forKey: @"Filter"];
2124    }
2125    else
2126        [sender setState: NSOnState];
2127
2128    [self applyFilter: nil];
2129}
2130
2131- (void) setFilterSearchType: (id) sender
2132{
2133    NSString * oldFilterType = [fDefaults stringForKey: @"FilterSearchType"];
2134   
2135    NSInteger prevTag, currentTag = [sender tag];
2136    if ([oldFilterType isEqualToString: FILTER_TYPE_TRACKER])
2137        prevTag = FILTER_TYPE_TAG_TRACKER;
2138    else
2139        prevTag = FILTER_TYPE_TAG_NAME;
2140   
2141    if (currentTag != prevTag)
2142    {
2143        NSString * filterType;
2144        if (currentTag == FILTER_TYPE_TAG_TRACKER)
2145            filterType = FILTER_TYPE_TRACKER;
2146        else
2147            filterType = FILTER_TYPE_NAME;
2148       
2149        [fDefaults setObject: filterType forKey: @"FilterSearchType"];
2150       
2151        [[fSearchFilterField cell] setPlaceholderString: [sender title]];
2152    }
2153   
2154    [self applyFilter: nil];
2155}
2156
2157- (void) switchFilter: (id) sender
2158{
2159    NSString * filterType = [fDefaults stringForKey: @"Filter"];
2160   
2161    NSButton * button;
2162    if ([filterType isEqualToString: FILTER_NONE])
2163        button = sender == fNextFilterItem ? fActiveFilterButton : fPauseFilterButton;
2164    else if ([filterType isEqualToString: FILTER_ACTIVE])
2165        button = sender == fNextFilterItem ? fDownloadFilterButton : fNoFilterButton;
2166    else if ([filterType isEqualToString: FILTER_DOWNLOAD])
2167        button = sender == fNextFilterItem ? fSeedFilterButton : fActiveFilterButton;
2168    else if ([filterType isEqualToString: FILTER_SEED])
2169        button = sender == fNextFilterItem ? fPauseFilterButton : fDownloadFilterButton;
2170    else if ([filterType isEqualToString: FILTER_PAUSE])
2171        button = sender == fNextFilterItem ? fNoFilterButton : fSeedFilterButton;
2172    else
2173        button = fNoFilterButton;
2174   
2175    [self setFilter: button];
2176}
2177
2178- (void) setStatusLabel: (id) sender
2179{
2180    NSString * statusLabel;
2181    switch ([sender tag])
2182    {
2183        case STATUS_RATIO_TOTAL_TAG:
2184            statusLabel = STATUS_RATIO_TOTAL;
2185            break;
2186        case STATUS_RATIO_SESSION_TAG:
2187            statusLabel = STATUS_RATIO_SESSION;
2188            break;
2189        case STATUS_TRANSFER_TOTAL_TAG:
2190            statusLabel = STATUS_TRANSFER_TOTAL;
2191            break;
2192        case STATUS_TRANSFER_SESSION_TAG:
2193            statusLabel = STATUS_TRANSFER_SESSION;
2194            break;
2195        default:
2196            return;
2197    }
2198   
2199    [fDefaults setObject: statusLabel forKey: @"StatusLabel"];
2200    [self updateUI];
2201}
2202
2203- (void) menuNeedsUpdate: (NSMenu *) menu
2204{
2205    if (menu == fGroupsSetMenu || menu == fGroupsSetContextMenu || menu == fGroupFilterMenu)
2206    {
2207        const BOOL filter = menu == fGroupFilterMenu;
2208       
2209        const NSInteger remaining = filter ? 3 : 0;
2210        for (NSInteger i = [menu numberOfItems]-1; i >= remaining; i--)
2211            [menu removeItemAtIndex: i];
2212       
2213        NSMenu * groupMenu;
2214        if (!filter)
2215            groupMenu = [[GroupsController groups] groupMenuWithTarget: self action: @selector(setGroup:) isSmall: NO];
2216        else
2217            groupMenu = [[GroupsController groups] groupMenuWithTarget: self action: @selector(setGroupFilter:) isSmall: YES];
2218       
2219        const NSInteger groupMenuCount = [groupMenu numberOfItems];
2220        for (NSInteger i = 0; i < groupMenuCount; i++)
2221        {
2222            NSMenuItem * item = [[groupMenu itemAtIndex: 0] retain];
2223            [groupMenu removeItemAtIndex: 0];
2224            [menu addItem: item];
2225            [item release];
2226        }
2227    }
2228    else if (menu == fUploadMenu || menu == fDownloadMenu)
2229    {
2230        if ([menu numberOfItems] > 3)
2231            return;
2232       
2233        const NSInteger speedLimitActionValue[] = { 5, 10, 20, 30, 40, 50, 75, 100, 150, 200, 250, 500, 750, -1 };
2234       
2235        NSMenuItem * item;
2236        for (NSInteger i = 0; speedLimitActionValue[i] != -1; i++)
2237        {
2238            item = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: NSLocalizedString(@"%d KB/s",
2239                    "Action menu -> upload/download limit"), speedLimitActionValue[i]] action: @selector(setQuickLimitGlobal:)
2240                    keyEquivalent: @""];
2241            [item setTarget: self];
2242            [item setRepresentedObject: [NSNumber numberWithInt: speedLimitActionValue[i]]];
2243            [menu addItem: item];
2244            [item release];
2245        }
2246    }
2247    else if (menu == fRatioStopMenu)
2248    {
2249        if ([menu numberOfItems] > 3)
2250            return;
2251       
2252        const float ratioLimitActionValue[] = { 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, -1 };
2253       
2254        NSMenuItem * item;
2255        for (NSInteger i = 0; ratioLimitActionValue[i] != -1; i++)
2256        {
2257            item = [[NSMenuItem alloc] initWithTitle: [NSString localizedStringWithFormat: @"%.2f", ratioLimitActionValue[i]]
2258                    action: @selector(setQuickRatioGlobal:) keyEquivalent: @""];
2259            [item setTarget: self];
2260            [item setRepresentedObject: [NSNumber numberWithFloat: ratioLimitActionValue[i]]];
2261            [menu addItem: item];
2262            [item release];
2263        }
2264    }
2265    else;
2266}
2267
2268- (void) setGroup: (id) sender
2269{
2270    for (Torrent * torrent in [fTableView selectedTorrents])
2271    {
2272        [fTableView removeCollapsedGroup: [torrent groupValue]]; //remove old collapsed group
2273       
2274        [torrent setGroupValue: [sender tag]];
2275    }
2276   
2277    [self applyFilter: nil];
2278    [self updateUI];
2279    [self updateTorrentHistory];
2280}
2281
2282- (void) setGroupFilter: (id) sender
2283{
2284    [fDefaults setInteger: [sender tag] forKey: @"FilterGroup"];
2285    [self updateGroupsFilterButton];
2286    [self applyFilter: nil];
2287}
2288
2289- (void) updateGroupsFilterButton
2290{
2291    NSInteger groupIndex = [fDefaults integerForKey: @"FilterGroup"];
2292   
2293    NSImage * icon;
2294    NSString * toolTip;
2295    if (groupIndex == GROUP_FILTER_ALL_TAG)
2296    {
2297        icon = [NSImage imageNamed: @"PinTemplate.png"];
2298        toolTip = NSLocalizedString(@"All Groups", "Groups -> Button");
2299    }
2300    else
2301    {
2302        icon = [[GroupsController groups] imageForIndex: groupIndex];
2303        NSString * groupName = groupIndex != -1 ? [[GroupsController groups] nameForIndex: groupIndex]
2304                                                : NSLocalizedString(@"None", "Groups -> Button");
2305        toolTip = [NSLocalizedString(@"Group", "Groups -> Button") stringByAppendingFormat: @": %@", groupName];
2306    }
2307   
2308    [[fGroupFilterMenu itemAtIndex: 0] setImage: icon];
2309    [fGroupsButton setToolTip: toolTip];
2310}
2311
2312- (void) updateGroupsFilters: (NSNotification *) notification
2313{
2314    [self updateGroupsFilterButton];
2315    [self applyFilter: nil];
2316}
2317
2318- (void) toggleSpeedLimit: (id) sender
2319{
2320    [fDefaults setBool: ![fDefaults boolForKey: @"SpeedLimit"] forKey: @"SpeedLimit"];
2321    [self speedLimitChanged: sender];
2322}
2323
2324- (void) speedLimitChanged: (id) sender
2325{
2326    tr_sessionUseAltSpeed(fLib, [fDefaults boolForKey: @"SpeedLimit"]);
2327    [self updateSpeedFieldsToolTips];
2328}
2329
2330//dict has been retained
2331- (void) altSpeedToggledCallbackIsLimited: (NSDictionary *) dict
2332{
2333    const BOOL isLimited = [[dict objectForKey: @"Active"] boolValue];
2334
2335    [fDefaults setBool: isLimited forKey: @"SpeedLimit"];
2336    [self updateSpeedFieldsToolTips];
2337   
2338    if (![[dict objectForKey: @"ByUser"] boolValue])
2339        [GrowlApplicationBridge notifyWithTitle: isLimited
2340                ? NSLocalizedString(@"Speed Limit Auto Enabled", "Growl notification title")
2341                : NSLocalizedString(@"Speed Limit Auto Disabled", "Growl notification title")
2342            description: NSLocalizedString(@"Bandwidth settings changed", "Growl notification description")
2343            notificationName: GROWL_AUTO_SPEED_LIMIT iconData: nil priority: 0 isSticky: NO clickContext: nil];
2344   
2345    [dict release];
2346}
2347
2348- (void) setLimitGlobalEnabled: (id) sender
2349{
2350    BOOL upload = [sender menu] == fUploadMenu;
2351    [fDefaults setBool: sender == (upload ? fUploadLimitItem : fDownloadLimitItem) forKey: upload ? @"CheckUpload" : @"CheckDownload"];
2352   
2353    [fPrefsController applySpeedSettings: nil];
2354}
2355
2356- (void) setQuickLimitGlobal: (id) sender
2357{
2358    BOOL upload = [sender menu] == fUploadMenu;
2359    [fDefaults setInteger: [[sender representedObject] intValue] forKey: upload ? @"UploadLimit" : @"DownloadLimit"];
2360    [fDefaults setBool: YES forKey: upload ? @"CheckUpload" : @"CheckDownload"];
2361   
2362    [fPrefsController updateLimitFields];
2363    [fPrefsController applySpeedSettings: nil];
2364}
2365
2366- (void) setRatioGlobalEnabled: (id) sender
2367{
2368    [fDefaults setBool: sender == fCheckRatioItem forKey: @"RatioCheck"];
2369   
2370    [fPrefsController applyRatioSetting: nil];
2371}
2372
2373- (void) setQuickRatioGlobal: (id) sender
2374{
2375    [fDefaults setBool: YES forKey: @"RatioCheck"];
2376    [fDefaults setFloat: [[sender representedObject] floatValue] forKey: @"RatioLimit"];
2377   
2378    [fPrefsController updateRatioStopField];
2379}
2380
2381- (void) sound: (NSSound *) sound didFinishPlaying: (BOOL) finishedPlaying
2382{
2383    fSoundPlaying = NO;
2384}
2385
2386- (void) watcher: (id<UKFileWatcher>) watcher receivedNotification: (NSString *) notification forPath: (NSString *) path
2387{
2388    if ([notification isEqualToString: UKFileWatcherWriteNotification])
2389    {
2390        if (![fDefaults boolForKey: @"AutoImport"] || ![fDefaults stringForKey: @"AutoImportDirectory"])
2391            return;
2392       
2393        if (fAutoImportTimer)
2394        {
2395            if ([fAutoImportTimer isValid])
2396                [fAutoImportTimer invalidate];
2397            [fAutoImportTimer release];
2398            fAutoImportTimer = nil;
2399        }
2400       
2401        //check again in 10 seconds in case torrent file wasn't complete
2402        fAutoImportTimer = [[NSTimer scheduledTimerWithTimeInterval: 10.0 target: self
2403            selector: @selector(checkAutoImportDirectory) userInfo: nil repeats: NO] retain];
2404       
2405        [self checkAutoImportDirectory];
2406    }
2407}
2408
2409- (void) changeAutoImport
2410{
2411    if (fAutoImportTimer)
2412    {
2413        if ([fAutoImportTimer isValid])
2414            [fAutoImportTimer invalidate];
2415        [fAutoImportTimer release];
2416        fAutoImportTimer = nil;
2417    }
2418   
2419    if (fAutoImportedNames)
2420    {
2421        [fAutoImportedNames release];
2422        fAutoImportedNames = nil;
2423    }
2424   
2425    [self checkAutoImportDirectory];
2426}
2427
2428- (void) checkAutoImportDirectory
2429{
2430    NSString * path;
2431    if (![fDefaults boolForKey: @"AutoImport"] || !(path = [fDefaults stringForKey: @"AutoImportDirectory"]))
2432        return;
2433   
2434    path = [path stringByExpandingTildeInPath];
2435   
2436    NSArray * importedNames;
2437    if (!(importedNames = [[NSFileManager defaultManager] directoryContentsAtPath: path]))
2438        return;
2439   
2440    //only check files that have not been checked yet
2441    NSMutableArray * newNames = [importedNames mutableCopy];
2442   
2443    if (fAutoImportedNames)
2444        [newNames removeObjectsInArray: fAutoImportedNames];
2445    else
2446        fAutoImportedNames = [[NSMutableArray alloc] init];
2447    [fAutoImportedNames setArray: importedNames];
2448   
2449    for (NSInteger i = [newNames count] - 1; i >= 0; i--)
2450    {
2451        NSString * file = [newNames objectAtIndex: i];
2452        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
2453            [newNames removeObjectAtIndex: i];
2454        else
2455            [newNames replaceObjectAtIndex: i withObject: [path stringByAppendingPathComponent: file]];
2456    }
2457   
2458    for (NSString * file in newNames)
2459    {
2460        tr_ctor * ctor = tr_ctorNew(fLib);
2461        tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
2462       
2463        switch (tr_torrentParse(ctor, NULL))
2464        {
2465            case TR_OK:
2466                [self openFiles: [NSArray arrayWithObject: file] addType: ADD_AUTO forcePath: nil];
2467               
2468                [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Torrent File Auto Added", "Growl notification title")
2469                    description: [file lastPathComponent] notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO
2470                    clickContext: nil];
2471                break;
2472           
2473            case TR_EINVALID:
2474                [fAutoImportedNames removeObject: [file lastPathComponent]];
2475        }
2476       
2477        tr_ctorFree(ctor);
2478    }
2479   
2480    [newNames release];
2481}
2482
2483- (void) beginCreateFile: (NSNotification *) notification
2484{
2485    if (![fDefaults boolForKey: @"AutoImport"])
2486        return;
2487   
2488    NSString * location = [notification object],
2489            * path = [fDefaults stringForKey: @"AutoImportDirectory"];
2490   
2491    if (location && path && [[[location stringByDeletingLastPathComponent] stringByExpandingTildeInPath]
2492                                    isEqualToString: [path stringByExpandingTildeInPath]])
2493        [fAutoImportedNames addObject: [location lastPathComponent]];
2494}
2495
2496- (NSInteger) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item
2497{
2498    if (item)
2499        return [[item torrents] count];
2500    else
2501        return [fDisplayedTorrents count];
2502}
2503
2504- (id) outlineView: (NSOutlineView *) outlineView child: (NSInteger) index ofItem: (id) item
2505{
2506    if (item)
2507        return [[item torrents] objectAtIndex: index];
2508    else
2509        return [fDisplayedTorrents objectAtIndex: index];
2510}
2511
2512- (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item
2513{
2514    return ![item isKindOfClass: [Torrent class]];
2515}
2516
2517- (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item
2518{
2519    if ([item isKindOfClass: [Torrent class]])
2520        return [item hashString];
2521    else
2522    {
2523        NSString * ident = [tableColumn identifier];
2524        if ([ident isEqualToString: @"Group"])
2525        {
2526            NSInteger group = [item groupIndex];
2527            return group != -1 ? [[GroupsController groups] nameForIndex: group]
2528                                : NSLocalizedString(@"No Group", "Group table row");
2529        }
2530        else if ([ident isEqualToString: @"Color"])
2531        {
2532            NSInteger group = [item groupIndex];
2533            return [[GroupsController groups] imageForIndex: group];
2534        }
2535        else if ([ident isEqualToString: @"DL Image"])
2536            return [NSImage imageNamed: @"DownArrowGroupTemplate.png"];
2537        else if ([ident isEqualToString: @"UL Image"])
2538            return [NSImage imageNamed: [fDefaults boolForKey: @"DisplayGroupRowRatio"]
2539                                        ? @"YingYangGroupTemplate.png" : @"UpArrowGroupTemplate.png"];
2540        else
2541        {
2542            TorrentGroup * group = (TorrentGroup *)item;
2543           
2544            if ([fDefaults boolForKey: @"DisplayGroupRowRatio"])
2545                return [NSString stringForRatio: [group ratio]];
2546            else
2547            {
2548                CGFloat rate = [ident isEqualToString: @"UL"] ? [group uploadRate] : [group downloadRate];
2549                return [NSString stringForSpeed: rate];
2550            }
2551        }
2552    }
2553}
2554
2555- (BOOL) outlineView: (NSOutlineView *) outlineView writeItems: (NSArray *) items toPasteboard: (NSPasteboard *) pasteboard
2556{
2557    //only allow reordering of rows if sorting by order
2558    if ([fDefaults boolForKey: @"SortByGroup"] || [[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER])
2559    {
2560        NSMutableIndexSet * indexSet = [NSMutableIndexSet indexSet];
2561        for (id torrent in items)
2562        {
2563            if (![torrent isKindOfClass: [Torrent class]])
2564                return NO;
2565           
2566            [indexSet addIndex: [fTableView rowForItem: torrent]];
2567        }
2568       
2569        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
2570        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexSet] forType: TORRENT_TABLE_VIEW_DATA_TYPE];
2571        return YES;
2572    }
2573    return NO;
2574}
2575
2576- (NSDragOperation) outlineView: (NSOutlineView *) outlineView validateDrop: (id < NSDraggingInfo >) info proposedItem: (id) item
2577    proposedChildIndex: (NSInteger) index
2578{
2579    NSPasteboard * pasteboard = [info draggingPasteboard];
2580    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
2581    {
2582        if ([fDefaults boolForKey: @"SortByGroup"])
2583        {
2584            if (!item)
2585                return NSDragOperationNone;
2586           
2587            if ([[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER])
2588            {
2589                if ([item isKindOfClass: [Torrent class]])
2590                {
2591                    TorrentGroup * group = [fTableView parentForItem: item];
2592                    index = [[group torrents] indexOfObject: item] + 1;
2593                    item = group;
2594                }
2595            }
2596            else
2597            {
2598                if ([item isKindOfClass: [Torrent class]])
2599                    item = [fTableView parentForItem: item];
2600                index = NSOutlineViewDropOnItemIndex;
2601            }
2602        }
2603        else
2604        {
2605            if (item)
2606            {
2607                index = [fTableView rowForItem: item] + 1;
2608                item = nil;
2609            }
2610        }
2611       
2612        [fTableView setDropItem: item dropChildIndex: index];
2613        return NSDragOperationGeneric;
2614    }
2615   
2616    return NSDragOperationNone;
2617}
2618
2619- (BOOL) outlineView: (NSOutlineView *) outlineView acceptDrop: (id < NSDraggingInfo >) info item: (id) item
2620    childIndex: (NSInteger) newRow
2621{
2622    NSPasteboard * pasteboard = [info draggingPasteboard];
2623    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
2624    {
2625        //remember selected rows
2626        NSArray * selectedValues = [fTableView selectedValues];
2627   
2628        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData: [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
2629       
2630        //get the torrents to move
2631        NSMutableArray * movingTorrents = [NSMutableArray arrayWithCapacity: [indexes count]];
2632        for (NSUInteger i = [indexes firstIndex]; i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
2633            [movingTorrents addObject: [fTableView itemAtRow: i]];
2634       
2635        //reset groups
2636        if (item)
2637        {
2638            //change groups
2639            NSInteger groupValue = [item groupIndex];
2640            for (Torrent * torrent in movingTorrents)
2641            {
2642                //have to reset objects here to avoid weird crash
2643                [[[fTableView parentForItem: torrent] torrents] removeObject: torrent];
2644                [[item torrents] addObject: torrent];
2645               
2646                [torrent setGroupValue: groupValue];
2647            }
2648            //part 2 of avoiding weird crash
2649            [fTableView reloadItem: nil reloadChildren: YES];
2650        }
2651       
2652        //reorder queue order
2653        if (newRow != NSOutlineViewDropOnItemIndex)
2654        {
2655            //find torrent to place under
2656            NSArray * groupTorrents = item ? [item torrents] : fDisplayedTorrents;
2657            Torrent * topTorrent = nil;
2658            for (NSInteger i = newRow-1; i >= 0; i--)
2659            {
2660                Torrent * tempTorrent = [groupTorrents objectAtIndex: i];
2661                if (![movingTorrents containsObject: tempTorrent])
2662                {
2663                    topTorrent = tempTorrent;
2664                    break;
2665                }
2666            }
2667           
2668            //remove objects to reinsert
2669            [fTorrents removeObjectsInArray: movingTorrents];
2670           
2671            //insert objects at new location
2672            NSUInteger insertIndex = topTorrent ? [fTorrents indexOfObject: topTorrent] + 1 : 0;
2673            NSIndexSet * insertIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(insertIndex, [movingTorrents count])];
2674            [fTorrents insertObjects: movingTorrents atIndexes: insertIndexes];
2675        }
2676       
2677        [self applyFilter: nil];
2678        [fTableView selectValues: selectedValues];
2679    }
2680   
2681    return YES;
2682}
2683
2684- (void) torrentTableViewSelectionDidChange: (NSNotification *) notification
2685{
2686    [self resetInfo];
2687}
2688
2689- (NSDragOperation) draggingEntered: (id <NSDraggingInfo>) info
2690{
2691    NSPasteboard * pasteboard = [info draggingPasteboard];
2692    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
2693    {
2694        //check if any torrent files can be added
2695        BOOL torrent = NO;
2696        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
2697        for (NSString * file in files)
2698        {
2699            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
2700            {
2701                tr_ctor * ctor = tr_ctorNew(fLib);
2702                tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
2703                switch (tr_torrentParse(ctor, NULL))
2704                {
2705                    case TR_OK:
2706                        if (!fOverlayWindow)
2707                            fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2708                        [fOverlayWindow setTorrents: files];
2709                       
2710                        return NSDragOperationCopy;
2711                   
2712                    case TR_EDUPLICATE:
2713                        torrent = YES;
2714                }
2715                tr_ctorFree(ctor);
2716            }
2717        }
2718       
2719        //create a torrent file if a single file
2720        if (!torrent && [files count] == 1)
2721        {
2722            if (!fOverlayWindow)
2723                fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2724            [fOverlayWindow setFile: [[files objectAtIndex: 0] lastPathComponent]];
2725           
2726            return NSDragOperationCopy;
2727        }
2728    }
2729    else if ([[pasteboard types] containsObject: NSURLPboardType])
2730    {
2731        if (!fOverlayWindow)
2732            fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2733        [fOverlayWindow setURL: [[NSURL URLFromPasteboard: pasteboard] relativeString]];
2734       
2735        return NSDragOperationCopy;
2736    }
2737    else;
2738   
2739    return NSDragOperationNone;
2740}
2741
2742- (void) draggingExited: (id <NSDraggingInfo>) info
2743{
2744    if (fOverlayWindow)
2745        [fOverlayWindow fadeOut];
2746}
2747
2748- (BOOL) performDragOperation: (id <NSDraggingInfo>) info
2749{
2750    if (fOverlayWindow)
2751        [fOverlayWindow fadeOut];
2752   
2753    NSPasteboard * pasteboard = [info draggingPasteboard];
2754    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
2755    {
2756        BOOL torrent = NO, accept = YES;
2757       
2758        //create an array of files that can be opened
2759        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
2760        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
2761        for (NSString * file in files)
2762        {
2763            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
2764            {
2765                tr_ctor * ctor = tr_ctorNew(fLib);
2766                tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
2767                switch (tr_torrentParse(ctor, NULL))
2768                {
2769                    case TR_OK:
2770                        [filesToOpen addObject: file];
2771                        torrent = YES;
2772                        break;
2773                       
2774                    case TR_EDUPLICATE:
2775                        torrent = YES;
2776                }
2777                tr_ctorFree(ctor);
2778            }
2779        }
2780       
2781        if ([filesToOpen count] > 0)
2782            [self application: NSApp openFiles: filesToOpen];
2783        else
2784        {
2785            if (!torrent && [files count] == 1)
2786                [CreatorWindowController createTorrentFile: fLib forFile: [files objectAtIndex: 0]];
2787            else
2788                accept = NO;
2789        }
2790        [filesToOpen release];
2791       
2792        return accept;
2793    }
2794    else if ([[pasteboard types] containsObject: NSURLPboardType])
2795    {
2796        NSURL * url;
2797        if ((url = [NSURL URLFromPasteboard: pasteboard]))
2798        {
2799            [self openURL: url];
2800            return YES;
2801        }
2802    }
2803    else;
2804   
2805    return NO;
2806}
2807
2808- (void) toggleSmallView: (id) sender
2809{
2810    BOOL makeSmall = ![fDefaults boolForKey: @"SmallView"];
2811    [fDefaults setBool: makeSmall forKey: @"SmallView"];
2812   
2813    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
2814   
2815    [fTableView noteHeightOfRowsWithIndexesChanged: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTableView numberOfRows])]];
2816   
2817    //window min height
2818    NSSize contentMinSize = [fWindow contentMinSize],
2819            contentSize = [[fWindow contentView] frame].size;
2820    contentMinSize.height = contentSize.height - [[fTableView enclosingScrollView] frame].size.height
2821                            + [fTableView rowHeight] + [fTableView intercellSpacing].height;
2822    [fWindow setContentMinSize: contentMinSize];
2823   
2824    //resize for larger min height if not set to auto size
2825    if (![fDefaults boolForKey: @"AutoSize"])
2826    {
2827        if (!makeSmall && contentSize.height < contentMinSize.height)
2828        {
2829            NSRect frame = [fWindow frame];
2830            CGFloat heightChange = contentMinSize.height - contentSize.height;
2831            frame.size.height += heightChange;
2832            frame.origin.y -= heightChange;
2833           
2834            [fWindow setFrame: frame display: YES];
2835        }
2836    }
2837    else
2838        [self setWindowSizeToFit];
2839}
2840
2841- (void) togglePiecesBar: (id) sender
2842{
2843    [fDefaults setBool: ![fDefaults boolForKey: @"PiecesBar"] forKey: @"PiecesBar"];
2844    [fTableView togglePiecesBar];
2845}
2846
2847- (void) toggleAvailabilityBar: (id) sender
2848{
2849    [fDefaults setBool: ![fDefaults boolForKey: @"DisplayProgressBarAvailable"] forKey: @"DisplayProgressBarAvailable"];
2850    [fTableView display];
2851}
2852
2853- (void) toggleStatusString: (id) sender
2854{
2855    if ([fDefaults boolForKey: @"SmallView"])
2856        [fDefaults setBool: ![fDefaults boolForKey: @"DisplaySmallStatusRegular"] forKey: @"DisplaySmallStatusRegular"];
2857    else
2858        [fDefaults setBool: ![fDefaults boolForKey: @"DisplayStatusProgressSelected"] forKey: @"DisplayStatusProgressSelected"];
2859   
2860    [fTableView reloadData];
2861}
2862
2863- (NSRect) windowFrameByAddingHeight: (CGFloat) height checkLimits: (BOOL) check
2864{
2865    NSScrollView * scrollView = [fTableView enclosingScrollView];
2866   
2867    //convert pixels to points
2868    NSRect windowFrame = [fWindow frame];
2869    NSSize windowSize = [scrollView convertSize: windowFrame.size fromView: nil];
2870    windowSize.height += height;
2871   
2872    if (check)
2873    {
2874        NSSize minSize = [scrollView convertSize: [fWindow minSize] fromView: nil];
2875       
2876        if (windowSize.height < minSize.height)
2877            windowSize.height = minSize.height;
2878        else
2879        {
2880            NSSize maxSize = [scrollView convertSize: [[fWindow screen] visibleFrame].size fromView: nil];
2881            if ([fStatusBar isHidden])
2882                maxSize.height -= [fStatusBar frame].size.height;
2883            if ([fFilterBar isHidden])
2884                maxSize.height -= [fFilterBar frame].size.height;
2885            if (windowSize.height > maxSize.height)
2886                windowSize.height = maxSize.height;
2887        }
2888    }
2889
2890    //convert points to pixels
2891    windowSize = [scrollView convertSize: windowSize toView: nil];
2892
2893    windowFrame.origin.y -= (windowSize.height - windowFrame.size.height);
2894    windowFrame.size.height = windowSize.height;
2895    return windowFrame;
2896}
2897
2898- (void) toggleStatusBar: (id) sender
2899{
2900    [self showStatusBar: [fStatusBar isHidden] animate: YES];
2901    [fDefaults setBool: ![fStatusBar isHidden] forKey: @"StatusBar"];
2902}
2903
2904//doesn't save shown state
2905- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
2906{
2907    if (show != [fStatusBar isHidden])
2908        return;
2909
2910    if (show)
2911        [fStatusBar setHidden: NO];
2912
2913    NSRect frame;
2914    CGFloat heightChange = [fStatusBar frame].size.height;
2915    if (!show)
2916        heightChange *= -1;
2917   
2918    //allow bar to show even if not enough room
2919    if (show && ![fDefaults boolForKey: @"AutoSize"])
2920    {
2921        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2922        CGFloat change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2923        if (change < 0.0)
2924        {
2925            frame = [fWindow frame];
2926            frame.size.height += change;
2927            frame.origin.y -= change;
2928            [fWindow setFrame: frame display: NO animate: NO];
2929        }
2930    }
2931
2932    [self updateUI];
2933   
2934    NSScrollView * scrollView = [fTableView enclosingScrollView];
2935   
2936    //set views to not autoresize
2937    NSUInteger statsMask = [fStatusBar autoresizingMask];
2938    NSUInteger filterMask = [fFilterBar autoresizingMask];
2939    NSUInteger scrollMask = [scrollView autoresizingMask];
2940    [fStatusBar setAutoresizingMask: NSViewNotSizable];
2941    [fFilterBar setAutoresizingMask: NSViewNotSizable];
2942    [scrollView setAutoresizingMask: NSViewNotSizable];
2943   
2944    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2945    [fWindow setFrame: frame display: YES animate: animate];
2946   
2947    //re-enable autoresize
2948    [fStatusBar setAutoresizingMask: statsMask];
2949    [fFilterBar setAutoresizingMask: filterMask];
2950    [scrollView setAutoresizingMask: scrollMask];
2951   
2952    //change min size
2953    NSSize minSize = [fWindow contentMinSize];
2954    minSize.height += heightChange;
2955    [fWindow setContentMinSize: minSize];
2956   
2957    if (!show)
2958        [fStatusBar setHidden: YES];
2959}
2960
2961- (void) toggleFilterBar: (id) sender
2962{
2963    //disable filtering when hiding
2964    if (![fFilterBar isHidden])
2965    {
2966        [fSearchFilterField setStringValue: @""];
2967        [self setFilter: fNoFilterButton];
2968        [self setGroupFilter: [fGroupFilterMenu itemWithTag: GROUP_FILTER_ALL_TAG]];
2969    }
2970
2971    [self showFilterBar: [fFilterBar isHidden] animate: YES];
2972    [fDefaults setBool: ![fFilterBar isHidden] forKey: @"FilterBar"];
2973}
2974
2975//doesn't save shown state
2976- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
2977{
2978    if (show != [fFilterBar isHidden])
2979        return;
2980
2981    if (show)
2982        [fFilterBar setHidden: NO];
2983
2984    NSRect frame;
2985    CGFloat heightChange = [fFilterBar frame].size.height;
2986    if (!show)
2987        heightChange *= -1;
2988   
2989    //allow bar to show even if not enough room
2990    if (show && ![fDefaults boolForKey: @"AutoSize"])
2991    {
2992        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2993        CGFloat change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2994        if (change < 0.0)
2995        {
2996            frame = [fWindow frame];
2997            frame.size.height += change;
2998            frame.origin.y -= change;
2999            [fWindow setFrame: frame display: NO animate: NO];
3000        }
3001    }
3002   
3003    NSScrollView * scrollView = [fTableView enclosingScrollView];
3004
3005    //set views to not autoresize
3006    NSUInteger filterMask = [fFilterBar autoresizingMask];
3007    NSUInteger scrollMask = [scrollView autoresizingMask];
3008    [fFilterBar setAutoresizingMask: NSViewNotSizable];
3009    [scrollView setAutoresizingMask: NSViewNotSizable];
3010   
3011    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
3012    [fWindow setFrame: frame display: YES animate: animate];
3013   
3014    //re-enable autoresize
3015    [fFilterBar setAutoresizingMask: filterMask];
3016    [scrollView setAutoresizingMask: scrollMask];
3017   
3018    //change min size
3019    NSSize minSize = [fWindow contentMinSize];
3020    minSize.height += heightChange;
3021    [fWindow setContentMinSize: minSize];
3022   
3023    if (!show)
3024    {
3025        [fFilterBar setHidden: YES];
3026        [fWindow makeFirstResponder: fTableView];
3027    }
3028}
3029
3030- (void) focusFilterField
3031{
3032    [fWindow makeFirstResponder: fSearchFilterField];
3033    if ([fFilterBar isHidden])
3034        [self toggleFilterBar: self];
3035}
3036
3037- (ButtonToolbarItem *) standardToolbarButtonWithIdentifier: (NSString *) ident
3038{
3039    ButtonToolbarItem * item = [[ButtonToolbarItem alloc] initWithItemIdentifier: ident];
3040   
3041    NSButton * button = [[NSButton alloc] initWithFrame: NSZeroRect];
3042    [button setBezelStyle: NSTexturedRoundedBezelStyle];
3043    [button setStringValue: @""];
3044   
3045    [item setView: button];
3046    [button release];
3047   
3048    const NSSize buttonSize = NSMakeSize(36.0, 25.0);
3049    [item setMinSize: buttonSize];
3050    [item setMaxSize: buttonSize];
3051   
3052    return [item autorelease];
3053}
3054
3055- (NSToolbarItem *) toolbar: (NSToolbar *) toolbar itemForItemIdentifier: (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
3056{
3057    if ([ident isEqualToString: TOOLBAR_CREATE])
3058    {
3059        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3060       
3061        [item setLabel: NSLocalizedString(@"Create", "Create toolbar item -> label")];
3062        [item setPaletteLabel: NSLocalizedString(@"Create Torrent File", "Create toolbar item -> palette label")];
3063        [item setToolTip: NSLocalizedString(@"Create torrent file", "Create toolbar item -> tooltip")];
3064        [item setImage: [NSImage imageNamed: @"ToolbarCreateTemplate.png"]];
3065        [item setTarget: self];
3066        [item setAction: @selector(createFile:)];
3067        [item setAutovalidates: NO];
3068       
3069        return item;
3070    }
3071    else if ([ident isEqualToString: TOOLBAR_OPEN_FILE])
3072    {
3073        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3074       
3075        [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")];
3076        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")];
3077        [item setToolTip: NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip")];
3078        [item setImage: [NSImage imageNamed: @"ToolbarOpenTemplate.png"]];
3079        [item setTarget: self];
3080        [item setAction: @selector(openShowSheet:)];
3081        [item setAutovalidates: NO];
3082       
3083        return item;
3084    }
3085    else if ([ident isEqualToString: TOOLBAR_OPEN_WEB])
3086    {
3087        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3088       
3089        [item setLabel: NSLocalizedString(@"Open Address", "Open address toolbar item -> label")];
3090        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Address", "Open address toolbar item -> palette label")];
3091        [item setToolTip: NSLocalizedString(@"Open torrent web address", "Open address toolbar item -> tooltip")];
3092        [item setImage: [NSImage imageNamed: @"ToolbarOpenWebTemplate.png"]];
3093        [item setTarget: self];
3094        [item setAction: @selector(openURLShowSheet:)];
3095        [item setAutovalidates: NO];
3096       
3097        return item;
3098    }
3099    else if ([ident isEqualToString: TOOLBAR_REMOVE])
3100    {
3101        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3102       
3103        [item setLabel: NSLocalizedString(@"Remove", "Remove toolbar item -> label")];
3104        [item setPaletteLabel: NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label")];
3105        [item setToolTip: NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip")];
3106        [item setImage: [NSImage imageNamed: @"ToolbarRemoveTemplate.png"]];
3107        [item setTarget: self];
3108        [item setAction: @selector(removeNoDelete:)];
3109       
3110        return item;
3111    }
3112    else if ([ident isEqualToString: TOOLBAR_INFO])
3113    {
3114        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3115        [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled
3116       
3117        [item setLabel: NSLocalizedString(@"Inspector", "Inspector toolbar item -> label")];
3118        [item setPaletteLabel: NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label")];
3119        [item setToolTip: NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip")];
3120        [item setImage: [NSImage imageNamed: @"ToolbarInfoTemplate.png"]];
3121        [item setTarget: self];
3122        [item setAction: @selector(showInfo:)];
3123       
3124        return item;
3125    }
3126    else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_ALL])
3127    {
3128        GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident];
3129       
3130        NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect];
3131        [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]];
3132        [groupItem setView: segmentedControl];
3133        NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell];
3134       
3135        [segmentedControl setSegmentCount: 2];
3136        [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary];
3137       
3138        const NSSize groupSize = NSMakeSize(72.0, 25.0);
3139        [groupItem setMinSize: groupSize];
3140        [groupItem setMaxSize: groupSize];
3141       
3142        [groupItem setLabel: NSLocalizedString(@"Apply All", "All toolbar item -> label")];
3143        [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume All", "All toolbar item -> palette label")];
3144        [groupItem setTarget: self];
3145        [groupItem setAction: @selector(allToolbarClicked:)];
3146       
3147        [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, nil]];
3148       
3149        [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG];
3150        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarPauseAllTemplate.png"] forSegment: TOOLBAR_PAUSE_TAG];
3151        [segmentedCell setToolTip: NSLocalizedString(@"Pause all transfers",
3152                                    "All toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG];
3153       
3154        [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG];
3155        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarResumeAllTemplate.png"] forSegment: TOOLBAR_RESUME_TAG];
3156        [segmentedCell setToolTip: NSLocalizedString(@"Resume all transfers",
3157                                    "All toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG];
3158       
3159        [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause All", "All toolbar item -> label"),
3160                                        NSLocalizedString(@"Resume All", "All toolbar item -> label"), nil]];
3161       
3162        [segmentedControl release];
3163        return [groupItem autorelease];
3164    }
3165    else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_SELECTED])
3166    {
3167        GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident];
3168       
3169        NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect];
3170        [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]];
3171        [groupItem setView: segmentedControl];
3172        NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell];
3173       
3174        [segmentedControl setSegmentCount: 2];
3175        [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary];
3176       
3177        const NSSize groupSize = NSMakeSize(72.0, 25.0);
3178        [groupItem setMinSize: groupSize];
3179        [groupItem setMaxSize: groupSize];
3180       
3181        [groupItem setLabel: NSLocalizedString(@"Apply Selected", "Selected toolbar item -> label")];
3182        [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume Selected", "Selected toolbar item -> palette label")];
3183        [groupItem setTarget: self];
3184        [groupItem setAction: @selector(selectedToolbarClicked:)];
3185       
3186        [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED, nil]];
3187       
3188        [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG];
3189        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarPauseSelectedTemplate.png"] forSegment: TOOLBAR_PAUSE_TAG];
3190        [segmentedCell setToolTip: NSLocalizedString(@"Pause selected transfers",
3191                                    "Selected toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG];
3192       
3193        [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG];
3194        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarResumeSelectedTemplate.png"] forSegment: TOOLBAR_RESUME_TAG];
3195        [segmentedCell setToolTip: NSLocalizedString(@"Resume selected transfers",
3196                                    "Selected toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG];
3197       
3198        [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause Selected", "Selected toolbar item -> label"),
3199                                        NSLocalizedString(@"Resume Selected", "Selected toolbar item -> label"), nil]];
3200       
3201        [segmentedControl release];
3202        return [groupItem autorelease];
3203    }
3204    else if ([ident isEqualToString: TOOLBAR_FILTER])
3205    {
3206        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3207        [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled
3208       
3209        [item setLabel: NSLocalizedString(@"Filter", "Filter toolbar item -> label")];
3210        [item setPaletteLabel: NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label")];
3211        [item setToolTip: NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip")];
3212        [item setImage: [NSImage imageNamed: @"ToolbarFilterTemplate.png"]];
3213        [item setTarget: self];
3214        [item setAction: @selector(toggleFilterBar:)];
3215       
3216        return item;
3217    }
3218    else if ([ident isEqualToString: TOOLBAR_QUICKLOOK])
3219    {
3220        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3221       
3222        [item setLabel: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> label")];
3223        [item setPaletteLabel: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> palette label")];
3224        [item setToolTip: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> tooltip")];
3225        [item setImage: [NSImage imageNamed: NSImageNameQuickLookTemplate]];
3226        [item setTarget: self];
3227        [item setAction: @selector(toggleQuickLook:)];
3228       
3229        return item;
3230    }
3231    else
3232        return nil;
3233}
3234
3235- (void) allToolbarClicked: (id) sender
3236{
3237    NSInteger tagValue = [sender isKindOfClass: [NSSegmentedControl class]]
3238                    ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [sender tag];
3239    switch (tagValue)
3240    {
3241        case TOOLBAR_PAUSE_TAG:
3242            [self stopAllTorrents: sender];
3243            break;
3244        case TOOLBAR_RESUME_TAG:
3245            [self resumeAllTorrents: sender];
3246            break;
3247    }
3248}
3249
3250- (void) selectedToolbarClicked: (id) sender
3251{
3252    NSInteger tagValue = [sender isKindOfClass: [NSSegmentedControl class]]
3253                    ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [sender tag];
3254    switch (tagValue)
3255    {
3256        case TOOLBAR_PAUSE_TAG:
3257            [self stopSelectedTorrents: sender];
3258            break;
3259        case TOOLBAR_RESUME_TAG:
3260            [self resumeSelectedTorrents: sender];
3261            break;
3262    }
3263}
3264
3265- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
3266{
3267    return [NSArray arrayWithObjects:
3268            TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_OPEN_WEB, TOOLBAR_REMOVE,
3269            TOOLBAR_PAUSE_RESUME_SELECTED, TOOLBAR_PAUSE_RESUME_ALL,
3270            TOOLBAR_QUICKLOOK, TOOLBAR_FILTER, TOOLBAR_INFO,
3271            NSToolbarSeparatorItemIdentifier,
3272            NSToolbarSpaceItemIdentifier,
3273            NSToolbarFlexibleSpaceItemIdentifier,
3274            NSToolbarCustomizeToolbarItemIdentifier, nil];
3275}
3276
3277- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
3278{
3279    return [NSArray arrayWithObjects:
3280            TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_REMOVE, NSToolbarSeparatorItemIdentifier,
3281            TOOLBAR_PAUSE_RESUME_ALL, NSToolbarFlexibleSpaceItemIdentifier,
3282            TOOLBAR_QUICKLOOK, TOOLBAR_FILTER, TOOLBAR_INFO, nil];
3283}
3284
3285- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
3286{
3287    NSString * ident = [toolbarItem itemIdentifier];
3288   
3289    //enable remove item
3290    if ([ident isEqualToString: TOOLBAR_REMOVE])
3291        return [fTableView numberOfSelectedRows] > 0;
3292
3293    //enable pause all item
3294    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
3295    {
3296        for (Torrent * torrent in fTorrents)
3297            if ([torrent isActive] || [torrent waitingToStart])
3298                return YES;
3299        return NO;
3300    }
3301
3302    //enable resume all item
3303    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
3304    {
3305        for (Torrent * torrent in fTorrents)
3306            if (![torrent isActive] && ![torrent waitingToStart])
3307                return YES;
3308        return NO;
3309    }
3310
3311    //enable pause item
3312    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
3313    {
3314        for (Torrent * torrent in [fTableView selectedTorrents])
3315            if ([torrent isActive] || [torrent waitingToStart])
3316                return YES;
3317        return NO;
3318    }
3319   
3320    //enable resume item
3321    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
3322    {
3323        for (Torrent * torrent in [fTableView selectedTorrents])
3324            if (![torrent isActive] && ![torrent waitingToStart])
3325                return YES;
3326        return NO;
3327    }
3328   
3329    //set info image
3330    if ([ident isEqualToString: TOOLBAR_INFO])
3331    {
3332        [(NSButton *)[toolbarItem view] setState: [[fInfoController window] isVisible]];
3333        return YES;
3334    }
3335   
3336    //set filter image
3337    if ([ident isEqualToString: TOOLBAR_FILTER])
3338    {
3339        [(NSButton *)[toolbarItem view] setState: ![fFilterBar isHidden]];
3340        return YES;
3341    }
3342   
3343    //enable quicklook item
3344    if ([ident isEqualToString: TOOLBAR_QUICKLOOK])
3345        return [[QuickLookController quickLook] canQuickLook];
3346
3347    return YES;
3348}
3349
3350- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
3351{
3352    SEL action = [menuItem action];
3353   
3354    if (action == @selector(toggleSpeedLimit:))
3355    {
3356        [menuItem setState: [fDefaults boolForKey: @"SpeedLimit"] ? NSOnState : NSOffState];
3357        return YES;
3358    }
3359   
3360    //only enable some items if it is in a context menu or the window is useable
3361    BOOL canUseTable = [fWindow isKeyWindow] || [[menuItem menu] supermenu] != [NSApp mainMenu];
3362
3363    //enable open items
3364    if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:))
3365        return [fWindow attachedSheet] == nil;
3366   
3367    //enable sort options
3368    if (action == @selector(setSort:))
3369    {
3370        NSString * sortType;
3371        switch ([menuItem tag])
3372        {
3373            case SORT_ORDER_TAG:
3374                sortType = SORT_ORDER;
3375                break;
3376            case SORT_DATE_TAG:
3377                sortType = SORT_DATE;
3378                break;
3379            case SORT_NAME_TAG:
3380                sortType = SORT_NAME;
3381                break;
3382            case SORT_PROGRESS_TAG:
3383                sortType = SORT_PROGRESS;
3384                break;
3385            case SORT_STATE_TAG:
3386                sortType = SORT_STATE;
3387                break;
3388            case SORT_TRACKER_TAG:
3389                sortType = SORT_TRACKER;
3390                break;
3391            case SORT_ACTIVITY_TAG:
3392                sortType = SORT_ACTIVITY;
3393        }
3394       
3395        [menuItem setState: [sortType isEqualToString: [fDefaults stringForKey: @"Sort"]] ? NSOnState : NSOffState];
3396        return [fWindow isVisible];
3397    }
3398   
3399    //enable sort options
3400    if (action == @selector(setStatusLabel:))
3401    {
3402        NSString * statusLabel;
3403        switch ([menuItem tag])
3404        {
3405            case STATUS_RATIO_TOTAL_TAG:
3406                statusLabel = STATUS_RATIO_TOTAL;
3407                break;
3408            case STATUS_RATIO_SESSION_TAG:
3409                statusLabel = STATUS_RATIO_SESSION;
3410                break;
3411            case STATUS_TRANSFER_TOTAL_TAG:
3412                statusLabel = STATUS_TRANSFER_TOTAL;
3413                break;
3414            case STATUS_TRANSFER_SESSION_TAG:
3415                statusLabel = STATUS_TRANSFER_SESSION;
3416        }
3417       
3418        [menuItem setState: [statusLabel isEqualToString: [fDefaults stringForKey: @"StatusLabel"]] ? NSOnState : NSOffState];
3419        return YES;
3420    }
3421   
3422    if (action == @selector(setGroup:))
3423    {
3424        BOOL checked = NO;
3425       
3426        NSInteger index = [menuItem tag];
3427        for (Torrent * torrent in [fTableView selectedTorrents])
3428            if (index == [torrent groupValue])
3429            {
3430                checked = YES;
3431                break;
3432            }
3433       
3434        [menuItem setState: checked ? NSOnState : NSOffState];
3435        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3436    }
3437   
3438    if (action == @selector(setGroupFilter:))
3439    {
3440        [menuItem setState: [menuItem tag] == [fDefaults integerForKey: @"FilterGroup"] ? NSOnState : NSOffState];
3441        return YES;
3442    }
3443   
3444    if (action == @selector(toggleSmallView:))
3445    {
3446        [menuItem setState: [fDefaults boolForKey: @"SmallView"] ? NSOnState : NSOffState];
3447        return [fWindow isVisible];
3448    }
3449   
3450    if (action == @selector(togglePiecesBar:))
3451    {
3452        [menuItem setState: [fDefaults boolForKey: @"PiecesBar"] ? NSOnState : NSOffState];
3453        return [fWindow isVisible];
3454    }
3455   
3456    if (action == @selector(toggleStatusString:))
3457    {
3458        if ([fDefaults boolForKey: @"SmallView"])
3459        {
3460            [menuItem setTitle: NSLocalizedString(@"Remaining Time", "Action menu -> status string toggle")];
3461            [menuItem setState: ![fDefaults boolForKey: @"DisplaySmallStatusRegular"] ? NSOnState : NSOffState];
3462        }
3463        else
3464        {
3465            [menuItem setTitle: NSLocalizedString(@"Status of Selected Files", "Action menu -> status string toggle")];
3466            [menuItem setState: [fDefaults boolForKey: @"DisplayStatusProgressSelected"] ? NSOnState : NSOffState];
3467        }
3468       
3469        return [fWindow isVisible];
3470    }
3471   
3472    if (action == @selector(toggleAvailabilityBar:))
3473    {
3474        [menuItem setState: [fDefaults boolForKey: @"DisplayProgressBarAvailable"] ? NSOnState : NSOffState];
3475        return [fWindow isVisible];
3476    }
3477   
3478    if (action == @selector(setLimitGlobalEnabled:))
3479    {
3480        BOOL upload = [menuItem menu] == fUploadMenu;
3481        BOOL limit = menuItem == (upload ? fUploadLimitItem : fDownloadLimitItem);
3482        if (limit)
3483            [menuItem setTitle: [NSString stringWithFormat: NSLocalizedString(@"Limit (%d KB/s)",
3484                                    "Action menu -> upload/download limit"),
3485                                    [fDefaults integerForKey: upload ? @"UploadLimit" : @"DownloadLimit"]]];
3486       
3487        [menuItem setState: [fDefaults boolForKey: upload ? @"CheckUpload" : @"CheckDownload"] ? limit : !limit];
3488        return YES;
3489    }
3490   
3491    if (action == @selector(setRatioGlobalEnabled:))
3492    {
3493        BOOL check = menuItem == fCheckRatioItem;
3494        if (check)
3495            [menuItem setTitle: [NSString localizedStringWithFormat: NSLocalizedString(@"Stop at Ratio (%.2f)",
3496                                    "Action menu -> ratio stop"), [fDefaults floatForKey: @"RatioLimit"]]];
3497       
3498        [menuItem setState: [fDefaults boolForKey: @"RatioCheck"] ? check : !check];
3499        return YES;
3500    }
3501
3502    //enable show info
3503    if (action == @selector(showInfo:))
3504    {
3505        NSString * title = [[fInfoController window] isVisible] ? NSLocalizedString(@"Hide Inspector", "View menu -> Inspector")
3506                            : NSLocalizedString(@"Show Inspector", "View menu -> Inspector");
3507        [menuItem setTitle: title];
3508
3509        return YES;
3510    }
3511   
3512    //enable prev/next inspector tab
3513    if (action == @selector(setInfoTab:))
3514        return [[fInfoController window] isVisible];
3515   
3516    //enable toggle status bar
3517    if (action == @selector(toggleStatusBar:))
3518    {
3519        NSString * title = [fStatusBar isHidden] ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar")
3520                            : NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar");
3521        [menuItem setTitle: title];
3522
3523        return [fWindow isVisible];
3524    }
3525   
3526    //enable toggle filter bar
3527    if (action == @selector(toggleFilterBar:))
3528    {
3529        NSString * title = [fFilterBar isHidden] ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar")
3530                            : NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar");
3531        [menuItem setTitle: title];
3532
3533        return [fWindow isVisible];
3534    }
3535   
3536    //enable prev/next filter button
3537    if (action == @selector(switchFilter:))
3538        return [fWindow isVisible] && ![fFilterBar isHidden];
3539   
3540    //enable quicklook item
3541    if (action == @selector(toggleQuickLook:))
3542        return [[QuickLookController quickLook] canQuickLook];
3543   
3544    //enable reveal in finder
3545    if (action == @selector(revealFile:))
3546        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3547
3548    //enable remove items
3549    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
3550        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteDataAndTorrent:))
3551    {
3552        BOOL warning = NO,
3553            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
3554            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteDataAndTorrent:);
3555       
3556        for (Torrent * torrent in [fTableView selectedTorrents])
3557        {
3558            if (!warning && [torrent isActive])
3559            {
3560                warning = onlyDownloading ? ![torrent isSeeding] : YES;
3561                if (warning && canDelete)
3562                    break;
3563            }
3564            if (!canDelete && [torrent publicTorrent])
3565            {
3566                canDelete = YES;
3567                if (warning)
3568                    break;
3569            }
3570        }
3571   
3572        //append or remove ellipsis when needed
3573        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
3574        if (warning && [fDefaults boolForKey: @"CheckRemove"])
3575        {
3576            if (![title hasSuffix: ellipsis])
3577                [menuItem setTitle: [title stringByAppendingEllipsis]];
3578        }
3579        else
3580        {
3581            if ([title hasSuffix: ellipsis])
3582                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
3583        }
3584       
3585        return canUseTable && canDelete && [fTableView numberOfSelectedRows] > 0;
3586    }
3587
3588    //enable pause all item
3589    if (action == @selector(stopAllTorrents:))
3590    {
3591        for (Torrent * torrent in fTorrents)
3592            if ([torrent isActive] || [torrent waitingToStart])
3593                return YES;
3594        return NO;
3595    }
3596   
3597    //enable resume all item
3598    if (action == @selector(resumeAllTorrents:))
3599    {
3600        for (Torrent * torrent in fTorrents)
3601            if (![torrent isActive] && ![torrent waitingToStart])
3602                return YES;
3603        return NO;
3604    }
3605   
3606    //enable resume all waiting item
3607    if (action == @selector(resumeWaitingTorrents:))
3608    {
3609        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
3610            return NO;
3611   
3612        for (Torrent * torrent in fTorrents)
3613            if (![torrent isActive] && [torrent waitingToStart])
3614                return YES;
3615        return NO;
3616    }
3617   
3618    //enable resume selected waiting item
3619    if (action == @selector(resumeSelectedTorrentsNoWait:))
3620    {
3621        if (!canUseTable)
3622            return NO;
3623       
3624        for (Torrent * torrent in [fTableView selectedTorrents])
3625            if (![torrent isActive])
3626                return YES;
3627        return NO;
3628    }
3629
3630    //enable pause item
3631    if (action == @selector(stopSelectedTorrents:))
3632    {
3633        if (!canUseTable)
3634            return NO;
3635   
3636        for (Torrent * torrent in [fTableView selectedTorrents])
3637            if ([torrent isActive] || [torrent waitingToStart])
3638                return YES;
3639        return NO;
3640    }
3641   
3642    //enable resume item
3643    if (action == @selector(resumeSelectedTorrents:))
3644    {
3645        if (!canUseTable)
3646            return NO;
3647   
3648        for (Torrent * torrent in [fTableView selectedTorrents])
3649            if (![torrent isActive] && ![torrent waitingToStart])
3650                return YES;
3651        return NO;
3652    }
3653   
3654    //enable manual announce item
3655    if (action == @selector(announceSelectedTorrents:))
3656    {
3657        if (!canUseTable)
3658            return NO;
3659       
3660        for (Torrent * torrent in [fTableView selectedTorrents])
3661            if ([torrent canManualAnnounce])
3662                return YES;
3663        return NO;
3664    }
3665   
3666    //enable reset cache item
3667    if (action == @selector(verifySelectedTorrents:))
3668        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3669   
3670    //enable move torrent file item
3671    if (action == @selector(moveDataFilesSelected:))
3672        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3673   
3674    //enable copy torrent file item
3675    if (action == @selector(copyTorrentFiles:))
3676        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3677   
3678    //enable reverse sort item
3679    if (action == @selector(setSortReverse:))
3680    {
3681        [menuItem setState: [fDefaults boolForKey: @"SortReverse"] ? NSOnState : NSOffState];
3682        return ![[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER];
3683    }
3684   
3685    //enable group sort item
3686    if (action == @selector(setSortByGroup:))
3687    {
3688        [menuItem setState: [fDefaults boolForKey: @"SortByGroup"] ? NSOnState : NSOffState];
3689        return YES;
3690    }
3691   
3692    //check proper filter search item
3693    if (action == @selector(setFilterSearchType:))
3694    {
3695        NSString * filterType = [fDefaults stringForKey: @"FilterSearchType"];
3696       
3697        BOOL state;
3698        if ([menuItem tag] == FILTER_TYPE_TAG_TRACKER)
3699            state = [filterType isEqualToString: FILTER_TYPE_TRACKER];
3700        else
3701            state = [filterType isEqualToString: FILTER_TYPE_NAME];
3702       
3703        [menuItem setState: state ? NSOnState : NSOffState];
3704        return YES;
3705    }
3706   
3707    return YES;
3708}
3709
3710- (void) sleepCallback: (natural_t) messageType argument: (void *) messageArgument
3711{
3712    switch (messageType)
3713    {
3714        case kIOMessageSystemWillSleep:
3715            //if there are any running transfers, wait 15 seconds for them to stop
3716            for (Torrent * torrent in fTorrents)
3717                if ([torrent isActive])
3718                {
3719                    //stop all transfers (since some are active) before going to sleep and remember to resume when we wake up
3720                    for (Torrent * torrent in fTorrents)
3721                        [torrent sleep];
3722                    sleep(15);
3723                    break;
3724                }
3725
3726            IOAllowPowerChange(fRootPort, (long) messageArgument);
3727            break;
3728
3729        case kIOMessageCanSystemSleep:
3730            if ([fDefaults boolForKey: @"SleepPrevent"])
3731            {
3732                //prevent idle sleep unless no torrents are active
3733                for (Torrent * torrent in fTorrents)
3734                    if ([torrent isActive] && ![torrent isStalled] && ![torrent isError])
3735                    {
3736                        IOCancelPowerChange(fRootPort, (long) messageArgument);
3737                        return;
3738                    }
3739            }
3740           
3741            IOAllowPowerChange(fRootPort, (long) messageArgument);
3742            break;
3743
3744        case kIOMessageSystemHasPoweredOn:
3745            //resume sleeping transfers after we wake up
3746            for (Torrent * torrent in fTorrents)
3747                [torrent wakeUp];
3748            #warning check speed limit timer?
3749            //[self autoSpeedLimitChange: nil];
3750            break;
3751    }
3752}
3753
3754- (NSMenu *) applicationDockMenu: (NSApplication *) sender
3755{
3756    NSInteger seeding = 0, downloading = 0;
3757    for (Torrent * torrent in fTorrents)
3758    {
3759        if ([torrent isSeeding])
3760            seeding++;
3761        else if ([torrent isActive])
3762            downloading++;
3763        else;
3764    }
3765   
3766    NSMenuItem * seedingItem = [fDockMenu itemWithTag: DOCK_SEEDING_TAG],
3767            * downloadingItem = [fDockMenu itemWithTag: DOCK_DOWNLOADING_TAG];
3768   
3769    BOOL hasSeparator = seedingItem || downloadingItem;
3770   
3771    if (seeding > 0)
3772    {
3773        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Seeding", "Dock item - Seeding"), seeding];
3774        if (!seedingItem)
3775        {
3776            seedingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
3777            [seedingItem setTag: DOCK_SEEDING_TAG];
3778            [fDockMenu insertItem: seedingItem atIndex: 0];
3779        }
3780        else
3781            [seedingItem setTitle: title];
3782    }
3783    else
3784    {
3785        if (seedingItem)
3786            [fDockMenu removeItem: seedingItem];
3787    }
3788   
3789    if (downloading > 0)
3790    {
3791        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Downloading", "Dock item - Downloading"), downloading];
3792        if (!downloadingItem)
3793        {
3794            downloadingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
3795            [downloadingItem setTag: DOCK_DOWNLOADING_TAG];
3796            [fDockMenu insertItem: downloadingItem atIndex: seeding > 0 ? 1 : 0];
3797        }
3798        else
3799            [downloadingItem setTitle: title];
3800    }
3801    else
3802    {
3803        if (downloadingItem)
3804            [fDockMenu removeItem: downloadingItem];
3805    }
3806   
3807    if (seeding > 0 || downloading > 0)
3808    {
3809        if (!hasSeparator)
3810            [fDockMenu insertItem: [NSMenuItem separatorItem] atIndex: seeding > 0 && downloading > 0 ? 2 : 1];
3811    }
3812    else
3813    {
3814        if (hasSeparator)
3815            [fDockMenu removeItemAtIndex: 0];
3816    }
3817   
3818    return fDockMenu;
3819}
3820
3821- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
3822{
3823    //if auto size is enabled, the current frame shouldn't need to change
3824    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
3825   
3826    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
3827    return frame;
3828}
3829
3830- (void) setWindowSizeToFit
3831{
3832    if ([fDefaults boolForKey: @"AutoSize"])
3833    {
3834        NSScrollView * scrollView = [fTableView enclosingScrollView];
3835       
3836        [scrollView setHasVerticalScroller: NO];
3837        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
3838        [scrollView setHasVerticalScroller: YES];
3839       
3840        //hack to ensure scrollbars don't disappear after resizing
3841        [scrollView setAutohidesScrollers: NO];
3842        [scrollView setAutohidesScrollers: YES];
3843    }
3844}
3845
3846- (NSRect) sizedWindowFrame
3847{
3848    NSInteger groups = ([fDisplayedTorrents count] > 0 && ![[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [Torrent class]])
3849                    ? [fDisplayedTorrents count] : 0;
3850   
3851    CGFloat heightChange = (GROUP_SEPARATOR_HEIGHT + [fTableView intercellSpacing].height) * groups
3852                        + ([fTableView rowHeight] + [fTableView intercellSpacing].height) * ([fTableView numberOfRows] - groups)
3853                        - [[fTableView enclosingScrollView] frame].size.height;
3854   
3855    return [self windowFrameByAddingHeight: heightChange checkLimits: YES];
3856}
3857
3858- (void) updateForExpandCollape
3859{
3860    [self setWindowSizeToFit];
3861    [self setBottomCountText: YES];
3862}
3863
3864- (void) showMainWindow: (id) sender
3865{
3866    [fWindow makeKeyAndOrderFront: nil];
3867}
3868
3869- (void) windowDidBecomeMain: (NSNotification *) notification
3870{
3871    [fBadger clearCompleted];
3872    [self updateUI];
3873}
3874
3875- (NSSize) windowWillResize: (NSWindow *) sender toSize: (NSSize) proposedFrameSize
3876{
3877    //only resize horizontally if autosize is enabled
3878    if ([fDefaults boolForKey: @"AutoSize"])
3879        proposedFrameSize.height = [fWindow frame].size.height;
3880    return proposedFrameSize;
3881}
3882
3883- (void) windowDidResize: (NSNotification *) notification
3884{
3885    if (![fStatusBar isHidden])
3886        [self resizeStatusButton];
3887   
3888    if ([fFilterBar isHidden])
3889        return;
3890   
3891    //replace all buttons
3892    [fNoFilterButton sizeToFit];
3893    [fActiveFilterButton sizeToFit];
3894    [fDownloadFilterButton sizeToFit];
3895    [fSeedFilterButton sizeToFit];
3896    [fPauseFilterButton sizeToFit];
3897   
3898    NSRect allRect = [fNoFilterButton frame];
3899    NSRect activeRect = [fActiveFilterButton frame];
3900    NSRect downloadRect = [fDownloadFilterButton frame];
3901    NSRect seedRect = [fSeedFilterButton frame];
3902    NSRect pauseRect = [fPauseFilterButton frame];
3903   
3904    //size search filter to not overlap buttons
3905    NSRect searchFrame = [fSearchFilterField frame];
3906    searchFrame.origin.x = NSMaxX(pauseRect) + 5.0;
3907    searchFrame.size.width = [fStatusBar frame].size.width - searchFrame.origin.x - 5.0;
3908   
3909    //make sure it is not too long
3910    if (searchFrame.size.width > SEARCH_FILTER_MAX_WIDTH)
3911    {
3912        searchFrame.origin.x += searchFrame.size.width - SEARCH_FILTER_MAX_WIDTH;
3913        searchFrame.size.width = SEARCH_FILTER_MAX_WIDTH;
3914    }
3915    else if (searchFrame.size.width < SEARCH_FILTER_MIN_WIDTH)
3916    {
3917        searchFrame.origin.x += searchFrame.size.width - SEARCH_FILTER_MIN_WIDTH;
3918        searchFrame.size.width = SEARCH_FILTER_MIN_WIDTH;
3919       
3920        //calculate width the buttons can take up
3921        const CGFloat allowedWidth = (searchFrame.origin.x - 5.0) - allRect.origin.x;
3922        const CGFloat currentWidth = NSWidth(allRect) + NSWidth(activeRect) + NSWidth(downloadRect) + NSWidth(seedRect)
3923                                        + NSWidth(pauseRect) + 4.0; //add 4 for space between buttons
3924        const CGFloat ratio = allowedWidth / currentWidth;
3925       
3926        //decrease button widths proportionally
3927        allRect.size.width  = NSWidth(allRect) * ratio;
3928        activeRect.size.width = NSWidth(activeRect) * ratio;
3929        downloadRect.size.width = NSWidth(downloadRect) * ratio;
3930        seedRect.size.width = NSWidth(seedRect) * ratio;
3931        pauseRect.size.width = NSWidth(pauseRect) * ratio;
3932    }
3933    else;
3934   
3935    activeRect.origin.x = NSMaxX(allRect) + 1.0;
3936    downloadRect.origin.x = NSMaxX(activeRect) + 1.0;
3937    seedRect.origin.x = NSMaxX(downloadRect) + 1.0;
3938    pauseRect.origin.x = NSMaxX(seedRect) + 1.0;
3939   
3940    [fNoFilterButton setFrame: allRect];
3941    [fActiveFilterButton setFrame: activeRect];
3942    [fDownloadFilterButton setFrame: downloadRect];
3943    [fSeedFilterButton setFrame: seedRect];
3944    [fPauseFilterButton setFrame: pauseRect];
3945   
3946    [fSearchFilterField setFrame: searchFrame];
3947}
3948
3949- (void) applicationWillUnhide: (NSNotification *) notification
3950{
3951    [self updateUI];
3952}
3953
3954- (NSArray *) quickLookURLs
3955{
3956    NSArray * selectedTorrents = [fTableView selectedTorrents];
3957    NSMutableArray * urlArray = [NSMutableArray arrayWithCapacity: [selectedTorrents count]];
3958   
3959    for (Torrent * torrent in selectedTorrents)
3960        if ([self canQuickLookTorrent: torrent])
3961            [urlArray addObject: [NSURL fileURLWithPath: [torrent dataLocation]]];
3962   
3963    return urlArray;
3964}
3965
3966- (BOOL) canQuickLook
3967{
3968    for (Torrent * torrent in [fTableView selectedTorrents])
3969        if ([self canQuickLookTorrent: torrent])
3970            return YES;
3971   
3972    return NO;
3973}
3974
3975- (BOOL) canQuickLookTorrent: (Torrent *) torrent
3976{
3977    if (![[NSFileManager defaultManager] fileExistsAtPath: [torrent dataLocation]])
3978        return NO;
3979   
3980    return [torrent isFolder] || [torrent isComplete];
3981}
3982
3983- (NSRect) quickLookFrameWithURL: (NSURL *) url
3984{
3985    if ([fWindow isVisible])
3986    {
3987        NSString * fullPath = [url path];
3988        NSRange visibleRows = [fTableView rowsInRect: [fTableView bounds]];
3989       
3990        for (NSInteger row = 0; row < NSMaxRange(visibleRows); row++)
3991        {
3992            id item = [fTableView itemAtRow: row];
3993            if ([item isKindOfClass: [Torrent class]] && [[(Torrent *)item dataLocation] isEqualToString: fullPath])
3994            {
3995                NSRect frame = [fTableView iconRectForRow: row];
3996                frame.origin = [fTableView convertPoint: frame.origin toView: nil];
3997                frame.origin = [fWindow convertBaseToScreen: frame.origin];
3998                frame.origin.y -= frame.size.height;
3999                return frame;
4000            }
4001        }
4002    }
4003   
4004    return NSZeroRect;
4005}
4006
4007- (void) toggleQuickLook: (id) sender
4008{
4009    [[QuickLookController quickLook] toggleQuickLook];
4010}
4011
4012- (void) linkHomepage: (id) sender
4013{
4014    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
4015}
4016
4017- (void) linkForums: (id) sender
4018{
4019    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
4020}
4021
4022- (void) linkTrac: (id) sender
4023{
4024    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: TRAC_URL]];
4025}
4026
4027- (void) linkDonate: (id) sender
4028{
4029    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: DONATE_URL]];
4030}
4031
4032- (void) updaterWillRelaunchApplication: (SUUpdater *) updater
4033{
4034    fUpdateInProgress = YES;
4035}
4036
4037- (NSDictionary *) registrationDictionaryForGrowl
4038{
4039    NSArray * notifications = [NSArray arrayWithObjects: GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE,
4040                                                            GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT, nil];
4041    return [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL,
4042                                notifications, GROWL_NOTIFICATIONS_DEFAULT, nil];
4043}
4044
4045- (void) growlNotificationWasClicked: (id) clickContext
4046{
4047    if (!clickContext || ![clickContext isKindOfClass: [NSDictionary class]])
4048        return;
4049   
4050    NSString * type = [clickContext objectForKey: @"Type"], * location;
4051    if (([type isEqualToString: GROWL_DOWNLOAD_COMPLETE] || [type isEqualToString: GROWL_SEEDING_COMPLETE])
4052            && (location = [clickContext objectForKey: @"Location"]))
4053        [[NSWorkspace sharedWorkspace] selectFile: location inFileViewerRootedAtPath: nil];
4054}
4055
4056- (void) rpcCallback: (tr_rpc_callback_type) type forTorrentStruct: (struct tr_torrent *) torrentStruct
4057{
4058    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
4059   
4060    //get the torrent
4061    Torrent * torrent = nil;
4062    if (torrentStruct != NULL && (type != TR_RPC_TORRENT_ADDED && type != TR_RPC_SESSION_CHANGED))
4063    {
4064        for (torrent in fTorrents)
4065            if (torrentStruct == [torrent torrentStruct])
4066            {
4067                [torrent retain];
4068                break;
4069            }
4070       
4071        if (!torrent)
4072        {
4073            [pool release];
4074           
4075            NSLog(@"No torrent found matching the given torrent struct from the RPC callback!");
4076            return;
4077        }
4078    }
4079   
4080    switch (type)
4081    {
4082        case TR_RPC_TORRENT_ADDED:
4083            [self performSelectorOnMainThread: @selector(rpcAddTorrentStruct:) withObject:
4084                [[NSValue valueWithPointer: torrentStruct] retain] waitUntilDone: NO];
4085            break;
4086       
4087        case TR_RPC_TORRENT_STARTED:
4088        case TR_RPC_TORRENT_STOPPED:
4089            [self performSelectorOnMainThread: @selector(rpcStartedStoppedTorrent:) withObject: torrent waitUntilDone: NO];
4090            break;
4091       
4092        case TR_RPC_TORRENT_REMOVING:
4093            [self performSelectorOnMainThread: @selector(rpcRemoveTorrent:) withObject: torrent waitUntilDone: NO];
4094            break;
4095       
4096        case TR_RPC_TORRENT_CHANGED:
4097            [self performSelectorOnMainThread: @selector(rpcChangedTorrent:) withObject: torrent waitUntilDone: NO];
4098            break;
4099       
4100        case TR_RPC_SESSION_CHANGED:
4101            [fPrefsController performSelectorOnMainThread: @selector(rpcUpdatePrefs) withObject: nil waitUntilDone: NO];
4102            break;
4103       
4104        default:
4105            NSLog(@"Unknown RPC command received!");
4106            [torrent release];
4107    }
4108   
4109    [pool release];
4110}
4111
4112- (void) rpcAddTorrentStruct: (NSValue *) torrentStructPtr
4113{
4114    tr_torrent * torrentStruct = (tr_torrent *)[torrentStructPtr pointerValue];
4115    [torrentStructPtr release];
4116   
4117    NSString * location = nil;
4118    if (tr_torrentGetDownloadDir(torrentStruct) != NULL)
4119        location = [NSString stringWithUTF8String: tr_torrentGetDownloadDir(torrentStruct)];
4120   
4121    Torrent * torrent = [[Torrent alloc] initWithTorrentStruct: torrentStruct location: location lib: fLib];
4122   
4123    [torrent update];
4124    [fTorrents addObject: torrent];
4125    [torrent release];
4126   
4127    [self updateTorrentsInQueue];
4128}
4129
4130- (void) rpcRemoveTorrent: (Torrent *) torrent
4131{
4132    [self confirmRemoveTorrents: [[NSArray arrayWithObject: torrent] retain] deleteData: NO deleteTorrent: NO];
4133    [torrent release];
4134}
4135
4136- (void) rpcStartedStoppedTorrent: (Torrent *) torrent
4137{
4138    [torrent update];
4139    [torrent release];
4140   
4141    [self updateUI];
4142    [self applyFilter: nil];
4143    [self updateTorrentHistory];
4144}
4145
4146- (void) rpcChangedTorrent: (Torrent *) torrent
4147{
4148    [torrent update];
4149   
4150    if ([[fTableView selectedTorrents] containsObject: torrent])
4151    {
4152        [fInfoController updateInfoStats]; //this will reload the file table
4153        [fInfoController updateOptions];
4154    }
4155   
4156    [torrent release];
4157}
4158
4159@end
Note: See TracBrowser for help on using the repository browser.