source: trunk/macosx/Controller.m @ 8966

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

For the first time shown, don't show the "never show again" on the donation dialog. The reasoning for this is that new users will get that dialog without a chance to thoroughly use the app, so they can have a week before having the option to hide forever.

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