source: trunk/macosx/Controller.m @ 9606

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

#2611 Transmission doesn't recognize magnet dragged onto application window

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