source: trunk/macosx/Controller.m @ 10948

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

Mac build compiles again

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