source: trunk/macosx/Controller.m @ 9234

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

refine the legal text

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