source: trunk/macosx/Controller.m @ 11988

Last change on this file since 11988 was 11988, checked in by livings124, 11 years ago

when resizing the filter bar, use the filter bar width instead of the status bar width; silence a warning

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