source: trunk/macosx/Controller.m @ 11992

Last change on this file since 11992 was 11992, checked in by livings124, 12 years ago

change the status bar's left menu to be within the button (in the xib)

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