source: trunk/macosx/Controller.m @ 11441

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

set the status bar's upload and download numbers to the raised style, like the rest of the items in the bar

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