source: trunk/macosx/Controller.m @ 9303

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

when Quick Look is visible, make the toolbar icon blue and append "Close" to the menu item

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