source: trunk/macosx/Controller.m @ 8118

Last change on this file since 8118 was 8118, checked in by charles, 13 years ago

(trunk) add a did-the-user-do-this flag to the "alt speed toggled" callback

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