source: trunk/macosx/Controller.m @ 13870

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

update Mac code for logging changes in r13868

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