source: trunk/macosx/Controller.m @ 8098

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

the Mac build works with the new speed limit libT code - still a bit quirky

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