source: trunk/macosx/Controller.m @ 10272

Last change on this file since 10272 was 10272, checked in by livings124, 12 years ago

in the torrent removal dialog, magnet links, as well as torrent files, can be used to re-add transfers

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