source: trunk/macosx/Controller.m @ 13321

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

#4922 On Lion, use window restoration on the preferences window

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