source: trunk/macosx/Controller.m @ 9722

Last change on this file since 9722 was 9722, checked in by livings124, 13 years ago

better implementation of r9721

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