source: trunk/macosx/Controller.m @ 11352

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

#3669 Streamline the bottom-left buttons, removing the button borders and using an updated turtle image. There is glue-code for this to work on localizations until they update the xib files.

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