source: trunk/macosx/Controller.m @ 14042

Last change on this file since 14042 was 14042, checked in by livings124, 9 years ago

combine the two rpc delete data methods

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