source: trunk/macosx/Controller.m @ 11224

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

update strings file

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