source: trunk/macosx/Controller.m @ 10073

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

#2720 When adding a duplicate magnet link, display an appropriate error message instead of a generic message

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