source: trunk/macosx/Controller.m @ 7427

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

cleanup for the group menu filling adjustment

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