source: trunk/macosx/Controller.m @ 9615

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

apply auto-group custom locations for magnet links

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