source: trunk/macosx/Controller.m @ 11885

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

remove selected transfers when removing completed transfers, removing through RPC, etc. (whenever removing transfers that aren't the selected transfers)

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