source: trunk/macosx/Controller.m @ 8119

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

only show a Growl notification when the speed limit is auto-enable/disabled

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