source: trunk/macosx/Controller.m @ 6256

Last change on this file since 6256 was 6256, checked in by livings124, 15 years ago

when stopping all downloads (of torrent files) on quit, get an enumerator straight from the dictionary instead of first generating an array

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