source: trunk/macosx/Controller.m @ 13414

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

#4963 Support Notification Center on 10.8

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