source: trunk/macosx/Controller.m @ 9342

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

use libtransmission for moving, incomplete directory, and directory controlling in general (still rough around the edges)

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