source: trunk/macosx/Controller.m @ 13987

Last change on this file since 13987 was 13987, checked in by livings124, 8 years ago

fix autoreleasing the file watcher queue

  • Property svn:keywords set to Date Rev Author Id
File size: 188.5 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 13987 2013-02-08 13:18:41Z 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    #warning remove log
2960    NSLog(@"%@",notification);
2961    NSParameterAssert([notification isEqualToString:VDKQueueWriteNotification]);
2962   
2963    if (![fDefaults boolForKey: @"AutoImport"] || ![fDefaults stringForKey: @"AutoImportDirectory"])
2964        return;
2965   
2966    if ([fAutoImportTimer isValid])
2967        [fAutoImportTimer invalidate];
2968    [fAutoImportTimer release];
2969   
2970    //check again in 10 seconds in case torrent file wasn't complete
2971    fAutoImportTimer = [[NSTimer scheduledTimerWithTimeInterval: 10.0 target: self
2972        selector: @selector(checkAutoImportDirectory) userInfo: nil repeats: NO] retain];
2973   
2974    [self checkAutoImportDirectory];
2975}
2976
2977- (void) changeAutoImport
2978{
2979    if ([fAutoImportTimer isValid])
2980        [fAutoImportTimer invalidate];
2981    [fAutoImportTimer release];
2982    fAutoImportTimer = nil;
2983   
2984    [fAutoImportedNames release];
2985    fAutoImportedNames = nil;
2986   
2987    [self checkAutoImportDirectory];
2988}
2989
2990- (void) checkAutoImportDirectory
2991{
2992    NSString * path;
2993    if (![fDefaults boolForKey: @"AutoImport"] || !(path = [fDefaults stringForKey: @"AutoImportDirectory"]))
2994        return;
2995   
2996    path = [path stringByExpandingTildeInPath];
2997   
2998    NSArray * importedNames;
2999    if (!(importedNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath: path error: NULL]))
3000        return;
3001   
3002    //only check files that have not been checked yet
3003    NSMutableArray * newNames = [importedNames mutableCopy];
3004   
3005    if (fAutoImportedNames)
3006        [newNames removeObjectsInArray: fAutoImportedNames];
3007    else
3008        fAutoImportedNames = [[NSMutableArray alloc] init];
3009    [fAutoImportedNames setArray: importedNames];
3010   
3011    for (NSString * file in newNames)
3012    {
3013        if ([file hasPrefix: @"."])
3014            continue;
3015       
3016        NSString * fullFile = [path stringByAppendingPathComponent: file];
3017       
3018        if (!([[[NSWorkspace sharedWorkspace] typeOfFile: fullFile error: NULL] isEqualToString: @"org.bittorrent.torrent"]
3019                || [[fullFile pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame))
3020            continue;
3021       
3022        tr_ctor * ctor = tr_ctorNew(fLib);
3023        tr_ctorSetMetainfoFromFile(ctor, [fullFile UTF8String]);
3024       
3025        switch (tr_torrentParse(ctor, NULL))
3026        {
3027            case TR_PARSE_OK:
3028                [self openFiles: [NSArray arrayWithObject: fullFile] addType: ADD_AUTO forcePath: nil];
3029               
3030                NSString * notificationTitle = NSLocalizedString(@"Torrent File Auto Added", "notification title");
3031                if ([NSApp isOnMountainLionOrBetter])
3032                {
3033                    NSUserNotification* notification = [[NSUserNotificationMtLion alloc] init];
3034                    [notification setTitle: notificationTitle];
3035                    [notification setInformativeText: file];
3036                   
3037                    [notification setHasActionButton: NO];
3038                   
3039                    [[NSUserNotificationCenterMtLion defaultUserNotificationCenter] deliverNotification: notification];
3040                    [notification release];
3041                }
3042               
3043                [GrowlApplicationBridge notifyWithTitle: notificationTitle
3044                    description: file notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO
3045                    clickContext: nil];
3046                break;
3047           
3048            case TR_PARSE_ERR:
3049                [fAutoImportedNames removeObject: file];
3050                break;
3051           
3052            case TR_PARSE_DUPLICATE: //let's ignore this (but silence a warning)
3053                break;
3054        }
3055       
3056        tr_ctorFree(ctor);
3057    }
3058   
3059    [newNames release];
3060}
3061
3062- (void) beginCreateFile: (NSNotification *) notification
3063{
3064    if (![fDefaults boolForKey: @"AutoImport"])
3065        return;
3066   
3067    NSString * location = [(NSURL *)[notification object] path],
3068            * path = [fDefaults stringForKey: @"AutoImportDirectory"];
3069   
3070    if (location && path && [[[location stringByDeletingLastPathComponent] stringByExpandingTildeInPath]
3071                                    isEqualToString: [path stringByExpandingTildeInPath]])
3072        [fAutoImportedNames addObject: [location lastPathComponent]];
3073}
3074
3075- (NSInteger) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item
3076{
3077    if (item)
3078        return [[item torrents] count];
3079    else
3080        return [fDisplayedTorrents count];
3081}
3082
3083- (id) outlineView: (NSOutlineView *) outlineView child: (NSInteger) index ofItem: (id) item
3084{
3085    if (item)
3086        return [[item torrents] objectAtIndex: index];
3087    else
3088        return [fDisplayedTorrents objectAtIndex: index];
3089}
3090
3091- (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item
3092{
3093    return ![item isKindOfClass: [Torrent class]];
3094}
3095
3096- (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item
3097{
3098    if ([item isKindOfClass: [Torrent class]]) {
3099        if (tableColumn)
3100            return nil;
3101        return [item hashString];
3102    }
3103    else
3104    {
3105        NSString * ident = [tableColumn identifier];
3106        if ([ident isEqualToString: @"Group"])
3107        {
3108            NSInteger group = [item groupIndex];
3109            return group != -1 ? [[GroupsController groups] nameForIndex: group]
3110                                : NSLocalizedString(@"No Group", "Group table row");
3111        }
3112        else if ([ident isEqualToString: @"Color"])
3113        {
3114            NSInteger group = [item groupIndex];
3115            return [[GroupsController groups] imageForIndex: group];
3116        }
3117        else if ([ident isEqualToString: @"DL Image"])
3118            return [NSImage imageNamed: @"DownArrowGroupTemplate"];
3119        else if ([ident isEqualToString: @"UL Image"])
3120            return [NSImage imageNamed: [fDefaults boolForKey: @"DisplayGroupRowRatio"]
3121                                        ? @"YingYangGroupTemplate" : @"UpArrowGroupTemplate"];
3122        else
3123        {
3124            TorrentGroup * group = (TorrentGroup *)item;
3125           
3126            if ([fDefaults boolForKey: @"DisplayGroupRowRatio"])
3127                return [NSString stringForRatio: [group ratio]];
3128            else
3129            {
3130                CGFloat rate = [ident isEqualToString: @"UL"] ? [group uploadRate] : [group downloadRate];
3131                return [NSString stringForSpeed: rate];
3132            }
3133        }
3134    }
3135}
3136
3137- (BOOL) outlineView: (NSOutlineView *) outlineView writeItems: (NSArray *) items toPasteboard: (NSPasteboard *) pasteboard
3138{
3139    //only allow reordering of rows if sorting by order
3140    if ([fDefaults boolForKey: @"SortByGroup"] || [[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER])
3141    {
3142        NSMutableIndexSet * indexSet = [NSMutableIndexSet indexSet];
3143        for (id torrent in items)
3144        {
3145            if (![torrent isKindOfClass: [Torrent class]])
3146                return NO;
3147           
3148            [indexSet addIndex: [fTableView rowForItem: torrent]];
3149        }
3150       
3151        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
3152        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexSet] forType: TORRENT_TABLE_VIEW_DATA_TYPE];
3153        return YES;
3154    }
3155    return NO;
3156}
3157
3158- (NSDragOperation) outlineView: (NSOutlineView *) outlineView validateDrop: (id < NSDraggingInfo >) info proposedItem: (id) item
3159    proposedChildIndex: (NSInteger) index
3160{
3161    NSPasteboard * pasteboard = [info draggingPasteboard];
3162    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
3163    {
3164        if ([fDefaults boolForKey: @"SortByGroup"])
3165        {
3166            if (!item)
3167                return NSDragOperationNone;
3168           
3169            if ([[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER])
3170            {
3171                if ([item isKindOfClass: [Torrent class]])
3172                {
3173                    TorrentGroup * group = [fTableView parentForItem: item];
3174                    index = [[group torrents] indexOfObject: item] + 1;
3175                    item = group;
3176                }
3177            }
3178            else
3179            {
3180                if ([item isKindOfClass: [Torrent class]])
3181                    item = [fTableView parentForItem: item];
3182                index = NSOutlineViewDropOnItemIndex;
3183            }
3184        }
3185        else
3186        {
3187            if (index == NSOutlineViewDropOnItemIndex)
3188                return NSDragOperationNone;
3189           
3190            if (item)
3191            {
3192                index = [fTableView rowForItem: item] + 1;
3193                item = nil;
3194            }
3195        }
3196       
3197        [fTableView setDropItem: item dropChildIndex: index];
3198        return NSDragOperationGeneric;
3199    }
3200   
3201    return NSDragOperationNone;
3202}
3203
3204- (BOOL) outlineView: (NSOutlineView *) outlineView acceptDrop: (id < NSDraggingInfo >) info item: (id) item childIndex: (NSInteger) newRow
3205{
3206    NSPasteboard * pasteboard = [info draggingPasteboard];
3207    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
3208    {
3209        //remember selected rows
3210        NSArray * selectedValues = nil;
3211        if (![NSApp isOnLionOrBetter])
3212            selectedValues = [fTableView selectedValues];
3213   
3214        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData: [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
3215       
3216        //get the torrents to move
3217        NSMutableArray * movingTorrents = [NSMutableArray arrayWithCapacity: [indexes count]];
3218        for (NSUInteger i = [indexes firstIndex]; i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
3219        {
3220            Torrent * torrent = [fTableView itemAtRow: i];
3221            [movingTorrents addObject: torrent];
3222           
3223            //change groups
3224            if (item)
3225                [torrent setGroupValue: [item groupIndex] determinationType: TorrentDeterminationUserSpecified];
3226        }
3227       
3228        //reorder queue order
3229        if (newRow != NSOutlineViewDropOnItemIndex)
3230        {
3231            //find torrent to place under
3232            NSArray * groupTorrents = item ? [item torrents] : fDisplayedTorrents;
3233            Torrent * topTorrent = nil;
3234            for (NSInteger i = newRow-1; i >= 0; i--)
3235            {
3236                Torrent * tempTorrent = [groupTorrents objectAtIndex: i];
3237                if (![movingTorrents containsObject: tempTorrent])
3238                {
3239                    topTorrent = tempTorrent;
3240                    break;
3241                }
3242            }
3243           
3244            //remove objects to reinsert
3245            [fTorrents removeObjectsInArray: movingTorrents];
3246           
3247            //insert objects at new location
3248            const NSUInteger insertIndex = topTorrent ? [fTorrents indexOfObject: topTorrent] + 1 : 0;
3249            NSIndexSet * insertIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(insertIndex, [movingTorrents count])];
3250            [fTorrents insertObjects: movingTorrents atIndexes: insertIndexes];
3251           
3252            //we need to make sure the queue order is updated in the Torrent object before we sort - safest to just reset all queue positions
3253            NSUInteger i = 0;
3254            for (Torrent * torrent in fTorrents)
3255            {
3256                [torrent setQueuePosition: i++];
3257                [torrent update];
3258            }
3259           
3260            //do the drag animation here so that the dragged torrents are the ones that are animated as moving, and not the torrents around them
3261            const BOOL onLion = [NSApp isOnLionOrBetter];
3262            if (onLion)
3263                [fTableView beginUpdates];
3264           
3265            NSUInteger insertDisplayIndex = topTorrent ? [groupTorrents indexOfObject: topTorrent] + 1 : 0;
3266           
3267            for (Torrent * torrent in movingTorrents)
3268            {
3269                TorrentGroup * oldParent = item ? [fTableView parentForItem: torrent] : nil;
3270                NSMutableArray * oldTorrents = oldParent ? [oldParent torrents] : fDisplayedTorrents;
3271                const NSUInteger oldIndex = [oldTorrents indexOfObject: torrent];
3272               
3273                if (item == oldParent)
3274                {
3275                    if (oldIndex < insertDisplayIndex)
3276                        --insertDisplayIndex;
3277                    [oldTorrents moveObjectAtIndex: oldIndex toIndex: insertDisplayIndex];
3278                }
3279                else
3280                {
3281                    NSAssert(item && oldParent, @"Expected to be dragging between group rows");
3282                   
3283                    NSMutableArray * newTorrents = [(TorrentGroup *)item torrents];
3284                    [newTorrents insertObject: torrent atIndex: insertDisplayIndex];
3285                    [oldTorrents removeObjectAtIndex: oldIndex];
3286                }
3287               
3288                if (onLion)
3289                    [fTableView moveItemAtIndex: oldIndex inParent: oldParent toIndex: insertDisplayIndex inParent: item];
3290               
3291                ++insertDisplayIndex;
3292            }
3293           
3294            if (onLion)
3295                [fTableView endUpdates];
3296        }
3297       
3298        [self applyFilter];
3299        if (selectedValues)
3300            [fTableView selectValues: selectedValues];
3301    }
3302   
3303    return YES;
3304}
3305
3306- (void) torrentTableViewSelectionDidChange: (NSNotification *) notification
3307{
3308    [self resetInfo];
3309    [[fWindow toolbar] validateVisibleItems];
3310}
3311
3312- (NSDragOperation) draggingEntered: (id <NSDraggingInfo>) info
3313{
3314    NSPasteboard * pasteboard = [info draggingPasteboard];
3315    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
3316    {
3317        //check if any torrent files can be added
3318        BOOL torrent = NO;
3319        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
3320        for (NSString * file in files)
3321        {
3322            if ([[[NSWorkspace sharedWorkspace] typeOfFile: file error: NULL] isEqualToString: @"org.bittorrent.torrent"]
3323                    || [[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
3324            {
3325                torrent = YES;
3326                tr_ctor * ctor = tr_ctorNew(fLib);
3327                tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
3328                if (tr_torrentParse(ctor, NULL) == TR_PARSE_OK)
3329                {
3330                    if (!fOverlayWindow)
3331                        fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
3332                    [fOverlayWindow setTorrents: files];
3333                   
3334                    return NSDragOperationCopy;
3335                }
3336                tr_ctorFree(ctor);
3337            }
3338        }
3339       
3340        //create a torrent file if a single file
3341        if (!torrent && [files count] == 1)
3342        {
3343            if (!fOverlayWindow)
3344                fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
3345            [fOverlayWindow setFile: [[files objectAtIndex: 0] lastPathComponent]];
3346           
3347            return NSDragOperationCopy;
3348        }
3349    }
3350    else if ([[pasteboard types] containsObject: NSURLPboardType])
3351    {
3352        if (!fOverlayWindow)
3353            fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
3354        [fOverlayWindow setURL: [[NSURL URLFromPasteboard: pasteboard] relativeString]];
3355       
3356        return NSDragOperationCopy;
3357    }
3358    else;
3359   
3360    return NSDragOperationNone;
3361}
3362
3363- (void) draggingExited: (id <NSDraggingInfo>) info
3364{
3365    if (fOverlayWindow)
3366        [fOverlayWindow fadeOut];
3367}
3368
3369- (BOOL) performDragOperation: (id <NSDraggingInfo>) info
3370{
3371    if (fOverlayWindow)
3372        [fOverlayWindow fadeOut];
3373   
3374    NSPasteboard * pasteboard = [info draggingPasteboard];
3375    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
3376    {
3377        BOOL torrent = NO, accept = YES;
3378       
3379        //create an array of files that can be opened
3380        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
3381        NSMutableArray * filesToOpen = [NSMutableArray arrayWithCapacity: [files count]];
3382        for (NSString * file in files)
3383        {
3384            if ([[[NSWorkspace sharedWorkspace] typeOfFile: file error: NULL] isEqualToString: @"org.bittorrent.torrent"]
3385                    || [[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
3386            {
3387                torrent = YES;
3388                tr_ctor * ctor = tr_ctorNew(fLib);
3389                tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
3390                if (tr_torrentParse(ctor, NULL) == TR_PARSE_OK)
3391                    [filesToOpen addObject: file];
3392                tr_ctorFree(ctor);
3393            }
3394        }
3395       
3396        if ([filesToOpen count] > 0)
3397            [self application: NSApp openFiles: filesToOpen];
3398        else
3399        {
3400            if (!torrent && [files count] == 1)
3401                [CreatorWindowController createTorrentFile: fLib forFile: [NSURL fileURLWithPath: [files objectAtIndex: 0]]];
3402            else
3403                accept = NO;
3404        }
3405       
3406        return accept;
3407    }
3408    else if ([[pasteboard types] containsObject: NSURLPboardType])
3409    {
3410        NSURL * url;
3411        if ((url = [NSURL URLFromPasteboard: pasteboard]))
3412        {
3413            [self openURL: [url absoluteString]];
3414            return YES;
3415        }
3416    }
3417    else;
3418   
3419    return NO;
3420}
3421
3422- (void) toggleSmallView: (id) sender
3423{
3424    BOOL makeSmall = ![fDefaults boolForKey: @"SmallView"];
3425    [fDefaults setBool: makeSmall forKey: @"SmallView"];
3426   
3427    [fTableView setUsesAlternatingRowBackgroundColors: !makeSmall];
3428   
3429    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
3430   
3431    if ([NSApp isOnLionOrBetter])
3432        [fTableView beginUpdates];
3433    [fTableView noteHeightOfRowsWithIndexesChanged: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTableView numberOfRows])]];
3434    if ([NSApp isOnLionOrBetter])
3435        [fTableView endUpdates];
3436   
3437    //resize for larger min height if not set to auto size
3438    if (![fDefaults boolForKey: @"AutoSize"])
3439    {
3440        const NSSize contentSize = [[fWindow contentView] frame].size;
3441       
3442        NSSize contentMinSize = [fWindow contentMinSize];
3443        contentMinSize.height = [self minWindowContentSizeAllowed];
3444        [fWindow setContentMinSize: contentMinSize];
3445       
3446        //make sure the window already isn't too small
3447        if (!makeSmall && contentSize.height < contentMinSize.height)
3448        {
3449            NSRect frame = [fWindow frame];
3450            CGFloat heightChange = contentMinSize.height - contentSize.height;
3451            frame.size.height += heightChange;
3452            frame.origin.y -= heightChange;
3453           
3454            [fWindow setFrame: frame display: YES];
3455        }
3456    }
3457    else
3458        [self setWindowSizeToFit];
3459}
3460
3461- (void) togglePiecesBar: (id) sender
3462{
3463    [fDefaults setBool: ![fDefaults boolForKey: @"PiecesBar"] forKey: @"PiecesBar"];
3464    [fTableView togglePiecesBar];
3465}
3466
3467- (void) toggleAvailabilityBar: (id) sender
3468{
3469    [fDefaults setBool: ![fDefaults boolForKey: @"DisplayProgressBarAvailable"] forKey: @"DisplayProgressBarAvailable"];
3470    [fTableView display];
3471}
3472
3473#warning elliminate when 10.7-only
3474- (void) toggleStatusString: (id) sender
3475{
3476    if ([fDefaults boolForKey: @"SmallView"])
3477        [fDefaults setBool: ![fDefaults boolForKey: @"DisplaySmallStatusRegular"] forKey: @"DisplaySmallStatusRegular"];
3478    else
3479        [fDefaults setBool: ![fDefaults boolForKey: @"DisplayStatusProgressSelected"] forKey: @"DisplayStatusProgressSelected"];
3480   
3481    [fTableView setNeedsDisplay: YES];
3482}
3483
3484- (NSRect) windowFrameByAddingHeight: (CGFloat) height checkLimits: (BOOL) check
3485{
3486    NSScrollView * scrollView = [fTableView enclosingScrollView];
3487   
3488    //convert pixels to points
3489    NSRect windowFrame = [fWindow frame];
3490    NSSize windowSize = [scrollView convertSize: windowFrame.size fromView: nil];
3491    windowSize.height += height;
3492   
3493    if (check)
3494    {
3495        //we can't call minSize, since it might be set to the current size (auto size)
3496        const CGFloat minHeight = [self minWindowContentSizeAllowed]
3497                                    + (NSHeight([fWindow frame]) - NSHeight([[fWindow contentView] frame])); //contentView to window
3498       
3499        if (windowSize.height <= minHeight)
3500            windowSize.height = minHeight;
3501        else
3502        {
3503            NSScreen * screen = [fWindow screen];
3504            if (screen)
3505            {
3506                NSSize maxSize = [scrollView convertSize: [screen visibleFrame].size fromView: nil];
3507                if (!fStatusBar)
3508                    maxSize.height -= STATUS_BAR_HEIGHT;
3509                if (!fFilterBar)
3510                    maxSize.height -= FILTER_BAR_HEIGHT;
3511                if (windowSize.height > maxSize.height)
3512                    windowSize.height = maxSize.height;
3513            }
3514        }
3515    }
3516
3517    //convert points to pixels
3518    windowSize = [scrollView convertSize: windowSize toView: nil];
3519
3520    windowFrame.origin.y -= (windowSize.height - windowFrame.size.height);
3521    windowFrame.size.height = windowSize.height;
3522    return windowFrame;
3523}
3524
3525- (void) toggleStatusBar: (id) sender
3526{
3527    const BOOL show = fStatusBar == nil;
3528    [self showStatusBar: show animate: YES];
3529    [fDefaults setBool: show forKey: @"StatusBar"];
3530}
3531
3532//doesn't save shown state
3533- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
3534{
3535    const BOOL prevShown = fStatusBar != nil;
3536    if (show == prevShown)
3537        return;
3538   
3539    if (show)
3540    {
3541        fStatusBar = [[StatusBarController alloc] initWithLib: fLib];
3542       
3543        NSView * contentView = [fWindow contentView];
3544        const NSSize windowSize = [contentView convertSize: [fWindow frame].size fromView: nil];
3545       
3546        NSRect statusBarFrame = [[fStatusBar view] frame];
3547        statusBarFrame.size.width = windowSize.width;
3548        [[fStatusBar view] setFrame: statusBarFrame];
3549       
3550        [contentView addSubview: [fStatusBar view]];
3551        [[fStatusBar view] setFrameOrigin: NSMakePoint(0.0, NSMaxY([contentView frame]))];
3552    }
3553   
3554    CGFloat heightChange = [[fStatusBar view] frame].size.height;
3555    if (!show)
3556        heightChange *= -1;
3557   
3558    //allow bar to show even if not enough room
3559    if (show && ![fDefaults boolForKey: @"AutoSize"])
3560    {
3561        NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
3562       
3563        NSScreen * screen = [fWindow screen];
3564        if (screen)
3565        {
3566            CGFloat change = [screen visibleFrame].size.height - frame.size.height;
3567            if (change < 0.0)
3568            {
3569                frame = [fWindow frame];
3570                frame.size.height += change;
3571                frame.origin.y -= change;
3572                [fWindow setFrame: frame display: NO animate: NO];
3573            }
3574        }
3575    }
3576   
3577    [self updateUI];
3578   
3579    NSScrollView * scrollView = [fTableView enclosingScrollView];
3580   
3581    //set views to not autoresize
3582    const NSUInteger statsMask = [[fStatusBar view] autoresizingMask];
3583    [[fStatusBar view] setAutoresizingMask: NSViewNotSizable];
3584    NSUInteger filterMask;
3585    if (fFilterBar)
3586    {
3587        filterMask = [[fFilterBar view] autoresizingMask];
3588        [[fFilterBar view] setAutoresizingMask: NSViewNotSizable];
3589    }
3590    const NSUInteger scrollMask = [scrollView autoresizingMask];
3591    [scrollView setAutoresizingMask: NSViewNotSizable];
3592   
3593    NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
3594    [fWindow setFrame: frame display: YES animate: animate];
3595   
3596    //re-enable autoresize
3597    [[fStatusBar view] setAutoresizingMask: statsMask];
3598    if (fFilterBar)
3599        [[fFilterBar view] setAutoresizingMask: filterMask];
3600    [scrollView setAutoresizingMask: scrollMask];
3601   
3602    if (!show)
3603    {
3604        [[fStatusBar view] removeFromSuperviewWithoutNeedingDisplay];
3605        [fStatusBar release];
3606        fStatusBar = nil;
3607    }
3608   
3609    if ([fDefaults boolForKey: @"AutoSize"])
3610        [self setWindowMinMaxToCurrent];
3611    else
3612    {
3613        //change min size
3614        NSSize minSize = [fWindow contentMinSize];
3615        minSize.height += heightChange;
3616        [fWindow setContentMinSize: minSize];
3617    }
3618}
3619
3620- (void) toggleFilterBar: (id) sender
3621{
3622    const BOOL show = fFilterBar == nil;
3623   
3624    [self showFilterBar: show animate: YES];
3625    [fDefaults setBool: show forKey: @"FilterBar"];
3626    [[fWindow toolbar] validateVisibleItems];
3627   
3628    //disable filtering when hiding
3629    if (!show)
3630        [fFilterBar reset: NO];
3631   
3632    [self applyFilter]; //do even if showing to ensure tooltips are updated
3633}
3634
3635//doesn't save shown state
3636- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
3637{
3638    const BOOL prevShown = fFilterBar != nil;
3639    if (show == prevShown)
3640        return;
3641   
3642    if (show)
3643    {
3644        fFilterBar = [[FilterBarController alloc] init];
3645       
3646        NSView * contentView = [fWindow contentView];
3647        const NSSize windowSize = [contentView convertSize: [fWindow frame].size fromView: nil];
3648       
3649        NSRect filterBarFrame = [[fFilterBar view] frame];
3650        filterBarFrame.size.width = windowSize.width;
3651        [[fFilterBar view] setFrame: filterBarFrame];
3652       
3653        if (fStatusBar)
3654            [contentView addSubview: [fFilterBar view] positioned: NSWindowBelow relativeTo: [fStatusBar view]];
3655        else
3656            [contentView addSubview: [fFilterBar view]];
3657        const CGFloat originY = fStatusBar ? NSMinY([[fStatusBar view] frame]) : NSMaxY([contentView frame]);
3658        [[fFilterBar view] setFrameOrigin: NSMakePoint(0.0, originY)];
3659    }
3660    else
3661        [fWindow makeFirstResponder: fTableView];
3662   
3663    CGFloat heightChange = NSHeight([[fFilterBar view] frame]);
3664    if (!show)
3665        heightChange *= -1;
3666   
3667    //allow bar to show even if not enough room
3668    if (show && ![fDefaults boolForKey: @"AutoSize"])
3669    {
3670        NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
3671       
3672        NSScreen * screen = [fWindow screen];
3673        if (screen)
3674        {
3675            CGFloat change = [screen visibleFrame].size.height - frame.size.height;
3676            if (change < 0.0)
3677            {
3678                frame = [fWindow frame];
3679                frame.size.height += change;
3680                frame.origin.y -= change;
3681                [fWindow setFrame: frame display: NO animate: NO];
3682            }
3683        }
3684    }
3685   
3686    NSScrollView * scrollView = [fTableView enclosingScrollView];
3687
3688    //set views to not autoresize
3689    const NSUInteger filterMask = [[fFilterBar view] autoresizingMask];
3690    const NSUInteger scrollMask = [scrollView autoresizingMask];
3691    [[fFilterBar view] setAutoresizingMask: NSViewNotSizable];
3692    [scrollView setAutoresizingMask: NSViewNotSizable];
3693   
3694    const NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
3695    [fWindow setFrame: frame display: YES animate: animate];
3696   
3697    //re-enable autoresize
3698    [[fFilterBar view] setAutoresizingMask: filterMask];
3699    [scrollView setAutoresizingMask: scrollMask];
3700   
3701    if (!show)
3702    {
3703        [[fFilterBar view] removeFromSuperviewWithoutNeedingDisplay];
3704        [fFilterBar release];
3705        fFilterBar = nil;
3706    }
3707   
3708    if ([fDefaults boolForKey: @"AutoSize"])
3709        [self setWindowMinMaxToCurrent];
3710    else
3711    {
3712        //change min size
3713        NSSize minSize = [fWindow contentMinSize];
3714        minSize.height += heightChange;
3715        [fWindow setContentMinSize: minSize];
3716    }
3717}
3718
3719- (void) focusFilterField
3720{
3721    if (!fFilterBar)
3722        [self toggleFilterBar: self];
3723    [fFilterBar focusSearchField];
3724}
3725
3726- (BOOL) acceptsPreviewPanelControl: (QLPreviewPanel *) panel
3727{
3728    return !fQuitting;
3729}
3730
3731- (void) beginPreviewPanelControl: (QLPreviewPanel *) panel
3732{
3733    fPreviewPanel = [panel retain];
3734    [fPreviewPanel setDelegate: self];
3735    [fPreviewPanel setDataSource: self];
3736}
3737
3738- (void) endPreviewPanelControl: (QLPreviewPanel *) panel
3739{
3740    [fPreviewPanel release];
3741    fPreviewPanel = nil;
3742}
3743
3744- (NSArray *) quickLookableTorrents
3745{
3746    NSArray * selectedTorrents = [fTableView selectedTorrents];
3747    NSMutableArray * qlArray = [NSMutableArray arrayWithCapacity: [selectedTorrents count]];
3748   
3749    for (Torrent * torrent in selectedTorrents)
3750        if (([torrent isFolder] || [torrent isComplete]) && [torrent dataLocation])
3751            [qlArray addObject: torrent];
3752   
3753    return qlArray;
3754}
3755
3756- (NSInteger) numberOfPreviewItemsInPreviewPanel: (QLPreviewPanel *) panel
3757{
3758    if ([fInfoController canQuickLook])
3759        return [[fInfoController quickLookURLs] count];
3760    else
3761        return [[self quickLookableTorrents] count];
3762}
3763
3764- (id <QLPreviewItem>) previewPanel: (QLPreviewPanel *) panel previewItemAtIndex: (NSInteger) index
3765{
3766    if ([fInfoController canQuickLook])
3767        return [[fInfoController quickLookURLs] objectAtIndex: index];
3768    else
3769        return [[self quickLookableTorrents] objectAtIndex: index];
3770}
3771
3772- (BOOL) previewPanel: (QLPreviewPanel *) panel handleEvent: (NSEvent *) event
3773{
3774    /*if ([event type] == NSKeyDown)
3775    {
3776        [super keyDown: event];
3777        return YES;
3778    }*/
3779   
3780    return NO;
3781}
3782
3783- (NSRect) previewPanel: (QLPreviewPanel *) panel sourceFrameOnScreenForPreviewItem: (id <QLPreviewItem>) item
3784{
3785    if ([fInfoController canQuickLook])
3786        return [fInfoController quickLookSourceFrameForPreviewItem: item];
3787    else
3788    {
3789        if (![fWindow isVisible])
3790            return NSZeroRect;
3791       
3792        const NSInteger row = [fTableView rowForItem: item];
3793        if (row == -1)
3794            return NSZeroRect;
3795       
3796        NSRect frame = [fTableView iconRectForRow: row];
3797       
3798        if (!NSIntersectsRect([fTableView visibleRect], frame))
3799            return NSZeroRect;
3800       
3801        frame.origin = [fTableView convertPoint: frame.origin toView: nil];
3802        frame.origin = [fWindow convertBaseToScreen: frame.origin];
3803        frame.origin.y -= frame.size.height;
3804        return frame;
3805    }
3806}
3807
3808- (ButtonToolbarItem *) standardToolbarButtonWithIdentifier: (NSString *) ident
3809{
3810    ButtonToolbarItem * item = [[ButtonToolbarItem alloc] initWithItemIdentifier: ident];
3811   
3812    NSButton * button = [[NSButton alloc] init];
3813    [button setBezelStyle: NSTexturedRoundedBezelStyle];
3814    [button setStringValue: @""];
3815   
3816    [item setView: button];
3817    [button release];
3818   
3819    const NSSize buttonSize = NSMakeSize(36.0, 25.0);
3820    [item setMinSize: buttonSize];
3821    [item setMaxSize: buttonSize];
3822   
3823    return [item autorelease];
3824}
3825
3826- (NSToolbarItem *) toolbar: (NSToolbar *) toolbar itemForItemIdentifier: (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
3827{
3828    if ([ident isEqualToString: TOOLBAR_CREATE])
3829    {
3830        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3831       
3832        [item setLabel: NSLocalizedString(@"Create", "Create toolbar item -> label")];
3833        [item setPaletteLabel: NSLocalizedString(@"Create Torrent File", "Create toolbar item -> palette label")];
3834        [item setToolTip: NSLocalizedString(@"Create torrent file", "Create toolbar item -> tooltip")];
3835        [item setImage: [NSImage imageNamed: @"ToolbarCreateTemplate"]];
3836        [item setTarget: self];
3837        [item setAction: @selector(createFile:)];
3838        [item setAutovalidates: NO];
3839       
3840        return item;
3841    }
3842    else if ([ident isEqualToString: TOOLBAR_OPEN_FILE])
3843    {
3844        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3845       
3846        [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")];
3847        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")];
3848        [item setToolTip: NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip")];
3849        [item setImage: [NSImage imageNamed: @"ToolbarOpenTemplate"]];
3850        [item setTarget: self];
3851        [item setAction: @selector(openShowSheet:)];
3852        [item setAutovalidates: NO];
3853       
3854        return item;
3855    }
3856    else if ([ident isEqualToString: TOOLBAR_OPEN_WEB])
3857    {
3858        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3859       
3860        [item setLabel: NSLocalizedString(@"Open Address", "Open address toolbar item -> label")];
3861        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Address", "Open address toolbar item -> palette label")];
3862        [item setToolTip: NSLocalizedString(@"Open torrent web address", "Open address toolbar item -> tooltip")];
3863        [item setImage: [NSImage imageNamed: @"ToolbarOpenWebTemplate"]];
3864        [item setTarget: self];
3865        [item setAction: @selector(openURLShowSheet:)];
3866        [item setAutovalidates: NO];
3867       
3868        return item;
3869    }
3870    else if ([ident isEqualToString: TOOLBAR_REMOVE])
3871    {
3872        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3873       
3874        [item setLabel: NSLocalizedString(@"Remove", "Remove toolbar item -> label")];
3875        [item setPaletteLabel: NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label")];
3876        [item setToolTip: NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip")];
3877        [item setImage: [NSImage imageNamed: @"ToolbarRemoveTemplate"]];
3878        [item setTarget: self];
3879        [item setAction: @selector(removeNoDelete:)];
3880        [item setVisibilityPriority: NSToolbarItemVisibilityPriorityHigh];
3881       
3882        return item;
3883    }
3884    else if ([ident isEqualToString: TOOLBAR_INFO])
3885    {
3886        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3887        [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled
3888       
3889        [item setLabel: NSLocalizedString(@"Inspector", "Inspector toolbar item -> label")];
3890        [item setPaletteLabel: NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label")];
3891        [item setToolTip: NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip")];
3892        [item setImage: [NSImage imageNamed: @"ToolbarInfoTemplate"]];
3893        [item setTarget: self];
3894        [item setAction: @selector(showInfo:)];
3895       
3896        return item;
3897    }
3898    else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_ALL])
3899    {
3900        GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident];
3901       
3902        NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect];
3903        [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]];
3904        [groupItem setView: segmentedControl];
3905        NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell];
3906       
3907        [segmentedControl setSegmentCount: 2];
3908        [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary];
3909       
3910        const NSSize groupSize = NSMakeSize(72.0, 25.0);
3911        [groupItem setMinSize: groupSize];
3912        [groupItem setMaxSize: groupSize];
3913       
3914        [groupItem setLabel: NSLocalizedString(@"Apply All", "All toolbar item -> label")];
3915        [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume All", "All toolbar item -> palette label")];
3916        [groupItem setTarget: self];
3917        [groupItem setAction: @selector(allToolbarClicked:)];
3918       
3919        [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, nil]];
3920       
3921        [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG];
3922        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarPauseAllTemplate"] forSegment: TOOLBAR_PAUSE_TAG];
3923        [segmentedCell setToolTip: NSLocalizedString(@"Pause all transfers",
3924                                    "All toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG];
3925       
3926        [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG];
3927        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarResumeAllTemplate"] forSegment: TOOLBAR_RESUME_TAG];
3928        [segmentedCell setToolTip: NSLocalizedString(@"Resume all transfers",
3929                                    "All toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG];
3930       
3931        [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause All", "All toolbar item -> label"),
3932                                        NSLocalizedString(@"Resume All", "All toolbar item -> label"), nil]];
3933       
3934        [segmentedControl release];
3935       
3936        [groupItem setVisibilityPriority: NSToolbarItemVisibilityPriorityHigh];
3937       
3938        return [groupItem autorelease];
3939    }
3940    else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_SELECTED])
3941    {
3942        GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident];
3943       
3944        NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect];
3945        [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]];
3946        [groupItem setView: segmentedControl];
3947        NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell];
3948       
3949        [segmentedControl setSegmentCount: 2];
3950        [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary];
3951       
3952        const NSSize groupSize = NSMakeSize(72.0, 25.0);
3953        [groupItem setMinSize: groupSize];
3954        [groupItem setMaxSize: groupSize];
3955       
3956        [groupItem setLabel: NSLocalizedString(@"Apply Selected", "Selected toolbar item -> label")];
3957        [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume Selected", "Selected toolbar item -> palette label")];
3958        [groupItem setTarget: self];
3959        [groupItem setAction: @selector(selectedToolbarClicked:)];
3960       
3961        [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED, nil]];
3962       
3963        [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG];
3964        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarPauseSelectedTemplate"] forSegment: TOOLBAR_PAUSE_TAG];
3965        [segmentedCell setToolTip: NSLocalizedString(@"Pause selected transfers",
3966                                    "Selected toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG];
3967       
3968        [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG];
3969        [segmentedControl setImage: [NSImage imageNamed: @"ToolbarResumeSelectedTemplate"] forSegment: TOOLBAR_RESUME_TAG];
3970        [segmentedCell setToolTip: NSLocalizedString(@"Resume selected transfers",
3971                                    "Selected toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG];
3972       
3973        [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause Selected", "Selected toolbar item -> label"),
3974                                        NSLocalizedString(@"Resume Selected", "Selected toolbar item -> label"), nil]];
3975       
3976        [segmentedControl release];
3977       
3978        [groupItem setVisibilityPriority: NSToolbarItemVisibilityPriorityHigh];
3979       
3980        return [groupItem autorelease];
3981    }
3982    else if ([ident isEqualToString: TOOLBAR_FILTER])
3983    {
3984        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3985        [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled
3986       
3987        [item setLabel: NSLocalizedString(@"Filter", "Filter toolbar item -> label")];
3988        [item setPaletteLabel: NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label")];
3989        [item setToolTip: NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip")];
3990        [item setImage: [NSImage imageNamed: @"ToolbarFilterTemplate"]];
3991        [item setTarget: self];
3992        [item setAction: @selector(toggleFilterBar:)];
3993       
3994        return item;
3995    }
3996    else if ([ident isEqualToString: TOOLBAR_QUICKLOOK])
3997    {
3998        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3999        [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled
4000       
4001        [item setLabel: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> label")];
4002        [item setPaletteLabel: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> palette label")];
4003        [item setToolTip: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> tooltip")];
4004        [item setImage: [NSImage imageNamed: NSImageNameQuickLookTemplate]];
4005        [item setTarget: self];
4006        [item setAction: @selector(toggleQuickLook:)];
4007        [item setVisibilityPriority: NSToolbarItemVisibilityPriorityLow];
4008       
4009        return item;
4010    }
4011    else
4012        return nil;
4013}
4014
4015- (void) allToolbarClicked: (id) sender
4016{
4017    NSInteger tagValue = [sender isKindOfClass: [NSSegmentedControl class]]
4018                    ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [(NSControl *)sender tag];
4019    switch (tagValue)
4020    {
4021        case TOOLBAR_PAUSE_TAG:
4022            [self stopAllTorrents: sender];
4023            break;
4024        case TOOLBAR_RESUME_TAG:
4025            [self resumeAllTorrents: sender];
4026            break;
4027    }
4028}
4029
4030- (void) selectedToolbarClicked: (id) sender
4031{
4032    NSInteger tagValue = [sender isKindOfClass: [NSSegmentedControl class]]
4033                    ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [(NSControl *)sender tag];
4034    switch (tagValue)
4035    {
4036        case TOOLBAR_PAUSE_TAG:
4037            [self stopSelectedTorrents: sender];
4038            break;
4039        case TOOLBAR_RESUME_TAG:
4040            [self resumeSelectedTorrents: sender];
4041            break;
4042    }
4043}
4044
4045- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
4046{
4047    return [NSArray arrayWithObjects:
4048            TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_OPEN_WEB, TOOLBAR_REMOVE,
4049            TOOLBAR_PAUSE_RESUME_SELECTED, TOOLBAR_PAUSE_RESUME_ALL,
4050            TOOLBAR_QUICKLOOK, TOOLBAR_FILTER, TOOLBAR_INFO,
4051            NSToolbarSeparatorItemIdentifier,
4052            NSToolbarSpaceItemIdentifier,
4053            NSToolbarFlexibleSpaceItemIdentifier,
4054            NSToolbarCustomizeToolbarItemIdentifier, nil];
4055}
4056
4057- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
4058{
4059    return [NSArray arrayWithObjects:
4060            TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_REMOVE, NSToolbarSpaceItemIdentifier,
4061            TOOLBAR_PAUSE_RESUME_ALL, NSToolbarFlexibleSpaceItemIdentifier,
4062            TOOLBAR_QUICKLOOK, TOOLBAR_FILTER, TOOLBAR_INFO, nil];
4063}
4064
4065- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
4066{
4067    NSString * ident = [toolbarItem itemIdentifier];
4068   
4069    //enable remove item
4070    if ([ident isEqualToString: TOOLBAR_REMOVE])
4071        return [fTableView numberOfSelectedRows] > 0;
4072
4073    //enable pause all item
4074    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
4075    {
4076        for (Torrent * torrent in fTorrents)
4077            if ([torrent isActive] || [torrent waitingToStart])
4078                return YES;
4079        return NO;
4080    }
4081
4082    //enable resume all item
4083    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
4084    {
4085        for (Torrent * torrent in fTorrents)
4086            if (![torrent isActive] && ![torrent waitingToStart] && ![torrent isFinishedSeeding])
4087                return YES;
4088        return NO;
4089    }
4090
4091    //enable pause item
4092    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
4093    {
4094        for (Torrent * torrent in [fTableView selectedTorrents])
4095            if ([torrent isActive] || [torrent waitingToStart])
4096                return YES;
4097        return NO;
4098    }
4099   
4100    //enable resume item
4101    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
4102    {
4103        for (Torrent * torrent in [fTableView selectedTorrents])
4104            if (![torrent isActive] && ![torrent waitingToStart])
4105                return YES;
4106        return NO;
4107    }
4108   
4109    //set info item
4110    if ([ident isEqualToString: TOOLBAR_INFO])
4111    {
4112        [(NSButton *)[toolbarItem view] setState: [[fInfoController window] isVisible]];
4113        return YES;
4114    }
4115   
4116    //set filter item
4117    if ([ident isEqualToString: TOOLBAR_FILTER])
4118    {
4119        [(NSButton *)[toolbarItem view] setState: fFilterBar != nil];
4120        return YES;
4121    }
4122   
4123    //set quick look item
4124    if ([ident isEqualToString: TOOLBAR_QUICKLOOK])
4125    {
4126        [(NSButton *)[toolbarItem view] setState: [QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]];
4127        return YES;
4128    }
4129
4130    return YES;
4131}
4132
4133- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
4134{
4135    SEL action = [menuItem action];
4136   
4137    if (action == @selector(toggleSpeedLimit:))
4138    {
4139        [menuItem setState: [fDefaults boolForKey: @"SpeedLimit"] ? NSOnState : NSOffState];
4140        return YES;
4141    }
4142   
4143    //only enable some items if it is in a context menu or the window is useable
4144    BOOL canUseTable = [fWindow isKeyWindow] || [[menuItem menu] supermenu] != [NSApp mainMenu];
4145
4146    //enable open items
4147    if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:))
4148        return [fWindow attachedSheet] == nil;
4149   
4150    //enable sort options
4151    if (action == @selector(setSort:))
4152    {
4153        NSString * sortType;
4154        switch ([menuItem tag])
4155        {
4156            case SORT_ORDER_TAG:
4157                sortType = SORT_ORDER;
4158                break;
4159            case SORT_DATE_TAG:
4160                sortType = SORT_DATE;
4161                break;
4162            case SORT_NAME_TAG:
4163                sortType = SORT_NAME;
4164                break;
4165            case SORT_PROGRESS_TAG:
4166                sortType = SORT_PROGRESS;
4167                break;
4168            case SORT_STATE_TAG:
4169                sortType = SORT_STATE;
4170                break;
4171            case SORT_TRACKER_TAG:
4172                sortType = SORT_TRACKER;
4173                break;
4174            case SORT_ACTIVITY_TAG:
4175                sortType = SORT_ACTIVITY;
4176                break;
4177            case SORT_SIZE_TAG:
4178                sortType = SORT_SIZE;
4179                break;
4180            default:
4181                NSAssert1(NO, @"Unknown sort tag received: %ld", [menuItem tag]);
4182        }
4183       
4184        [menuItem setState: [sortType isEqualToString: [fDefaults stringForKey: @"Sort"]] ? NSOnState : NSOffState];
4185        return [fWindow isVisible];
4186    }
4187   
4188    if (action == @selector(setGroup:))
4189    {
4190        BOOL checked = NO;
4191       
4192        NSInteger index = [menuItem tag];
4193        for (Torrent * torrent in [fTableView selectedTorrents])
4194            if (index == [torrent groupValue])
4195            {
4196                checked = YES;
4197                break;
4198            }
4199       
4200        [menuItem setState: checked ? NSOnState : NSOffState];
4201        return canUseTable && [fTableView numberOfSelectedRows] > 0;
4202    }
4203   
4204    if (action == @selector(toggleSmallView:))
4205    {
4206        [menuItem setState: [fDefaults boolForKey: @"SmallView"] ? NSOnState : NSOffState];
4207        return [fWindow isVisible];
4208    }
4209   
4210    if (action == @selector(togglePiecesBar:))
4211    {
4212        [menuItem setState: [fDefaults boolForKey: @"PiecesBar"] ? NSOnState : NSOffState];
4213        return [fWindow isVisible];
4214    }
4215   
4216    #warning remove when menu is removed (10.7-only)
4217    if (action == @selector(toggleStatusString:))
4218    {
4219        if ([fDefaults boolForKey: @"SmallView"])
4220        {
4221            [menuItem setTitle: NSLocalizedString(@"Remaining Time", "Action menu -> status string toggle")];
4222            [menuItem setState: ![fDefaults boolForKey: @"DisplaySmallStatusRegular"] ? NSOnState : NSOffState];
4223        }
4224        else
4225        {
4226            [menuItem setTitle: NSLocalizedString(@"Status of Selected Files", "Action menu -> status string toggle")];
4227            [menuItem setState: [fDefaults boolForKey: @"DisplayStatusProgressSelected"] ? NSOnState : NSOffState];
4228        }
4229       
4230        return [fWindow isVisible];
4231    }
4232   
4233    if (action == @selector(toggleAvailabilityBar:))
4234    {
4235        [menuItem setState: [fDefaults boolForKey: @"DisplayProgressBarAvailable"] ? NSOnState : NSOffState];
4236        return [fWindow isVisible];
4237    }
4238   
4239    if (action == @selector(setLimitGlobalEnabled:))
4240    {
4241        BOOL upload = [menuItem menu] == fUploadMenu;
4242        BOOL limit = menuItem == (upload ? fUploadLimitItem : fDownloadLimitItem);
4243        if (limit)
4244            [menuItem setTitle: [NSString stringWithFormat: NSLocalizedString(@"Limit (%d KB/s)",
4245                                    "Action menu -> upload/download limit"),
4246                                    [fDefaults integerForKey: upload ? @"UploadLimit" : @"DownloadLimit"]]];
4247       
4248        [menuItem setState: [fDefaults boolForKey: upload ? @"CheckUpload" : @"CheckDownload"] ? limit : !limit];
4249        return YES;
4250    }
4251   
4252    if (action == @selector(setRatioGlobalEnabled:))
4253    {
4254        BOOL check = menuItem == fCheckRatioItem;
4255        if (check)
4256            [menuItem setTitle: [NSString localizedStringWithFormat: NSLocalizedString(@"Stop at Ratio (%.2f)",
4257                                    "Action menu -> ratio stop"), [fDefaults floatForKey: @"RatioLimit"]]];
4258       
4259        [menuItem setState: [fDefaults boolForKey: @"RatioCheck"] ? check : !check];
4260        return YES;
4261    }
4262
4263    //enable show info
4264    if (action == @selector(showInfo:))
4265    {
4266        NSString * title = [[fInfoController window] isVisible] ? NSLocalizedString(@"Hide Inspector", "View menu -> Inspector")
4267                            : NSLocalizedString(@"Show Inspector", "View menu -> Inspector");
4268        [menuItem setTitle: title];
4269
4270        return YES;
4271    }
4272   
4273    //enable prev/next inspector tab
4274    if (action == @selector(setInfoTab:))
4275        return [[fInfoController window] isVisible];
4276   
4277    //enable toggle status bar
4278    if (action == @selector(toggleStatusBar:))
4279    {
4280        NSString * title = !fStatusBar ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar")
4281                            : NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar");
4282        [menuItem setTitle: title];
4283
4284        return [fWindow isVisible];
4285    }
4286   
4287    //enable toggle filter bar
4288    if (action == @selector(toggleFilterBar:))
4289    {
4290        NSString * title = !fFilterBar ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar")
4291                            : NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar");
4292        [menuItem setTitle: title];
4293
4294        return [fWindow isVisible];
4295    }
4296   
4297    //enable prev/next filter button
4298    if (action == @selector(switchFilter:))
4299        return [fWindow isVisible] && fFilterBar;
4300   
4301    //enable reveal in finder
4302    if (action == @selector(revealFile:))
4303        return canUseTable && [fTableView numberOfSelectedRows] > 0;
4304   
4305    //enable renaming file/folder
4306    if (action == @selector(renameSelected:))
4307        return canUseTable && [fTableView numberOfSelectedRows] == 1;
4308
4309    //enable remove items
4310    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:))
4311    {
4312        BOOL warning = NO;
4313       
4314        for (Torrent * torrent in [fTableView selectedTorrents])
4315        {
4316            if ([torrent isActive])
4317            {
4318                if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? ![torrent isSeeding] : YES)
4319                {
4320                    warning = YES;
4321                    break;
4322                }
4323            }
4324        }
4325   
4326        //append or remove ellipsis when needed
4327        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
4328        if (warning && [fDefaults boolForKey: @"CheckRemove"])
4329        {
4330            if (![title hasSuffix: ellipsis])
4331                [menuItem setTitle: [title stringByAppendingEllipsis]];
4332        }
4333        else
4334        {
4335            if ([title hasSuffix: ellipsis])
4336                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
4337        }
4338       
4339        return canUseTable && [fTableView numberOfSelectedRows] > 0;
4340    }
4341   
4342    //remove all completed transfers item
4343    if (action == @selector(clearCompleted:))
4344    {
4345        //append or remove ellipsis when needed
4346        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
4347        if ([fDefaults boolForKey: @"WarningRemoveCompleted"])
4348        {
4349            if (![title hasSuffix: ellipsis])
4350                [menuItem setTitle: [title stringByAppendingEllipsis]];
4351        }
4352        else
4353        {
4354            if ([title hasSuffix: ellipsis])
4355                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
4356        }
4357       
4358        for (Torrent * torrent in fTorrents)
4359            if ([torrent isFinishedSeeding])
4360                return YES;
4361        return NO;
4362    }
4363
4364    //enable pause all item
4365    if (action == @selector(stopAllTorrents:))
4366    {
4367        for (Torrent * torrent in fTorrents)
4368            if ([torrent isActive] || [torrent waitingToStart])
4369                return YES;
4370        return NO;
4371    }
4372   
4373    //enable resume all item
4374    if (action == @selector(resumeAllTorrents:))
4375    {
4376        for (Torrent * torrent in fTorrents)
4377            if (![torrent isActive] && ![torrent waitingToStart] && ![torrent isFinishedSeeding])
4378                return YES;
4379        return NO;
4380    }
4381   
4382    //enable resume all waiting item
4383    if (action == @selector(resumeWaitingTorrents:))
4384    {
4385        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
4386            return NO;
4387   
4388        for (Torrent * torrent in fTorrents)
4389            if ([torrent waitingToStart])
4390                return YES;
4391        return NO;
4392    }
4393   
4394    //enable resume selected waiting item
4395    if (action == @selector(resumeSelectedTorrentsNoWait:))
4396    {
4397        if (!canUseTable)
4398            return NO;
4399       
4400        for (Torrent * torrent in [fTableView selectedTorrents])
4401            if (![torrent isActive])
4402                return YES;
4403        return NO;
4404    }
4405
4406    //enable pause item
4407    if (action == @selector(stopSelectedTorrents:))
4408    {
4409        if (!canUseTable)
4410            return NO;
4411   
4412        for (Torrent * torrent in [fTableView selectedTorrents])
4413            if ([torrent isActive] || [torrent waitingToStart])
4414                return YES;
4415        return NO;
4416    }
4417   
4418    //enable resume item
4419    if (action == @selector(resumeSelectedTorrents:))
4420    {
4421        if (!canUseTable)
4422            return NO;
4423   
4424        for (Torrent * torrent in [fTableView selectedTorrents])
4425            if (![torrent isActive] && ![torrent waitingToStart])
4426                return YES;
4427        return NO;
4428    }
4429   
4430    //enable manual announce item
4431    if (action == @selector(announceSelectedTorrents:))
4432    {
4433        if (!canUseTable)
4434            return NO;
4435       
4436        for (Torrent * torrent in [fTableView selectedTorrents])
4437            if ([torrent canManualAnnounce])
4438                return YES;
4439        return NO;
4440    }
4441   
4442    //enable reset cache item
4443    if (action == @selector(verifySelectedTorrents:))
4444    {
4445        if (!canUseTable)
4446            return NO;
4447           
4448        for (Torrent * torrent in [fTableView selectedTorrents])
4449            if (![torrent isMagnet])
4450                return YES;
4451        return NO;
4452    }
4453   
4454    //enable move torrent file item
4455    if (action == @selector(moveDataFilesSelected:))
4456        return canUseTable && [fTableView numberOfSelectedRows] > 0;
4457   
4458    //enable copy torrent file item
4459    if (action == @selector(copyTorrentFiles:))
4460    {
4461        if (!canUseTable)
4462            return NO;
4463       
4464        for (Torrent * torrent in [fTableView selectedTorrents])
4465            if (![torrent isMagnet])
4466                return YES;
4467        return NO;
4468    }
4469   
4470    //enable copy torrent file item
4471    if (action == @selector(copyMagnetLinks:))
4472        return canUseTable && [fTableView numberOfSelectedRows] > 0;
4473   
4474    //enable reverse sort item
4475    if (action == @selector(setSortReverse:))
4476    {
4477        const BOOL isReverse = [menuItem tag] == SORT_DESC_TAG;
4478        [menuItem setState: (isReverse == [fDefaults boolForKey: @"SortReverse"]) ? NSOnState : NSOffState];
4479        return ![[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER];
4480    }
4481   
4482    //enable group sort item
4483    if (action == @selector(setSortByGroup:))
4484    {
4485        [menuItem setState: [fDefaults boolForKey: @"SortByGroup"] ? NSOnState : NSOffState];
4486        return YES;
4487    }
4488   
4489    if (action == @selector(toggleQuickLook:))
4490    {
4491        const BOOL visible =[QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible];
4492        //text consistent with Finder
4493        NSString * title = !visible ? NSLocalizedString(@"Quick Look", "View menu -> Quick Look")
4494                                    : NSLocalizedString(@"Close Quick Look", "View menu -> Quick Look");
4495        [menuItem setTitle: title];
4496       
4497        return YES;
4498    }
4499   
4500    return YES;
4501}
4502
4503- (void) sleepCallback: (natural_t) messageType argument: (void *) messageArgument
4504{
4505    switch (messageType)
4506    {
4507        case kIOMessageSystemWillSleep:
4508        {
4509            //stop all transfers (since some are active) before going to sleep and remember to resume when we wake up
4510            BOOL anyActive = NO;
4511            for (Torrent * torrent in fTorrents)
4512            {
4513                if ([torrent isActive])
4514                    anyActive = YES;
4515                [torrent sleep]; //have to call on all, regardless if they are active
4516            }
4517           
4518            //if there are any running transfers, wait 15 seconds for them to stop
4519            if (anyActive)
4520            {
4521                sleep(15);
4522            }
4523           
4524            IOAllowPowerChange(fRootPort, (long) messageArgument);
4525            break;
4526        }
4527
4528        case kIOMessageCanSystemSleep:
4529            if ([fDefaults boolForKey: @"SleepPrevent"])
4530            {
4531                //prevent idle sleep unless no torrents are active
4532                for (Torrent * torrent in fTorrents)
4533                    if ([torrent isActive] && ![torrent isStalled] && ![torrent isError])
4534                    {
4535                        IOCancelPowerChange(fRootPort, (long) messageArgument);
4536                        return;
4537                    }
4538            }
4539           
4540            IOAllowPowerChange(fRootPort, (long) messageArgument);
4541            break;
4542
4543        case kIOMessageSystemHasPoweredOn:
4544            //resume sleeping transfers after we wake up
4545            for (Torrent * torrent in fTorrents)
4546                [torrent wakeUp];
4547            break;
4548    }
4549}
4550
4551- (NSMenu *) applicationDockMenu: (NSApplication *) sender
4552{
4553    if (fQuitting)
4554        return nil;
4555   
4556    NSUInteger seeding = 0, downloading = 0;
4557    for (Torrent * torrent in fTorrents)
4558    {
4559        if ([torrent isSeeding])
4560            seeding++;
4561        else if ([torrent isActive])
4562            downloading++;
4563        else;
4564    }
4565   
4566    NSMenu * menu = [[NSMenu alloc] init];
4567   
4568    if (seeding > 0)
4569    {
4570        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Seeding", "Dock item - Seeding"), seeding];
4571        [menu addItemWithTitle: title action: nil keyEquivalent: @""];
4572    }
4573   
4574    if (downloading > 0)
4575    {
4576        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Downloading", "Dock item - Downloading"), downloading];
4577        [menu addItemWithTitle: title action: nil keyEquivalent: @""];
4578    }
4579   
4580    if (seeding > 0 || downloading > 0)
4581        [menu addItem: [NSMenuItem separatorItem]];
4582   
4583    [menu addItemWithTitle: NSLocalizedString(@"Pause All", "Dock item") action: @selector(stopAllTorrents:) keyEquivalent: @""];
4584    [menu addItemWithTitle: NSLocalizedString(@"Resume All", "Dock item") action: @selector(resumeAllTorrents:) keyEquivalent: @""];
4585    [menu addItem: [NSMenuItem separatorItem]];
4586    [menu addItemWithTitle: NSLocalizedString(@"Speed Limit", "Dock item") action: @selector(toggleSpeedLimit:) keyEquivalent: @""];
4587   
4588    return [menu autorelease];
4589}
4590
4591- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
4592{
4593    //if auto size is enabled, the current frame shouldn't need to change
4594    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
4595   
4596    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
4597    return frame;
4598}
4599
4600- (void) setWindowSizeToFit
4601{
4602    if ([fDefaults boolForKey: @"AutoSize"])
4603    {
4604        NSScrollView * scrollView = [fTableView enclosingScrollView];
4605       
4606        [scrollView setHasVerticalScroller: NO];
4607        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
4608        [scrollView setHasVerticalScroller: YES];
4609       
4610        [self setWindowMinMaxToCurrent];
4611    }
4612}
4613
4614- (NSRect) sizedWindowFrame
4615{
4616    NSUInteger groups = ([fDisplayedTorrents count] > 0 && ![[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [Torrent class]])
4617                        ? [fDisplayedTorrents count] : 0;
4618   
4619    CGFloat heightChange = (GROUP_SEPARATOR_HEIGHT + [fTableView intercellSpacing].height) * groups
4620                        + ([fTableView rowHeight] + [fTableView intercellSpacing].height) * ([fTableView numberOfRows] - groups)
4621                        - NSHeight([[fTableView enclosingScrollView] frame]);
4622   
4623    return [self windowFrameByAddingHeight: heightChange checkLimits: YES];
4624}
4625
4626- (void) updateForAutoSize
4627{
4628    if ([fDefaults boolForKey: @"AutoSize"])
4629        [self setWindowSizeToFit];
4630    else
4631    {
4632        NSSize contentMinSize = [fWindow contentMinSize];
4633        contentMinSize.height = [self minWindowContentSizeAllowed];
4634       
4635        [fWindow setContentMinSize: contentMinSize];
4636       
4637        NSSize contentMaxSize = [fWindow contentMaxSize];
4638        contentMaxSize.height = FLT_MAX;
4639        [fWindow setContentMaxSize: contentMaxSize];
4640    }
4641}
4642
4643- (void) setWindowMinMaxToCurrent
4644{
4645    const CGFloat height = NSHeight([[fWindow contentView] frame]);
4646   
4647    NSSize minSize = [fWindow contentMinSize],
4648            maxSize = [fWindow contentMaxSize];
4649    minSize.height = height;
4650    maxSize.height = height;
4651   
4652    [fWindow setContentMinSize: minSize];
4653    [fWindow setContentMaxSize: maxSize];
4654}
4655
4656- (CGFloat) minWindowContentSizeAllowed
4657{
4658    CGFloat contentMinHeight = NSHeight([[fWindow contentView] frame]) - NSHeight([[fTableView enclosingScrollView] frame])
4659                                + [fTableView rowHeight] + [fTableView intercellSpacing].height;
4660    return contentMinHeight;
4661}
4662
4663- (void) updateForExpandCollape
4664{
4665    [self setWindowSizeToFit];
4666    [self setBottomCountText: YES];
4667}
4668
4669- (void) showMainWindow: (id) sender
4670{
4671    [fWindow makeKeyAndOrderFront: nil];
4672}
4673
4674- (void) windowDidBecomeMain: (NSNotification *) notification
4675{
4676    [fBadger clearCompleted];
4677    [self updateUI];
4678}
4679
4680- (void) applicationWillUnhide: (NSNotification *) notification
4681{
4682    [self updateUI];
4683}
4684
4685- (void) toggleQuickLook: (id) sender
4686{
4687    if ([[QLPreviewPanel sharedPreviewPanel] isVisible])
4688        [[QLPreviewPanel sharedPreviewPanel] orderOut: nil];
4689    else
4690        [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront: nil];
4691}
4692
4693- (void) linkHomepage: (id) sender
4694{
4695    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
4696}
4697
4698- (void) linkForums: (id) sender
4699{
4700    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
4701}
4702
4703- (void) linkTrac: (id) sender
4704{
4705    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: TRAC_URL]];
4706}
4707
4708- (void) linkDonate: (id) sender
4709{
4710    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: DONATE_URL]];
4711}
4712
4713- (void) updaterWillRelaunchApplication: (SUUpdater *) updater
4714{
4715    fQuitRequested = YES;
4716}
4717
4718- (NSDictionary *) registrationDictionaryForGrowl
4719{
4720    NSArray * notifications = @[GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE, GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT];
4721   
4722    return @{GROWL_NOTIFICATIONS_ALL : notifications,
4723                GROWL_NOTIFICATIONS_DEFAULT : notifications };
4724}
4725
4726- (void) growlNotificationWasClicked: (id) clickContext
4727{
4728    if (![clickContext isKindOfClass: [NSDictionary class]])
4729        return;
4730   
4731    NSString * type = [clickContext objectForKey: @"Type"], * location;
4732    if (([type isEqualToString: GROWL_DOWNLOAD_COMPLETE] || [type isEqualToString: GROWL_SEEDING_COMPLETE])
4733            && (location = [clickContext objectForKey: @"Location"]))
4734    {
4735        NSURL * file = [NSURL fileURLWithPath: location];
4736        [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: [NSArray arrayWithObject: file]];
4737    }
4738}
4739
4740- (void) rpcCallback: (tr_rpc_callback_type) type forTorrentStruct: (struct tr_torrent *) torrentStruct
4741{
4742    @autoreleasepool
4743    {
4744        //get the torrent
4745        Torrent * torrent = nil;
4746        if (torrentStruct != NULL && (type != TR_RPC_TORRENT_ADDED && type != TR_RPC_SESSION_CHANGED && type != TR_RPC_SESSION_CLOSE))
4747        {
4748            for (torrent in fTorrents)
4749                if (torrentStruct == [torrent torrentStruct])
4750                {
4751                    [torrent retain];
4752                    break;
4753                }
4754           
4755            if (!torrent)
4756            {
4757                NSLog(@"No torrent found matching the given torrent struct from the RPC callback!");
4758                return;
4759            }
4760        }
4761       
4762        switch (type)
4763        {
4764            case TR_RPC_TORRENT_ADDED:
4765                [self performSelectorOnMainThread: @selector(rpcAddTorrentStruct:) withObject:
4766                    [[NSValue valueWithPointer: torrentStruct] retain] waitUntilDone: NO];
4767                break;
4768           
4769            case TR_RPC_TORRENT_STARTED:
4770            case TR_RPC_TORRENT_STOPPED:
4771                [self performSelectorOnMainThread: @selector(rpcStartedStoppedTorrent:) withObject: torrent waitUntilDone: NO];
4772                break;
4773           
4774            case TR_RPC_TORRENT_REMOVING:
4775                [self performSelectorOnMainThread: @selector(rpcRemoveTorrent:) withObject: torrent waitUntilDone: NO];
4776                break;
4777           
4778            case TR_RPC_TORRENT_TRASHING:
4779                [self performSelectorOnMainThread: @selector(rpcRemoveTorrentDeleteData:) withObject: torrent waitUntilDone: NO];
4780                break;
4781           
4782            case TR_RPC_TORRENT_CHANGED:
4783                [self performSelectorOnMainThread: @selector(rpcChangedTorrent:) withObject: torrent waitUntilDone: NO];
4784                break;
4785           
4786            case TR_RPC_TORRENT_MOVED:
4787                [self performSelectorOnMainThread: @selector(rpcMovedTorrent:) withObject: torrent waitUntilDone: NO];
4788                break;
4789           
4790            case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED:
4791                [self performSelectorOnMainThread: @selector(rpcUpdateQueue) withObject: nil waitUntilDone: NO];
4792                break;
4793           
4794            case TR_RPC_SESSION_CHANGED:
4795                [fPrefsController performSelectorOnMainThread: @selector(rpcUpdatePrefs) withObject: nil waitUntilDone: NO];
4796                break;
4797           
4798            case TR_RPC_SESSION_CLOSE:
4799                fQuitRequested = YES;
4800                [NSApp performSelectorOnMainThread: @selector(terminate:) withObject: self waitUntilDone: NO];
4801                break;
4802           
4803            default:
4804                NSAssert1(NO, @"Unknown RPC command received: %d", type);
4805                [torrent release];
4806        }
4807    }
4808}
4809
4810- (void) rpcAddTorrentStruct: (NSValue *) torrentStructPtr
4811{
4812    tr_torrent * torrentStruct = (tr_torrent *)[torrentStructPtr pointerValue];
4813    [torrentStructPtr release];
4814   
4815    NSString * location = nil;
4816    if (tr_torrentGetDownloadDir(torrentStruct) != NULL)
4817        location = [NSString stringWithUTF8String: tr_torrentGetDownloadDir(torrentStruct)];
4818   
4819    Torrent * torrent = [[Torrent alloc] initWithTorrentStruct: torrentStruct location: location lib: fLib];
4820   
4821    //change the location if the group calls for it (this has to wait until after the torrent is created)
4822    if ([[GroupsController groups] usesCustomDownloadLocationForIndex: [torrent groupValue]])
4823    {
4824        location = [[GroupsController groups] customDownloadLocationForIndex: [torrent groupValue]];
4825        [torrent changeDownloadFolderBeforeUsing: location determinationType: TorrentDeterminationAutomatic];
4826    }
4827   
4828    [torrent update];
4829    [fTorrents addObject: torrent];
4830    [torrent release];
4831   
4832    if (!fAddingTransfers)
4833        fAddingTransfers = [[NSMutableSet alloc] init];
4834    [fAddingTransfers addObject: torrent];
4835   
4836    [self fullUpdateUI];
4837}
4838
4839- (void) rpcRemoveTorrent: (Torrent *) torrent
4840{
4841    [self confirmRemoveTorrents: @[ torrent ] deleteData: NO];
4842    [torrent release];
4843}
4844
4845- (void) rpcRemoveTorrentDeleteData: (Torrent *) torrent
4846{
4847    [self confirmRemoveTorrents: @[ torrent ] deleteData: YES];
4848    [torrent release];
4849}
4850
4851- (void) rpcStartedStoppedTorrent: (Torrent *) torrent
4852{
4853    [torrent update];
4854    [torrent release];
4855   
4856    [self updateUI];
4857    [self applyFilter];
4858    [self updateTorrentHistory];
4859}
4860
4861- (void) rpcChangedTorrent: (Torrent *) torrent
4862{
4863    [torrent update];
4864   
4865    if ([[fTableView selectedTorrents] containsObject: torrent])
4866    {
4867        [fInfoController updateInfoStats]; //this will reload the file table
4868        [fInfoController updateOptions];
4869    }
4870   
4871    [torrent release];
4872}
4873
4874- (void) rpcMovedTorrent: (Torrent *) torrent
4875{
4876    [torrent update];
4877    [torrent updateTimeMachineExclude];
4878   
4879    if ([[fTableView selectedTorrents] containsObject: torrent])
4880        [fInfoController updateInfoStats];
4881   
4882    [torrent release];
4883}
4884
4885- (void) rpcUpdateQueue
4886{
4887    for (Torrent * torrent in fTorrents)
4888        [torrent update];
4889   
4890    NSArray * selectedValues = [fTableView selectedValues];
4891   
4892    NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey: @"queuePosition" ascending: YES];
4893    NSArray * descriptors = [NSArray arrayWithObject: descriptor];
4894   
4895    [fTorrents sortUsingDescriptors: descriptors];
4896   
4897    [self fullUpdateUI];
4898   
4899    [fTableView selectValues: selectedValues];
4900}
4901
4902@end
Note: See TracBrowser for help on using the repository browser.