source: trunk/macosx/Controller.m @ 13178

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

use NSSearchPathForDirectoriesInDomains to retrieve the Application Support directory for config files, instead of libtransmission's hardcoded path

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