source: trunk/macosx/Controller.m @ 10469

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

add an option to the prefs to show the add window for magnet links

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