source: trunk/macosx/Controller.m @ 13184

Last change on this file since 13184 was 13184, checked in by livings124, 10 years ago

Sort the Sort menu items on launch, since the main menu is now localized through a strings file.

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