source: trunk/macosx/Controller.m @ 5004

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

insignificant filtering improvements

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