source: trunk/macosx/Controller.m @ 5002

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

fix a problem with groups on 10.4

  • Property svn:keywords set to Date Rev Author Id
File size: 147.0 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 5002 2008-02-10 16:56:13Z 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 = [NSMutableArray arrayWithArray: 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    [fDisplayedTorrents setArray: [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: fDisplayedTorrents];
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        [fDisplayedTorrents sortUsingDescriptors: [NSArray arrayWithObject: groupDescriptor]];
1932       
1933        NSMutableArray * groups = [NSMutableArray array], * groupTorrents;
1934        int oldGroupValue = -2;
1935        for (i = 0; i < [fDisplayedTorrents count]; i++)
1936        {
1937            Torrent * torrent = [fDisplayedTorrents 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   
1955    //actually sort
1956    [self sortTorrentsIgnoreSelected];
1957    [fTableView selectValues: selectedValues];
1958   
1959    //reset expanded/collapsed rows
1960    if (groupRows)
1961    {
1962        enumerator = [fDisplayedTorrents objectEnumerator];
1963        NSDictionary * dict;
1964        while ((dict = [enumerator nextObject]))
1965        {
1966            if ([fTableView isGroupCollapsed: [[dict objectForKey: @"Group"] intValue]])
1967                [fTableView collapseItem: dict];
1968            else
1969                [fTableView expandItem: dict];
1970        }
1971    }
1972   
1973    [self setBottomCountTextFiltering: groupRows || filterStatus || filterGroup || filterText];
1974
1975    [self setWindowSizeToFit];
1976}
1977
1978//resets filter and sorts torrents
1979- (void) setFilter: (id) sender
1980{
1981    NSString * oldFilterType = [fDefaults stringForKey: @"Filter"];
1982   
1983    NSButton * prevFilterButton;
1984    if ([oldFilterType isEqualToString: FILTER_PAUSE])
1985        prevFilterButton = fPauseFilterButton;
1986    else if ([oldFilterType isEqualToString: FILTER_ACTIVE])
1987        prevFilterButton = fActiveFilterButton;
1988    else if ([oldFilterType isEqualToString: FILTER_SEED])
1989        prevFilterButton = fSeedFilterButton;
1990    else if ([oldFilterType isEqualToString: FILTER_DOWNLOAD])
1991        prevFilterButton = fDownloadFilterButton;
1992    else
1993        prevFilterButton = fNoFilterButton;
1994   
1995    if (sender != prevFilterButton)
1996    {
1997        [prevFilterButton setState: NSOffState];
1998        [sender setState: NSOnState];
1999
2000        NSString * filterType;
2001        if (sender == fActiveFilterButton)
2002            filterType = FILTER_ACTIVE;
2003        else if (sender == fDownloadFilterButton)
2004            filterType = FILTER_DOWNLOAD;
2005        else if (sender == fPauseFilterButton)
2006            filterType = FILTER_PAUSE;
2007        else if (sender == fSeedFilterButton)
2008            filterType = FILTER_SEED;
2009        else
2010            filterType = FILTER_NONE;
2011
2012        [fDefaults setObject: filterType forKey: @"Filter"];
2013    }
2014    else
2015        [sender setState: NSOnState];
2016
2017    [self applyFilter: nil];
2018}
2019
2020- (void) setFilterSearchType: (id) sender
2021{
2022    NSString * oldFilterType = [fDefaults stringForKey: @"FilterSearchType"];
2023   
2024    int prevTag, currentTag = [sender tag];
2025    if ([oldFilterType isEqualToString: FILTER_TYPE_TRACKER])
2026        prevTag = FILTER_TYPE_TAG_TRACKER;
2027    else
2028        prevTag = FILTER_TYPE_TAG_NAME;
2029   
2030    if (currentTag != prevTag)
2031    {
2032        NSString * filterType;
2033        if (currentTag == FILTER_TYPE_TAG_TRACKER)
2034            filterType = FILTER_TYPE_TRACKER;
2035        else
2036            filterType = FILTER_TYPE_NAME;
2037       
2038        [fDefaults setObject: filterType forKey: @"FilterSearchType"];
2039       
2040        [[fSearchFilterField cell] setPlaceholderString: [sender title]];
2041    }
2042   
2043    [self applyFilter: nil];
2044}
2045
2046- (void) switchFilter: (id) sender
2047{
2048    NSString * filterType = [fDefaults stringForKey: @"Filter"];
2049   
2050    NSButton * button;
2051    if ([filterType isEqualToString: FILTER_NONE])
2052        button = sender == fNextFilterItem ? fActiveFilterButton : fPauseFilterButton;
2053    else if ([filterType isEqualToString: FILTER_ACTIVE])
2054        button = sender == fNextFilterItem ? fDownloadFilterButton : fNoFilterButton;
2055    else if ([filterType isEqualToString: FILTER_DOWNLOAD])
2056        button = sender == fNextFilterItem ? fSeedFilterButton : fActiveFilterButton;
2057    else if ([filterType isEqualToString: FILTER_SEED])
2058        button = sender == fNextFilterItem ? fPauseFilterButton : fDownloadFilterButton;
2059    else if ([filterType isEqualToString: FILTER_PAUSE])
2060        button = sender == fNextFilterItem ? fNoFilterButton : fSeedFilterButton;
2061    else
2062        button = fNoFilterButton;
2063   
2064    [self setFilter: button];
2065}
2066
2067- (void) setStatusLabel: (id) sender
2068{
2069    NSString * statusLabel;
2070    switch ([sender tag])
2071    {
2072        case STATUS_RATIO_TOTAL_TAG:
2073            statusLabel = STATUS_RATIO_TOTAL;
2074            break;
2075        case STATUS_RATIO_SESSION_TAG:
2076            statusLabel = STATUS_RATIO_SESSION;
2077            break;
2078        case STATUS_TRANSFER_TOTAL_TAG:
2079            statusLabel = STATUS_TRANSFER_TOTAL;
2080            break;
2081        case STATUS_TRANSFER_SESSION_TAG:
2082            statusLabel = STATUS_TRANSFER_SESSION;
2083            break;
2084        default:
2085            return;
2086    }
2087   
2088    [fDefaults setObject: statusLabel forKey: @"StatusLabel"];
2089    [self updateUI];
2090}
2091
2092- (void) showGroups: (id) sender
2093{
2094    [[GroupsWindowController groups] showWindow: self];
2095}
2096
2097- (void) menuNeedsUpdate: (NSMenu *) menu
2098{
2099    if (menu == fGroupsSetMenu || menu == fGroupsSetContextMenu)
2100    {
2101        int i;
2102        for (i = [menu numberOfItems]-1 - 2; i >= 0; i--)
2103            [menu removeItemAtIndex: i];
2104       
2105        NSMenu * groupMenu = [[GroupsWindowController groups] groupMenuWithTarget: self action: @selector(setGroup:) isSmall: NO];
2106        [menu appendItemsFromMenu: groupMenu atIndexes: [NSIndexSet indexSetWithIndexesInRange:
2107                NSMakeRange(0, [groupMenu numberOfItems])] atBottom: NO];
2108    }
2109    else if (menu == fGroupFilterMenu)
2110    {
2111        int i;
2112        for (i = [menu numberOfItems]-1; i >= 3; i--)
2113            [menu removeItemAtIndex: i];
2114       
2115        NSMenu * groupMenu = [[GroupsWindowController groups] groupMenuWithTarget: self action: @selector(setGroupFilter:)
2116                                isSmall: YES];
2117        [menu appendItemsFromMenu: groupMenu atIndexes: [NSIndexSet indexSetWithIndexesInRange:
2118                NSMakeRange(0, [groupMenu numberOfItems])] atBottom: YES];
2119    }
2120    else if (menu == fUploadMenu || menu == fDownloadMenu)
2121    {
2122        if ([menu numberOfItems] > 3)
2123            return;
2124       
2125        const int speedLimitActionValue[] = { 5, 10, 20, 30, 40, 50, 75, 100, 150, 200, 250, 500, 750, -1 };
2126       
2127        NSMenuItem * item;
2128        int i;
2129        for (i = 0; speedLimitActionValue[i] != -1; i++)
2130        {
2131            item = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: NSLocalizedString(@"%d KB/s",
2132                    "Action menu -> upload/download limit"), speedLimitActionValue[i]] action: @selector(setQuickLimitGlobal:)
2133                    keyEquivalent: @""];
2134            [item setTarget: self];
2135            [item setRepresentedObject: [NSNumber numberWithInt: speedLimitActionValue[i]]];
2136            [menu addItem: item];
2137            [item release];
2138        }
2139    }
2140    else if (menu == fRatioStopMenu)
2141    {
2142        if ([menu numberOfItems] > 3)
2143            return;
2144       
2145        const float ratioLimitActionValue[] = { 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, -1 };
2146       
2147        NSMenuItem * item;
2148        int i;
2149        for (i = 0; ratioLimitActionValue[i] != -1; i++)
2150        {
2151            item = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: @"%.2f", ratioLimitActionValue[i]]
2152                    action: @selector(setQuickRatioGlobal:) keyEquivalent: @""];
2153            [item setTarget: self];
2154            [item setRepresentedObject: [NSNumber numberWithFloat: ratioLimitActionValue[i]]];
2155            [menu addItem: item];
2156            [item release];
2157        }
2158    }
2159    else;
2160}
2161
2162- (void) setGroup: (id) sender
2163{
2164    NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
2165    Torrent * torrent;
2166    while ((torrent = [enumerator nextObject]))
2167    {
2168        [fTableView removeCollapsedGroup: [torrent groupValue]]; //remove old collapsed group
2169       
2170        [torrent setGroupValue: [sender tag]];
2171    }
2172   
2173    [self applyFilter: nil];
2174    [self updateUI];
2175    [self updateTorrentHistory];
2176}
2177
2178- (void) setGroupFilter: (id) sender
2179{
2180    [fDefaults setInteger: [sender tag] forKey: @"FilterGroup"];
2181    [self updateGroupsFilterButton];
2182    [self applyFilter: nil];
2183}
2184
2185- (void) updateGroupsFilterButton
2186{
2187    int index = [fDefaults integerForKey: @"FilterGroup"];
2188   
2189    NSImage * icon = nil;
2190    NSString * toolTip;
2191    switch (index)
2192    {
2193        case GROUP_FILTER_ALL_TAG:
2194            icon = [NSImage imageNamed: @"PinTemplate.png"];
2195            toolTip = NSLocalizedString(@"All Groups", "Groups -> Button");
2196            break;
2197        case -1:
2198            if ([NSApp isOnLeopardOrBetter])
2199                icon = [NSImage imageNamed: NSImageNameStopProgressTemplate];
2200            toolTip = NSLocalizedString(@"Group: No Label", "Groups -> Button");
2201            break;
2202        default:
2203            icon = [[GroupsWindowController groups] imageForIndex: index isSmall: YES];
2204            toolTip = [NSLocalizedString(@"Group: ", "Groups -> Button") stringByAppendingString:
2205                        [[GroupsWindowController groups] nameForIndex: index]];
2206    }
2207   
2208    [[fGroupFilterMenu itemAtIndex: 0] setImage: icon];
2209    [fGroupsButton setToolTip: toolTip];
2210}
2211
2212- (void) updateGroupsFilters: (NSNotification *) notification
2213{
2214    [self updateGroupsFilterButton];
2215    [self applyFilter: nil];
2216}
2217
2218- (void) toggleSpeedLimit: (id) sender
2219{
2220    [fDefaults setBool: ![fDefaults boolForKey: @"SpeedLimit"] forKey: @"SpeedLimit"];
2221    [fPrefsController applySpeedSettings: nil];
2222}
2223
2224- (void) autoSpeedLimitChange: (NSNotification *) notification
2225{
2226    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
2227        return;
2228 
2229    NSCalendarDate * onDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2230                        [[fDefaults objectForKey: @"SpeedLimitAutoOnDate"] timeIntervalSinceReferenceDate]],
2231        * offDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2232                        [[fDefaults objectForKey: @"SpeedLimitAutoOffDate"] timeIntervalSinceReferenceDate]],
2233        * nowDate = [NSCalendarDate calendarDate];
2234   
2235    //check if should be on if within range
2236    int onTime = [onDate hourOfDay] * 60 + [onDate minuteOfHour],
2237        offTime = [offDate hourOfDay] * 60 + [offDate minuteOfHour],
2238        nowTime = [nowDate hourOfDay] * 60 + [nowDate minuteOfHour];
2239   
2240    BOOL shouldBeOn = NO;
2241    if (onTime < offTime)
2242        shouldBeOn = onTime <= nowTime && nowTime < offTime;
2243    else if (onTime > offTime)
2244        shouldBeOn = onTime <= nowTime || nowTime < offTime;
2245    else;
2246   
2247    if ([fDefaults boolForKey: @"SpeedLimit"] != shouldBeOn)
2248        [self toggleSpeedLimit: nil];
2249}
2250
2251- (void) autoSpeedLimit
2252{
2253    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
2254        return;
2255   
2256    //only toggle if within first few seconds of minutes
2257    NSCalendarDate * nowDate = [NSCalendarDate calendarDate];
2258    if ([nowDate secondOfMinute] > AUTO_SPEED_LIMIT_SECONDS)
2259        return;
2260   
2261    NSCalendarDate * offDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2262                        [[fDefaults objectForKey: @"SpeedLimitAutoOffDate"] timeIntervalSinceReferenceDate]];
2263   
2264    BOOL toggle;
2265    if ([fDefaults boolForKey: @"SpeedLimit"])
2266        toggle = [nowDate hourOfDay] == [offDate hourOfDay] && [nowDate minuteOfHour] == [offDate minuteOfHour];
2267    else
2268    {
2269        NSCalendarDate * onDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2270                        [[fDefaults objectForKey: @"SpeedLimitAutoOnDate"] timeIntervalSinceReferenceDate]];
2271        toggle = ([nowDate hourOfDay] == [onDate hourOfDay] && [nowDate minuteOfHour] == [onDate minuteOfHour])
2272                    && !([onDate hourOfDay] == [offDate hourOfDay] && [onDate minuteOfHour] == [offDate minuteOfHour]);
2273    }
2274   
2275    if (toggle)
2276    {
2277        [self toggleSpeedLimit: nil];
2278       
2279        [GrowlApplicationBridge notifyWithTitle: [fDefaults boolForKey: @"SpeedLimit"]
2280                ? NSLocalizedString(@"Speed Limit Auto Enabled", "Growl notification title")
2281                : NSLocalizedString(@"Speed Limit Auto Disabled", "Growl notification title")
2282            description: NSLocalizedString(@"Bandwidth settings changed", "Growl notification description")
2283            notificationName: GROWL_AUTO_SPEED_LIMIT iconData: nil priority: 0 isSticky: NO clickContext: nil];
2284    }
2285}
2286
2287- (void) setLimitGlobalEnabled: (id) sender
2288{
2289    BOOL upload = [sender menu] == fUploadMenu;
2290    [fDefaults setBool: sender == (upload ? fUploadLimitItem : fDownloadLimitItem) forKey: upload ? @"CheckUpload" : @"CheckDownload"];
2291   
2292    [fPrefsController applySpeedSettings: nil];
2293}
2294
2295- (void) setQuickLimitGlobal: (id) sender
2296{
2297    BOOL upload = [sender menu] == fUploadMenu;
2298    [fDefaults setInteger: [[sender representedObject] intValue] forKey: upload ? @"UploadLimit" : @"DownloadLimit"];
2299    [fDefaults setBool: YES forKey: upload ? @"CheckUpload" : @"CheckDownload"];
2300   
2301    [fPrefsController updateLimitFields];
2302    [fPrefsController applySpeedSettings: nil];
2303}
2304
2305- (void) setRatioGlobalEnabled: (id) sender
2306{
2307    [fDefaults setBool: sender == fCheckRatioItem forKey: @"RatioCheck"];
2308}
2309
2310- (void) setQuickRatioGlobal: (id) sender
2311{
2312    [fDefaults setBool: YES forKey: @"RatioCheck"];
2313    [fDefaults setFloat: [[sender representedObject] floatValue] forKey: @"RatioLimit"];
2314   
2315    [fPrefsController updateRatioStopField];
2316}
2317
2318- (void) torrentStoppedForRatio: (NSNotification *) notification
2319{
2320    Torrent * torrent = [notification object];
2321   
2322    [self updateTorrentsInQueue];
2323    [fInfoController updateInfoStats];
2324    [fInfoController updateOptions];
2325   
2326    if ([fDefaults boolForKey: @"PlaySeedingSound"])
2327    {
2328        NSSound * sound;
2329        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]]))
2330            [sound play];
2331    }
2332   
2333    NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_SEEDING_COMPLETE, @"Type",
2334                                    [torrent dataLocation], @"Location", nil];
2335    [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Seeding Complete", "Growl notification title")
2336                        description: [torrent name] notificationName: GROWL_SEEDING_COMPLETE
2337                        iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
2338}
2339
2340-(void) watcher: (id<UKFileWatcher>) watcher receivedNotification: (NSString *) notification forPath: (NSString *) path
2341{
2342    if ([notification isEqualToString: UKFileWatcherWriteNotification])
2343    {
2344        if (![fDefaults boolForKey: @"AutoImport"] || ![fDefaults stringForKey: @"AutoImportDirectory"])
2345            return;
2346       
2347        if (fAutoImportTimer)
2348        {
2349            if ([fAutoImportTimer isValid])
2350                [fAutoImportTimer invalidate];
2351            [fAutoImportTimer release];
2352            fAutoImportTimer = nil;
2353        }
2354       
2355        //check again in 10 seconds in case torrent file wasn't complete
2356        fAutoImportTimer = [[NSTimer scheduledTimerWithTimeInterval: 10.0 target: self
2357            selector: @selector(checkAutoImportDirectory) userInfo: nil repeats: NO] retain];
2358       
2359        [self checkAutoImportDirectory];
2360    }
2361}
2362
2363- (void) changeAutoImport
2364{
2365    if (fAutoImportTimer)
2366    {
2367        if ([fAutoImportTimer isValid])
2368            [fAutoImportTimer invalidate];
2369        [fAutoImportTimer release];
2370        fAutoImportTimer = nil;
2371    }
2372   
2373    if (fAutoImportedNames)
2374    {
2375        [fAutoImportedNames release];
2376        fAutoImportedNames = nil;
2377    }
2378   
2379    [self checkAutoImportDirectory];
2380}
2381
2382- (void) checkAutoImportDirectory
2383{
2384    NSString * path;
2385    if (![fDefaults boolForKey: @"AutoImport"] || !(path = [fDefaults stringForKey: @"AutoImportDirectory"]))
2386        return;
2387   
2388    path = [path stringByExpandingTildeInPath];
2389   
2390    NSArray * importedNames;
2391    if (!(importedNames = [[NSFileManager defaultManager] directoryContentsAtPath: path]))
2392        return;
2393   
2394    //only check files that have not been checked yet
2395    NSMutableArray * newNames = [importedNames mutableCopy];
2396   
2397    if (fAutoImportedNames)
2398        [newNames removeObjectsInArray: fAutoImportedNames];
2399    else
2400        fAutoImportedNames = [[NSMutableArray alloc] init];
2401    [fAutoImportedNames setArray: importedNames];
2402   
2403    NSString * file;
2404    int i;
2405    for (i = [newNames count] - 1; i >= 0; i--)
2406    {
2407        file = [newNames objectAtIndex: i];
2408        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
2409            [newNames removeObjectAtIndex: i];
2410        else
2411            [newNames replaceObjectAtIndex: i withObject: [path stringByAppendingPathComponent: file]];
2412    }
2413   
2414    NSEnumerator * enumerator = [newNames objectEnumerator];
2415    tr_ctor * ctor;
2416    while ((file = [enumerator nextObject]))
2417    {
2418        ctor = tr_ctorNew(fLib);
2419        tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
2420       
2421        switch (tr_torrentParse(fLib, ctor, NULL))
2422        {
2423            case TR_OK:
2424                [self openFiles: [NSArray arrayWithObject: file] addType: ADD_NORMAL forcePath: nil];
2425               
2426                [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Torrent File Auto Added", "Growl notification title")
2427                    description: [file lastPathComponent] notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO
2428                    clickContext: nil];
2429                break;
2430           
2431            case TR_EINVALID:
2432                [fAutoImportedNames removeObject: [file lastPathComponent]];
2433        }
2434       
2435        tr_ctorFree(ctor);
2436    }
2437   
2438    [newNames release];
2439}
2440
2441- (void) beginCreateFile: (NSNotification *) notification
2442{
2443    if (![fDefaults boolForKey: @"AutoImport"])
2444        return;
2445   
2446    NSString * location = [notification object],
2447            * path = [fDefaults stringForKey: @"AutoImportDirectory"];
2448   
2449    if (location && path && [[[location stringByDeletingLastPathComponent] stringByExpandingTildeInPath]
2450                                    isEqualToString: [path stringByExpandingTildeInPath]])
2451        [fAutoImportedNames addObject: [location lastPathComponent]];
2452}
2453
2454- (NSInteger) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item
2455{
2456    if (item)
2457        return [[item objectForKey: @"Torrents"] count];
2458    else
2459        return [fDisplayedTorrents count];
2460}
2461
2462- (id) outlineView: (NSOutlineView *) outlineView child: (NSInteger) index ofItem: (id) item
2463{
2464    if (item)
2465        return [[item objectForKey: @"Torrents"] objectAtIndex: index];
2466    else
2467        return [fDisplayedTorrents objectAtIndex: index];
2468}
2469
2470- (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item
2471{
2472    return ![item isKindOfClass: [Torrent class]];
2473}
2474
2475- (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item
2476{
2477    if (![item isKindOfClass: [Torrent class]])
2478    {
2479        int group = [[item objectForKey: @"Group"] intValue];
2480        return group != -1 ? [[GroupsWindowController groups] nameForIndex: group] : NSLocalizedString(@"No Group", "Group table row");
2481    }
2482    else
2483        return [item hashString];
2484}
2485
2486- (BOOL) outlineView: (NSOutlineView *) outlineView writeItems: (NSArray *) items toPasteboard: (NSPasteboard *) pasteboard
2487{
2488    //only allow reordering of rows if sorting by order
2489    if ([[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER])
2490    {
2491        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
2492       
2493        NSMutableIndexSet * indexSet = [NSMutableIndexSet indexSet];
2494        NSEnumerator * enumerator = [items objectEnumerator];
2495        Torrent * torrent;
2496        while ((torrent = [enumerator nextObject]))
2497            [indexSet addIndex: [fTableView rowForItem: torrent]];
2498       
2499        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexSet] forType: TORRENT_TABLE_VIEW_DATA_TYPE];
2500        return YES;
2501    }
2502    return NO;
2503}
2504
2505- (NSDragOperation) outlineView: (NSOutlineView *) outlineView validateDrop: (id < NSDraggingInfo >) info proposedItem: (id) item
2506    proposedChildIndex: (NSInteger) index
2507{
2508    NSPasteboard * pasteboard = [info draggingPasteboard];
2509    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
2510    {
2511        if (item)
2512            index = [fTableView rowForItem: item] + 1;
2513       
2514        [fTableView setDropItem: nil dropChildIndex: index];
2515        return NSDragOperationGeneric;
2516    }
2517   
2518    return NSDragOperationNone;
2519}
2520
2521- (BOOL) outlineView: (NSOutlineView *) outlineView acceptDrop: (id < NSDraggingInfo >) info item: (id) item
2522    childIndex: (NSInteger) newRow
2523{
2524    NSPasteboard * pasteboard = [info draggingPasteboard];
2525    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
2526    {
2527        //remember selected rows
2528        NSArray * selectedValues = [fTableView selectedValues];
2529   
2530        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData:
2531                                [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
2532       
2533        //determine where to move them
2534        int i, originalRow = newRow;
2535        for (i = [indexes firstIndex]; i < originalRow && i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
2536            newRow--;
2537       
2538        //reinsert into array
2539        int insertIndex = newRow > 0 ? [[fDisplayedTorrents objectAtIndex: newRow-1] orderValue] + 1 : 0;
2540       
2541        //get all torrents to reorder
2542        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
2543                                                @"orderValue" ascending: YES] autorelease];
2544       
2545        NSMutableArray * sortedTorrents = [[fTorrents sortedArrayUsingDescriptors:
2546                                            [NSArray arrayWithObject: orderDescriptor]] mutableCopy];
2547       
2548        //remove objects to reinsert
2549        NSArray * movingTorrents = [[fDisplayedTorrents objectsAtIndexes: indexes] retain];
2550        [sortedTorrents removeObjectsInArray: movingTorrents];
2551       
2552        //insert objects at new location
2553        for (i = 0; i < [movingTorrents count]; i++)
2554            [sortedTorrents insertObject: [movingTorrents objectAtIndex: i] atIndex: insertIndex + i];
2555       
2556        [movingTorrents release];
2557       
2558        //redo order values
2559        i = 0;
2560        for (i = 0; i < [sortedTorrents count]; i++)
2561            [[sortedTorrents objectAtIndex: i] setOrderValue: i];
2562       
2563        [sortedTorrents release];
2564       
2565        [self applyFilter: nil];
2566       
2567        //set selected rows
2568        [fTableView selectValues: selectedValues];
2569    }
2570   
2571    return YES;
2572}
2573
2574- (void) torrentTableViewSelectionDidChange: (NSNotification *) notification
2575{
2576    [fInfoController setInfoForTorrents: [fTableView selectedTorrents]];
2577}
2578
2579- (NSDragOperation) draggingEntered: (id <NSDraggingInfo>) info
2580{
2581    NSPasteboard * pasteboard = [info draggingPasteboard];
2582    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
2583    {
2584        //check if any torrent files can be added
2585        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
2586        NSEnumerator * enumerator = [files objectEnumerator];
2587        NSString * file;
2588        BOOL torrent = NO;
2589        tr_ctor * ctor;
2590        while ((file = [enumerator nextObject]))
2591        {
2592            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
2593            {
2594                ctor = tr_ctorNew(fLib);
2595                tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
2596                switch (tr_torrentParse(fLib, ctor, NULL))
2597                {
2598                    case TR_OK:
2599                        if (!fOverlayWindow)
2600                            fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2601                        [fOverlayWindow setTorrents: files];
2602                       
2603                        return NSDragOperationCopy;
2604                   
2605                    case TR_EDUPLICATE:
2606                        torrent = YES;
2607                }
2608                tr_ctorFree(ctor);
2609            }
2610        }
2611       
2612        //create a torrent file if a single file
2613        if (!torrent && [files count] == 1)
2614        {
2615            if (!fOverlayWindow)
2616                fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2617            [fOverlayWindow setFile: [[files objectAtIndex: 0] lastPathComponent]];
2618           
2619            return NSDragOperationCopy;
2620        }
2621    }
2622    else if ([[pasteboard types] containsObject: NSURLPboardType])
2623    {
2624        if (!fOverlayWindow)
2625            fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2626        [fOverlayWindow setURL: [[NSURL URLFromPasteboard: pasteboard] relativeString]];
2627       
2628        return NSDragOperationCopy;
2629    }
2630    else;
2631   
2632    return NSDragOperationNone;
2633}
2634
2635- (void) draggingExited: (id <NSDraggingInfo>) info
2636{
2637    if (fOverlayWindow)
2638        [fOverlayWindow fadeOut];
2639}
2640
2641- (BOOL) performDragOperation: (id <NSDraggingInfo>) info
2642{
2643    if (fOverlayWindow)
2644        [fOverlayWindow fadeOut];
2645   
2646    NSPasteboard * pasteboard = [info draggingPasteboard];
2647    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
2648    {
2649        BOOL torrent = NO, accept = YES;
2650       
2651        //create an array of files that can be opened
2652        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
2653        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
2654        NSEnumerator * enumerator = [files objectEnumerator];
2655        NSString * file;
2656        tr_ctor * ctor;
2657        while ((file = [enumerator nextObject]))
2658        {
2659            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
2660            {
2661                ctor = tr_ctorNew(fLib);
2662                tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]);
2663                switch (tr_torrentParse(fLib, ctor, NULL))
2664                {
2665                    case TR_OK:
2666                        [filesToOpen addObject: file];
2667                        torrent = YES;
2668                        break;
2669                       
2670                    case TR_EDUPLICATE:
2671                        torrent = YES;
2672                }
2673                tr_ctorFree(ctor);
2674            }
2675        }
2676       
2677        if ([filesToOpen count] > 0)
2678            [self application: NSApp openFiles: filesToOpen];
2679        else
2680        {
2681            if (!torrent && [files count] == 1)
2682                [CreatorWindowController createTorrentFile: fLib forFile: [files objectAtIndex: 0]];
2683            else
2684                accept = NO;
2685        }
2686        [filesToOpen release];
2687       
2688        return accept;
2689    }
2690    else if ([[pasteboard types] containsObject: NSURLPboardType])
2691    {
2692        NSURL * url;
2693        if ((url = [NSURL URLFromPasteboard: pasteboard]))
2694        {
2695            [self openURL: url];
2696            return YES;
2697        }
2698    }
2699    else;
2700   
2701    return NO;
2702}
2703
2704- (void) toggleSmallView: (id) sender
2705{
2706    BOOL makeSmall = ![fDefaults boolForKey: @"SmallView"];
2707    [fDefaults setBool: makeSmall forKey: @"SmallView"];
2708   
2709    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
2710   
2711    [fTableView noteHeightOfRowsWithIndexesChanged: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTableView numberOfRows])]];
2712   
2713    //window min height
2714    NSSize contentMinSize = [fWindow contentMinSize],
2715            contentSize = [[fWindow contentView] frame].size;
2716    contentMinSize.height = contentSize.height - [[fTableView enclosingScrollView] frame].size.height
2717                            + [fTableView rowHeight] + [fTableView intercellSpacing].height;
2718    [fWindow setContentMinSize: contentMinSize];
2719   
2720    //resize for larger min height if not set to auto size
2721    if (![fDefaults boolForKey: @"AutoSize"])
2722    {
2723        if (!makeSmall && contentSize.height < contentMinSize.height)
2724        {
2725            NSRect frame = [fWindow frame];
2726            float heightChange = contentMinSize.height - contentSize.height;
2727            frame.size.height += heightChange;
2728            frame.origin.y -= heightChange;
2729           
2730            [fWindow setFrame: frame display: YES];
2731        }
2732    }
2733    else
2734        [self setWindowSizeToFit];
2735}
2736
2737- (void) togglePiecesBar: (id) sender
2738{
2739    [fDefaults setBool: ![fDefaults boolForKey: @"PiecesBar"] forKey: @"PiecesBar"];
2740    [fTableView togglePiecesBar];
2741}
2742
2743- (void) toggleAvailabilityBar: (id) sender
2744{
2745    [fDefaults setBool: ![fDefaults boolForKey: @"DisplayProgressBarAvailable"] forKey: @"DisplayProgressBarAvailable"];
2746    [fTableView display];
2747}
2748
2749- (void) toggleStatusBar: (id) sender
2750{
2751    [self showStatusBar: [fStatusBar isHidden] animate: YES];
2752    [fDefaults setBool: ![fStatusBar isHidden] forKey: @"StatusBar"];
2753}
2754
2755- (NSRect) windowFrameByAddingHeight: (float) height checkLimits: (BOOL) check
2756{
2757    NSScrollView * scrollView = [fTableView enclosingScrollView];
2758   
2759    //convert pixels to points
2760    NSRect windowFrame = [fWindow frame];
2761    NSSize windowSize = [scrollView convertSize: windowFrame.size fromView: nil];
2762    windowSize.height += height;
2763   
2764    if (check)
2765    {
2766        NSSize minSize = [scrollView convertSize: [fWindow minSize] fromView: nil];
2767       
2768        if (windowSize.height < minSize.height)
2769            windowSize.height = minSize.height;
2770        else
2771        {
2772            NSSize maxSize = [scrollView convertSize: [[fWindow screen] visibleFrame].size fromView: nil];
2773            if ([fStatusBar isHidden])
2774                maxSize.height -= [fStatusBar frame].size.height;
2775            if ([fFilterBar isHidden])
2776                maxSize.height -= [fFilterBar frame].size.height;
2777            if (windowSize.height > maxSize.height)
2778                windowSize.height = maxSize.height;
2779        }
2780    }
2781
2782    //convert points to pixels
2783    windowSize = [scrollView convertSize: windowSize toView: nil];
2784
2785    windowFrame.origin.y -= (windowSize.height - windowFrame.size.height);
2786    windowFrame.size.height = windowSize.height;
2787    return windowFrame;
2788}
2789
2790- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
2791{
2792    if (show != [fStatusBar isHidden])
2793        return;
2794
2795    if (show)
2796        [fStatusBar setHidden: NO];
2797
2798    NSRect frame;
2799    float heightChange = [fStatusBar frame].size.height;
2800    if (!show)
2801        heightChange *= -1;
2802   
2803    //allow bar to show even if not enough room
2804    if (show && ![fDefaults boolForKey: @"AutoSize"])
2805    {
2806        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2807        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2808        if (change < 0.0)
2809        {
2810            frame = [fWindow frame];
2811            frame.size.height += change;
2812            frame.origin.y -= change;
2813            [fWindow setFrame: frame display: NO animate: NO];
2814        }
2815    }
2816
2817    [self updateUI];
2818   
2819    NSScrollView * scrollView = [fTableView enclosingScrollView];
2820   
2821    //set views to not autoresize
2822    unsigned int statsMask = [fStatusBar autoresizingMask];
2823    unsigned int filterMask = [fFilterBar autoresizingMask];
2824    unsigned int scrollMask = [scrollView autoresizingMask];
2825    [fStatusBar setAutoresizingMask: NSViewNotSizable];
2826    [fFilterBar setAutoresizingMask: NSViewNotSizable];
2827    [scrollView setAutoresizingMask: NSViewNotSizable];
2828   
2829    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2830    [fWindow setFrame: frame display: YES animate: animate];
2831   
2832    //re-enable autoresize
2833    [fStatusBar setAutoresizingMask: statsMask];
2834    [fFilterBar setAutoresizingMask: filterMask];
2835    [scrollView setAutoresizingMask: scrollMask];
2836   
2837    //change min size
2838    NSSize minSize = [fWindow contentMinSize];
2839    minSize.height += heightChange;
2840    [fWindow setContentMinSize: minSize];
2841   
2842    if (!show)
2843        [fStatusBar setHidden: YES];
2844}
2845
2846- (void) toggleFilterBar: (id) sender
2847{
2848    //disable filtering when hiding
2849    if (![fFilterBar isHidden])
2850    {
2851        [fSearchFilterField setStringValue: @""];
2852        [self setFilter: fNoFilterButton];
2853        [self setGroupFilter: [fGroupFilterMenu itemWithTag: GROUP_FILTER_ALL_TAG]];
2854    }
2855
2856    [self showFilterBar: [fFilterBar isHidden] animate: YES];
2857    [fDefaults setBool: ![fFilterBar isHidden] forKey: @"FilterBar"];
2858}
2859
2860- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
2861{
2862    if (show != [fFilterBar isHidden])
2863        return;
2864
2865    if (show)
2866        [fFilterBar setHidden: NO];
2867
2868    NSRect frame;
2869    float heightChange = [fFilterBar frame].size.height;
2870    if (!show)
2871        heightChange *= -1;
2872   
2873    //allow bar to show even if not enough room
2874    if (show && ![fDefaults boolForKey: @"AutoSize"])
2875    {
2876        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2877        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2878        if (change < 0.0)
2879        {
2880            frame = [fWindow frame];
2881            frame.size.height += change;
2882            frame.origin.y -= change;
2883            [fWindow setFrame: frame display: NO animate: NO];
2884        }
2885    }
2886   
2887    NSScrollView * scrollView = [fTableView enclosingScrollView];
2888
2889    //set views to not autoresize
2890    unsigned int filterMask = [fFilterBar autoresizingMask];
2891    unsigned int scrollMask = [scrollView autoresizingMask];
2892    [fFilterBar setAutoresizingMask: NSViewNotSizable];
2893    [scrollView setAutoresizingMask: NSViewNotSizable];
2894   
2895    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2896    [fWindow setFrame: frame display: YES animate: animate];
2897   
2898    //re-enable autoresize
2899    [fFilterBar setAutoresizingMask: filterMask];
2900    [scrollView setAutoresizingMask: scrollMask];
2901   
2902    //change min size
2903    NSSize minSize = [fWindow contentMinSize];
2904    minSize.height += heightChange;
2905    [fWindow setContentMinSize: minSize];
2906   
2907    if (!show)
2908    {
2909        [fFilterBar setHidden: YES];
2910        [fWindow makeFirstResponder: fTableView];
2911    }
2912}
2913
2914- (ButtonToolbarItem *) standardToolbarButtonWithIdentifier: (NSString *) ident
2915{
2916    ButtonToolbarItem * item = [[ButtonToolbarItem alloc] initWithItemIdentifier: ident];
2917   
2918    NSButton * button = [[NSButton alloc] initWithFrame: NSZeroRect];
2919    [button setBezelStyle: NSTexturedRoundedBezelStyle];
2920    [button setStringValue: @""];
2921   
2922    [item setView: button];
2923    [button release];
2924   
2925    NSSize buttonSize = NSMakeSize(36.0, 25.0);
2926    [item setMinSize: buttonSize];
2927    [item setMaxSize: buttonSize];
2928   
2929    return [item autorelease];
2930}
2931
2932- (NSToolbarItem *) toolbar: (NSToolbar *) toolbar itemForItemIdentifier: (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
2933{
2934    if ([ident isEqualToString: TOOLBAR_CREATE])
2935    {
2936        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
2937       
2938        [item setLabel: NSLocalizedString(@"Create", "Create toolbar item -> label")];
2939        [item setPaletteLabel: NSLocalizedString(@"Create Torrent File", "Create toolbar item -> palette label")];
2940        [item setToolTip: NSLocalizedString(@"Create torrent file", "Create toolbar item -> tooltip")];
2941        [item setImage: [NSImage imageNamed: @"Create.png"]];
2942        [item setTarget: self];
2943        [item setAction: @selector(createFile:)];
2944        [item setAutovalidates: NO];
2945       
2946        return item;
2947    }
2948    else if ([ident isEqualToString: TOOLBAR_OPEN_FILE])
2949    {
2950        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
2951       
2952        [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")];
2953        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")];
2954        [item setToolTip: NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip")];
2955        [item setImage: [NSImage imageNamed: @"Open.png"]];
2956        [item setTarget: self];
2957        [item setAction: @selector(openShowSheet:)];
2958        [item setAutovalidates: NO];
2959       
2960        return item;
2961    }
2962    else if ([ident isEqualToString: TOOLBAR_OPEN_WEB])
2963    {
2964        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
2965       
2966        [item setLabel: NSLocalizedString(@"Open Address", "Open address toolbar item -> label")];
2967        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Address", "Open address toolbar item -> palette label")];
2968        [item setToolTip: NSLocalizedString(@"Open torrent web address", "Open address toolbar item -> tooltip")];
2969        [item setImage: [NSImage imageNamed: @"OpenWeb.png"]];
2970        [item setTarget: self];
2971        [item setAction: @selector(openURLShowSheet:)];
2972        [item setAutovalidates: NO];
2973       
2974        return item;
2975    }
2976    else if ([ident isEqualToString: TOOLBAR_REMOVE])
2977    {
2978        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
2979       
2980        [item setLabel: NSLocalizedString(@"Remove", "Remove toolbar item -> label")];
2981        [item setPaletteLabel: NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label")];
2982        [item setToolTip: NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip")];
2983        [item setImage: [NSImage imageNamed: @"Remove.png"]];
2984        [item setTarget: self];
2985        [item setAction: @selector(removeNoDelete:)];
2986       
2987        return item;
2988    }
2989    else if ([ident isEqualToString: TOOLBAR_INFO])
2990    {
2991        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
2992       
2993        [item setLabel: NSLocalizedString(@"Inspector", "Inspector toolbar item -> label")];
2994        [item setPaletteLabel: NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label")];
2995        [item setToolTip: NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip")];
2996        [item setImage: [NSImage imageNamed: @"Info.png"]];
2997        [item setTarget: self];
2998        [item setAction: @selector(showInfo:)];
2999       
3000        return item;
3001    }
3002    else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_ALL])
3003    {
3004        GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident];
3005       
3006        NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect];
3007        [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]];
3008        [groupItem setView: segmentedControl];
3009        NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell];
3010       
3011        [segmentedControl setSegmentCount: 2];
3012        [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary];
3013       
3014        NSSize groupSize = NSMakeSize(72.0, 25.0);
3015        [groupItem setMinSize: groupSize];
3016        [groupItem setMaxSize: groupSize];
3017       
3018        [groupItem setLabel: NSLocalizedString(@"Apply All", "All toolbar item -> label")];
3019        [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume All", "All toolbar item -> palette label")];
3020        [groupItem setTarget: self];
3021        [groupItem setAction: @selector(allToolbarClicked:)];
3022       
3023        [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, nil]];
3024       
3025        [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG];
3026        [segmentedControl setImage: [NSImage imageNamed: @"PauseAll.png"] forSegment: TOOLBAR_PAUSE_TAG];
3027        [segmentedCell setToolTip: NSLocalizedString(@"Pause all transfers",
3028                                    "All toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG];
3029       
3030        [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG];
3031        [segmentedControl setImage: [NSImage imageNamed: @"ResumeAll.png"] forSegment: TOOLBAR_RESUME_TAG];
3032        [segmentedCell setToolTip: NSLocalizedString(@"Resume all transfers",
3033                                    "All toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG];
3034       
3035        [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause All", "All toolbar item -> label"),
3036                                        NSLocalizedString(@"Resume All", "All toolbar item -> label"), nil]];
3037       
3038        [segmentedControl release];
3039        return [groupItem autorelease];
3040    }
3041    else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_SELECTED])
3042    {
3043        GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident];
3044       
3045        NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect];
3046        [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]];
3047        [groupItem setView: segmentedControl];
3048        NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell];
3049       
3050        [segmentedControl setSegmentCount: 2];
3051        [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary];
3052       
3053        NSSize groupSize = NSMakeSize(72.0, 25.0);
3054        [groupItem setMinSize: groupSize];
3055        [groupItem setMaxSize: groupSize];
3056       
3057        [groupItem setLabel: NSLocalizedString(@"Apply Selected", "Selected toolbar item -> label")];
3058        [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume Selected", "Selected toolbar item -> palette label")];
3059        [groupItem setTarget: self];
3060        [groupItem setAction: @selector(selectedToolbarClicked:)];
3061       
3062        [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED, nil]];
3063       
3064        [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG];
3065        [segmentedControl setImage: [NSImage imageNamed: @"PauseSelected.png"] forSegment: TOOLBAR_PAUSE_TAG];
3066        [segmentedCell setToolTip: NSLocalizedString(@"Pause selected transfers",
3067                                    "Selected toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG];
3068       
3069        [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG];
3070        [segmentedControl setImage: [NSImage imageNamed: @"ResumeSelected.png"] forSegment: TOOLBAR_RESUME_TAG];
3071        [segmentedCell setToolTip: NSLocalizedString(@"Resume selected transfers",
3072                                    "Selected toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG];
3073       
3074        [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause Selected", "Selected toolbar item -> label"),
3075                                        NSLocalizedString(@"Resume Selected", "Selected toolbar item -> label"), nil]];
3076       
3077        [segmentedControl release];
3078        return [groupItem autorelease];
3079    }
3080    else if ([ident isEqualToString: TOOLBAR_FILTER])
3081    {
3082        ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident];
3083       
3084        [item setLabel: NSLocalizedString(@"Filter", "Filter toolbar item -> label")];
3085        [item setPaletteLabel: NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label")];
3086        [item setToolTip: NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip")];
3087        [item setImage: [NSImage imageNamed: @"Filter.png"]];
3088        [item setTarget: self];
3089        [item setAction: @selector(toggleFilterBar:)];
3090       
3091        return item;
3092    }
3093    else
3094        return nil;
3095}
3096
3097- (void) allToolbarClicked: (id) sender
3098{
3099    int tagValue = [sender isKindOfClass: [NSSegmentedControl class]]
3100                    ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [sender tag];
3101    switch (tagValue)
3102    {
3103        case TOOLBAR_PAUSE_TAG:
3104            [self stopAllTorrents: sender];
3105            break;
3106        case TOOLBAR_RESUME_TAG:
3107            [self resumeAllTorrents: sender];
3108            break;
3109    }
3110}
3111
3112- (void) selectedToolbarClicked: (id) sender
3113{
3114    int tagValue = [sender isKindOfClass: [NSSegmentedControl class]]
3115                    ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [sender tag];
3116    switch (tagValue)
3117    {
3118        case TOOLBAR_PAUSE_TAG:
3119            [self stopSelectedTorrents: sender];
3120            break;
3121        case TOOLBAR_RESUME_TAG:
3122            [self resumeSelectedTorrents: sender];
3123            break;
3124    }
3125}
3126
3127- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar
3128{
3129    return [NSArray arrayWithObjects:
3130            TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_OPEN_WEB,
3131            TOOLBAR_REMOVE, TOOLBAR_PAUSE_RESUME_SELECTED, TOOLBAR_PAUSE_RESUME_ALL,
3132            TOOLBAR_FILTER, TOOLBAR_INFO,
3133            NSToolbarSeparatorItemIdentifier,
3134            NSToolbarSpaceItemIdentifier,
3135            NSToolbarFlexibleSpaceItemIdentifier,
3136            NSToolbarCustomizeToolbarItemIdentifier, nil];
3137}
3138
3139- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar
3140{
3141    return [NSArray arrayWithObjects:
3142            TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_REMOVE,
3143            NSToolbarSeparatorItemIdentifier,
3144            TOOLBAR_PAUSE_RESUME_ALL,
3145            NSToolbarFlexibleSpaceItemIdentifier,
3146            TOOLBAR_FILTER, TOOLBAR_INFO, nil];
3147}
3148
3149- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
3150{
3151    NSString * ident = [toolbarItem itemIdentifier];
3152   
3153    //enable remove item
3154    if ([ident isEqualToString: TOOLBAR_REMOVE])
3155        return [fTableView numberOfSelectedRows] > 0;
3156
3157    //enable pause all item
3158    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
3159    {
3160        Torrent * torrent;
3161        NSEnumerator * enumerator = [fTorrents objectEnumerator];
3162        while ((torrent = [enumerator nextObject]))
3163            if ([torrent isActive] || [torrent waitingToStart])
3164                return YES;
3165        return NO;
3166    }
3167
3168    //enable resume all item
3169    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
3170    {
3171        Torrent * torrent;
3172        NSEnumerator * enumerator = [fTorrents objectEnumerator];
3173        while ((torrent = [enumerator nextObject]))
3174            if (![torrent isActive] && ![torrent waitingToStart])
3175                return YES;
3176        return NO;
3177    }
3178
3179    //enable pause item
3180    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
3181    {
3182        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3183        Torrent * torrent;
3184        while ((torrent = [enumerator nextObject]))
3185            if ([torrent isActive] || [torrent waitingToStart])
3186                return YES;
3187        return NO;
3188    }
3189   
3190    //enable resume item
3191    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
3192    {
3193        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3194        Torrent * torrent;
3195        while ((torrent = [enumerator nextObject]))
3196            if (![torrent isActive] && ![torrent waitingToStart])
3197                return YES;
3198        return NO;
3199    }
3200   
3201    //set info image
3202    if ([ident isEqualToString: TOOLBAR_INFO])
3203    {
3204        [toolbarItem setImage: [[fInfoController window] isVisible] ? [NSImage imageNamed: @"InfoBlue.png"]
3205                                                                    : [NSImage imageNamed: @"Info.png"]];
3206        return YES;
3207    }
3208   
3209    //set filter image
3210    if ([ident isEqualToString: TOOLBAR_FILTER])
3211    {
3212        [toolbarItem setImage: ![fFilterBar isHidden] ? [NSImage imageNamed: @"FilterBlue.png"] : [NSImage imageNamed: @"Filter.png"]];
3213        return YES;
3214    }
3215
3216    return YES;
3217}
3218
3219- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
3220{
3221    SEL action = [menuItem action];
3222   
3223    if (action == @selector(toggleSpeedLimit:))
3224    {
3225        [menuItem setState: [fDefaults boolForKey: @"SpeedLimit"] ? NSOnState : NSOffState];
3226        return YES;
3227    }
3228   
3229    //only enable some items if it is in a context menu or the window is useable
3230    BOOL canUseTable = [fWindow isKeyWindow] || [[menuItem menu] supermenu] != [NSApp mainMenu];
3231
3232    //enable open items
3233    if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:))
3234        return [fWindow attachedSheet] == nil;
3235   
3236    //enable sort options
3237    if (action == @selector(setSort:))
3238    {
3239        NSString * sortType;
3240        switch ([menuItem tag])
3241        {
3242            case SORT_ORDER_TAG:
3243                sortType = SORT_ORDER;
3244                break;
3245            case SORT_DATE_TAG:
3246                sortType = SORT_DATE;
3247                break;
3248            case SORT_NAME_TAG:
3249                sortType = SORT_NAME;
3250                break;
3251            case SORT_PROGRESS_TAG:
3252                sortType = SORT_PROGRESS;
3253                break;
3254            case SORT_STATE_TAG:
3255                sortType = SORT_STATE;
3256                break;
3257            case SORT_TRACKER_TAG:
3258                sortType = SORT_TRACKER;
3259                break;
3260            case SORT_ACTIVITY_TAG:
3261                sortType = SORT_ACTIVITY;
3262                break;
3263            default:
3264                sortType = @"";
3265        }
3266       
3267        [menuItem setState: [sortType isEqualToString: [fDefaults stringForKey: @"Sort"]] ? NSOnState : NSOffState];
3268        return [fWindow isVisible];
3269    }
3270   
3271    //enable sort options
3272    if (action == @selector(setStatusLabel:))
3273    {
3274        NSString * statusLabel;
3275        switch ([menuItem tag])
3276        {
3277            case STATUS_RATIO_TOTAL_TAG:
3278                statusLabel = STATUS_RATIO_TOTAL;
3279                break;
3280            case STATUS_RATIO_SESSION_TAG:
3281                statusLabel = STATUS_RATIO_SESSION;
3282                break;
3283            case STATUS_TRANSFER_TOTAL_TAG:
3284                statusLabel = STATUS_TRANSFER_TOTAL;
3285                break;
3286            case STATUS_TRANSFER_SESSION_TAG:
3287                statusLabel = STATUS_TRANSFER_SESSION;
3288                break;
3289            default:
3290                statusLabel = @"";;
3291        }
3292       
3293        [menuItem setState: [statusLabel isEqualToString: [fDefaults stringForKey: @"StatusLabel"]] ? NSOnState : NSOffState];
3294        return YES;
3295    }
3296   
3297    if (action == @selector(setGroup:))
3298    {
3299        BOOL checked = NO;
3300       
3301        int index = [menuItem tag];
3302        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3303        Torrent * torrent;
3304        while ((torrent = [enumerator nextObject]))
3305            if (index == [torrent groupValue])
3306            {
3307                checked = YES;
3308                break;
3309            }
3310       
3311        [menuItem setState: checked ? NSOnState : NSOffState];
3312        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3313    }
3314   
3315    if (action == @selector(setGroupFilter:))
3316    {
3317        [menuItem setState: [menuItem tag] == [fDefaults integerForKey: @"FilterGroup"] ? NSOnState : NSOffState];
3318        return YES;
3319    }
3320   
3321    if (action == @selector(toggleSmallView:))
3322    {
3323        [menuItem setState: [fDefaults boolForKey: @"SmallView"] ? NSOnState : NSOffState];
3324        return [fWindow isVisible];
3325    }
3326   
3327    if (action == @selector(togglePiecesBar:))
3328    {
3329        [menuItem setState: [fDefaults boolForKey: @"PiecesBar"] ? NSOnState : NSOffState];
3330        return [fWindow isVisible];
3331    }
3332   
3333    if (action == @selector(toggleAvailabilityBar:))
3334    {
3335        [menuItem setState: [fDefaults boolForKey: @"DisplayProgressBarAvailable"] ? NSOnState : NSOffState];
3336        return [fWindow isVisible];
3337    }
3338   
3339    if (action == @selector(setLimitGlobalEnabled:))
3340    {
3341        BOOL upload = [menuItem menu] == fUploadMenu;
3342        BOOL limit = menuItem == (upload ? fUploadLimitItem : fDownloadLimitItem);
3343        if (limit)
3344            [menuItem setTitle: [NSString stringWithFormat: NSLocalizedString(@"Limit (%d KB/s)",
3345                                    "Action menu -> upload/download limit"),
3346                                    [fDefaults integerForKey: upload ? @"UploadLimit" : @"DownloadLimit"]]];
3347       
3348        [menuItem setState: [fDefaults boolForKey: upload ? @"CheckUpload" : @"CheckDownload"] ? limit : !limit];
3349        return YES;
3350    }
3351   
3352    if (action == @selector(setRatioGlobalEnabled:))
3353    {
3354        BOOL check = menuItem == fCheckRatioItem;
3355        if (check)
3356            [menuItem setTitle: [NSString stringWithFormat: NSLocalizedString(@"Stop at Ratio (%.2f)",
3357                                    "Action menu -> ratio stop"), [fDefaults floatForKey: @"RatioLimit"]]];
3358       
3359        [menuItem setState: [fDefaults boolForKey: @"RatioCheck"] ? check : !check];
3360        return YES;
3361    }
3362
3363    //enable show info
3364    if (action == @selector(showInfo:))
3365    {
3366        NSString * title = [[fInfoController window] isVisible] ? NSLocalizedString(@"Hide Inspector", "View menu -> Inspector")
3367                            : NSLocalizedString(@"Show Inspector", "View menu -> Inspector");
3368        [menuItem setTitle: title];
3369
3370        return YES;
3371    }
3372   
3373    //enable prev/next inspector tab
3374    if (action == @selector(setInfoTab:))
3375        return [[fInfoController window] isVisible];
3376   
3377    //enable toggle status bar
3378    if (action == @selector(toggleStatusBar:))
3379    {
3380        NSString * title = [fStatusBar isHidden] ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar")
3381                            : NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar");
3382        [menuItem setTitle: title];
3383
3384        return [fWindow isVisible];
3385    }
3386   
3387    //enable toggle filter bar
3388    if (action == @selector(toggleFilterBar:))
3389    {
3390        NSString * title = [fFilterBar isHidden] ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar")
3391                            : NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar");
3392        [menuItem setTitle: title];
3393
3394        return [fWindow isVisible];
3395    }
3396   
3397    //enable prev/next filter button
3398    if (action == @selector(switchFilter:))
3399        return [fWindow isVisible] && ![fFilterBar isHidden];
3400
3401    //enable reveal in finder
3402    if (action == @selector(revealFile:))
3403        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3404
3405    //enable remove items
3406    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
3407        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteDataAndTorrent:))
3408    {
3409        BOOL warning = NO,
3410            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
3411            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteDataAndTorrent:);
3412       
3413        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3414        Torrent * torrent;
3415        while ((torrent = [enumerator nextObject]))
3416        {
3417            if (!warning && [torrent isActive])
3418            {
3419                warning = onlyDownloading ? ![torrent isSeeding] : YES;
3420                if (warning && canDelete)
3421                    break;
3422            }
3423            if (!canDelete && [torrent publicTorrent])
3424            {
3425                canDelete = YES;
3426                if (warning)
3427                    break;
3428            }
3429        }
3430   
3431        //append or remove ellipsis when needed
3432        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
3433        if (warning && [fDefaults boolForKey: @"CheckRemove"])
3434        {
3435            if (![title hasSuffix: ellipsis])
3436                [menuItem setTitle: [title stringByAppendingEllipsis]];
3437        }
3438        else
3439        {
3440            if ([title hasSuffix: ellipsis])
3441                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
3442        }
3443       
3444        return canUseTable && canDelete && [fTableView numberOfSelectedRows] > 0;
3445    }
3446
3447    //enable pause all item
3448    if (action == @selector(stopAllTorrents:))
3449    {
3450        Torrent * torrent;
3451        NSEnumerator * enumerator = [fTorrents objectEnumerator];
3452        while ((torrent = [enumerator nextObject]))
3453            if ([torrent isActive] || [torrent waitingToStart])
3454                return YES;
3455        return NO;
3456    }
3457   
3458    //enable resume all item
3459    if (action == @selector(resumeAllTorrents:))
3460    {
3461        Torrent * torrent;
3462        NSEnumerator * enumerator = [fTorrents objectEnumerator];
3463        while ((torrent = [enumerator nextObject]))
3464            if (![torrent isActive] && ![torrent waitingToStart])
3465                return YES;
3466        return NO;
3467    }
3468   
3469    #warning hide queue options if all queues are disabled?
3470   
3471    //enable resume all waiting item
3472    if (action == @selector(resumeWaitingTorrents:))
3473    {
3474        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
3475            return NO;
3476   
3477        Torrent * torrent;
3478        NSEnumerator * enumerator = [fTorrents objectEnumerator];
3479        while ((torrent = [enumerator nextObject]))
3480            if (![torrent isActive] && [torrent waitingToStart])
3481                return YES;
3482        return NO;
3483    }
3484   
3485    //enable resume selected waiting item
3486    if (action == @selector(resumeSelectedTorrentsNoWait:))
3487    {
3488        if (!canUseTable)
3489            return NO;
3490       
3491        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3492        Torrent * torrent;
3493        while ((torrent = [enumerator nextObject]))
3494            if (![torrent isActive])
3495                return YES;
3496        return NO;
3497    }
3498
3499    //enable pause item
3500    if (action == @selector(stopSelectedTorrents:))
3501    {
3502        if (!canUseTable)
3503            return NO;
3504   
3505        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3506        Torrent * torrent;
3507        while ((torrent = [enumerator nextObject]))
3508            if ([torrent isActive] || [torrent waitingToStart])
3509                return YES;
3510        return NO;
3511    }
3512   
3513    //enable resume item
3514    if (action == @selector(resumeSelectedTorrents:))
3515    {
3516        if (!canUseTable)
3517            return NO;
3518   
3519        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3520        Torrent * torrent;
3521        while ((torrent = [enumerator nextObject]))
3522            if (![torrent isActive] && ![torrent waitingToStart])
3523                return YES;
3524        return NO;
3525    }
3526   
3527    //enable manual announce item
3528    if (action == @selector(announceSelectedTorrents:))
3529    {
3530        if (!canUseTable)
3531            return NO;
3532       
3533        NSEnumerator * enumerator = [[fTableView selectedTorrents] objectEnumerator];
3534        Torrent * torrent;
3535        while ((torrent = [enumerator nextObject]))
3536            if ([torrent canManualAnnounce])
3537                return YES;
3538        return NO;
3539    }
3540   
3541    //enable reset cache item
3542    if (action == @selector(resetCacheForSelectedTorrents:))
3543        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3544   
3545    //enable move torrent file item
3546    if (action == @selector(moveDataFiles:))
3547        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3548   
3549    //enable copy torrent file item
3550    if (action == @selector(copyTorrentFiles:))
3551        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3552   
3553    //enable reverse sort item
3554    if (action == @selector(setSortReverse:))
3555    {
3556        [menuItem setState: [fDefaults boolForKey: @"SortReverse"] ? NSOnState : NSOffState];
3557        return ![[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER];
3558    }
3559   
3560    //enable group sort item
3561    if (action == @selector(setSortByGroup:))
3562    {
3563        [menuItem setState: [fDefaults boolForKey: @"SortByGroup"] ? NSOnState : NSOffState];
3564        return ![[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER];
3565    }
3566   
3567    //check proper filter search item
3568    if (action == @selector(setFilterSearchType:))
3569    {
3570        NSString * filterType = [fDefaults stringForKey: @"FilterSearchType"];
3571       
3572        BOOL state;
3573        if ([menuItem tag] == FILTER_TYPE_TAG_TRACKER)
3574            state = [filterType isEqualToString: FILTER_TYPE_TRACKER];
3575        else
3576            state = [filterType isEqualToString: FILTER_TYPE_NAME];
3577       
3578        [menuItem setState: state ? NSOnState : NSOffState];
3579        return YES;
3580    }
3581   
3582    return YES;
3583}
3584
3585- (void) sleepCallBack: (natural_t) messageType argument: (void *) messageArgument
3586{
3587    NSEnumerator * enumerator;
3588    Torrent * torrent;
3589    BOOL allowSleep;
3590
3591    switch (messageType)
3592    {
3593        case kIOMessageSystemWillSleep:
3594            //close all connections before going to sleep and remember we should resume when we wake up
3595            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
3596
3597            //wait for running transfers to stop (5 second timeout)
3598            NSDate * start = [NSDate date];
3599            BOOL timeUp = NO;
3600           
3601            enumerator = [fTorrents objectEnumerator];
3602            while (!timeUp && (torrent = [enumerator nextObject]))
3603                while ([torrent isActive] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
3604                {
3605                    usleep(100000);
3606                    [torrent update];
3607                }
3608
3609            IOAllowPowerChange(fRootPort, (long) messageArgument);
3610            break;
3611
3612        case kIOMessageCanSystemSleep:
3613            allowSleep = YES;
3614            if ([fDefaults boolForKey: @"SleepPrevent"])
3615            {
3616                //prevent idle sleep unless no torrents are active
3617                enumerator = [fTorrents objectEnumerator];
3618                while ((torrent = [enumerator nextObject]))
3619                    if ([torrent isActive] && ![torrent isStalled] && ![torrent isError])
3620                    {
3621                        allowSleep = NO;
3622                        break;
3623                    }
3624            }
3625
3626            if (allowSleep)
3627                IOAllowPowerChange(fRootPort, (long) messageArgument);
3628            else
3629                IOCancelPowerChange(fRootPort, (long) messageArgument);
3630            break;
3631
3632        case kIOMessageSystemHasPoweredOn:
3633            //resume sleeping transfers after we wake up
3634            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
3635            [self autoSpeedLimitChange: nil];
3636            break;
3637    }
3638}
3639
3640- (NSMenu *) applicationDockMenu: (NSApplication *) sender
3641{
3642    int seeding = 0, downloading = 0;
3643    NSEnumerator * enumerator = [fTorrents objectEnumerator];
3644    Torrent * torrent;
3645    while ((torrent = [enumerator nextObject]))
3646    {
3647        if ([torrent isSeeding])
3648            seeding++;
3649        else if ([torrent isActive])
3650            downloading++;
3651        else;
3652    }
3653   
3654    NSMenuItem * seedingItem = [fDockMenu itemWithTag: DOCK_SEEDING_TAG],
3655            * downloadingItem = [fDockMenu itemWithTag: DOCK_DOWNLOADING_TAG];
3656   
3657    BOOL hasSeparator = seedingItem || downloadingItem;
3658   
3659    if (seeding > 0)
3660    {
3661        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Seeding",
3662                                                        "Dock item - Seeding"), seeding];
3663        if (!seedingItem)
3664        {
3665            seedingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
3666            [seedingItem setTag: DOCK_SEEDING_TAG];
3667            [fDockMenu insertItem: seedingItem atIndex: 0];
3668        }
3669        else
3670            [seedingItem setTitle: title];
3671    }
3672    else
3673    {
3674        if (seedingItem)
3675            [fDockMenu removeItem: seedingItem];
3676    }
3677   
3678    if (downloading > 0)
3679    {
3680        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Downloading",
3681                                                        "Dock item - Downloading"), downloading];
3682        if (!downloadingItem)
3683        {
3684            downloadingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
3685            [downloadingItem setTag: DOCK_DOWNLOADING_TAG];
3686            [fDockMenu insertItem: downloadingItem atIndex: seeding > 0 ? 1 : 0];
3687        }
3688        else
3689            [downloadingItem setTitle: title];
3690    }
3691    else
3692    {
3693        if (downloadingItem)
3694            [fDockMenu removeItem: downloadingItem];
3695    }
3696   
3697    if (seeding > 0 || downloading > 0)
3698    {
3699        if (!hasSeparator)
3700            [fDockMenu insertItem: [NSMenuItem separatorItem] atIndex: seeding > 0 && downloading > 0 ? 2 : 1];
3701    }
3702    else
3703    {
3704        if (hasSeparator)
3705            [fDockMenu removeItemAtIndex: 0];
3706    }
3707   
3708    return fDockMenu;
3709}
3710
3711- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
3712{
3713    //if auto size is enabled, the current frame shouldn't need to change
3714    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
3715   
3716    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
3717    return frame;
3718}
3719
3720- (void) setWindowSizeToFit
3721{
3722    if ([fDefaults boolForKey: @"AutoSize"])
3723    {
3724        NSScrollView * scrollView = [fTableView enclosingScrollView];
3725       
3726        [scrollView setHasVerticalScroller: NO];
3727        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
3728        [scrollView setHasVerticalScroller: YES];
3729    }
3730}
3731
3732- (NSRect) sizedWindowFrame
3733{
3734    int groups = ([fDisplayedTorrents count] > 0 && ![[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [Torrent class]])
3735                    ? [fDisplayedTorrents count] : 0;
3736   
3737    float heightChange = (GROUP_SEPARATOR_HEIGHT + [fTableView intercellSpacing].height) * groups
3738                        + ([fTableView rowHeight] + [fTableView intercellSpacing].height) * ([fTableView numberOfRows] - groups)
3739                        - [[fTableView enclosingScrollView] frame].size.height;
3740   
3741    return [self windowFrameByAddingHeight: heightChange checkLimits: YES];
3742}
3743
3744- (void) updateForExpandCollape
3745{
3746    [self setWindowSizeToFit];
3747    [self setBottomCountTextFiltering: YES];
3748}
3749
3750- (void) showMainWindow: (id) sender
3751{
3752    [fWindow makeKeyAndOrderFront: nil];
3753}
3754
3755- (void) windowDidBecomeMain: (NSNotification *) notification
3756{
3757    [fBadger clearCompleted];
3758    [self updateUI];
3759}
3760
3761- (NSSize) windowWillResize: (NSWindow *) sender toSize: (NSSize) proposedFrameSize
3762{
3763    //only resize horizontally if autosize is enabled
3764    if ([fDefaults boolForKey: @"AutoSize"])
3765        proposedFrameSize.height = [fWindow frame].size.height;
3766    return proposedFrameSize;
3767}
3768
3769- (void) windowDidResize: (NSNotification *) notification
3770{
3771    if ([fFilterBar isHidden])
3772        return;
3773   
3774    //replace all buttons
3775    [fActiveFilterButton sizeToFit];
3776    [fDownloadFilterButton sizeToFit];
3777    [fSeedFilterButton sizeToFit];
3778    [fPauseFilterButton sizeToFit];
3779   
3780    NSRect activeRect = [fActiveFilterButton frame];
3781   
3782    NSRect downloadRect = [fDownloadFilterButton frame];
3783    downloadRect.origin.x = NSMaxX(activeRect) + 1.0;
3784   
3785    NSRect seedRect = [fSeedFilterButton frame];
3786    seedRect.origin.x = NSMaxX(downloadRect) + 1.0;
3787   
3788    NSRect pauseRect = [fPauseFilterButton frame];
3789    pauseRect.origin.x = NSMaxX(seedRect) + 1.0;
3790   
3791    //size search filter to not overlap buttons
3792    NSRect searchFrame = [fSearchFilterField frame];
3793    searchFrame.origin.x = NSMaxX(pauseRect) + 5.0;
3794    searchFrame.size.width = [fStatusBar frame].size.width - searchFrame.origin.x - 5.0;
3795   
3796    //make sure it is not too long
3797    if (searchFrame.size.width > SEARCH_FILTER_MAX_WIDTH)
3798    {
3799        searchFrame.origin.x += searchFrame.size.width - SEARCH_FILTER_MAX_WIDTH;
3800        searchFrame.size.width = SEARCH_FILTER_MAX_WIDTH;
3801    }
3802    else if (searchFrame.size.width < SEARCH_FILTER_MIN_WIDTH)
3803    {
3804        searchFrame.origin.x += searchFrame.size.width - SEARCH_FILTER_MIN_WIDTH;
3805        searchFrame.size.width = SEARCH_FILTER_MIN_WIDTH;
3806       
3807        //resize each button until they don't overlap search
3808        int download = 0;
3809        BOOL seeding = NO, paused = NO;
3810        do
3811        {
3812            if (download < 8)
3813            {
3814                download++;
3815                downloadRect.size.width--;
3816               
3817                seedRect.origin.x--;
3818                pauseRect.origin.x--;
3819            }
3820            else if (!seeding)
3821            {
3822                seeding = YES;
3823                seedRect.size.width--;
3824               
3825                pauseRect.origin.x--;
3826            }
3827            else if (!paused)
3828            {
3829                paused = YES;
3830                pauseRect.size.width--;
3831            }
3832            else
3833            {
3834                activeRect.size.width--;
3835               
3836                downloadRect.origin.x--;
3837                seedRect.origin.x--;
3838                pauseRect.origin.x--;
3839               
3840                //reset
3841                download = 0;
3842                seeding = NO;
3843                paused = NO;
3844            }
3845        }
3846        while (NSMaxX(pauseRect) + 5.0 > searchFrame.origin.x);
3847    }
3848    else;
3849   
3850    [fActiveFilterButton setFrame: activeRect];
3851    [fDownloadFilterButton setFrame: downloadRect];
3852    [fSeedFilterButton setFrame: seedRect];
3853    [fPauseFilterButton setFrame: pauseRect];
3854   
3855    [fSearchFilterField setFrame: searchFrame];
3856}
3857
3858- (void) applicationWillUnhide: (NSNotification *) notification
3859{
3860    [self updateUI];
3861}
3862
3863- (void) linkHomepage: (id) sender
3864{
3865    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
3866}
3867
3868- (void) linkForums: (id) sender
3869{
3870    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
3871}
3872
3873- (void) linkDonate: (id) sender
3874{
3875    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: DONATE_URL]];
3876}
3877
3878- (void) prepareForUpdate: (NSNotification *) notification
3879{
3880    fUpdateInProgress = YES;
3881}
3882
3883- (NSDictionary *) registrationDictionaryForGrowl
3884{
3885    NSArray * notifications = [NSArray arrayWithObjects: GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE,
3886                                                            GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT, nil];
3887    return [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL,
3888                                notifications, GROWL_NOTIFICATIONS_DEFAULT, nil];
3889}
3890
3891- (void) growlNotificationWasClicked: (id) clickContext
3892{
3893    if (!clickContext || ![clickContext isKindOfClass: [NSDictionary class]])
3894        return;
3895   
3896    NSString * type = [clickContext objectForKey: @"Type"], * location;
3897    if (([type isEqualToString: GROWL_DOWNLOAD_COMPLETE] || [type isEqualToString: GROWL_SEEDING_COMPLETE])
3898            && (location = [clickContext objectForKey: @"Location"]))
3899        [[NSWorkspace sharedWorkspace] selectFile: location inFileViewerRootedAtPath: nil];
3900}
3901
3902- (void) ipcQuit
3903{
3904    fRemoteQuit = YES;
3905    [NSApp terminate: self];
3906}
3907
3908- (NSArray *) ipcGetTorrentsByID: (NSArray *) idlist
3909{
3910    if (!idlist)
3911        return fTorrents;
3912   
3913    NSMutableArray * torrents = [NSMutableArray array];
3914   
3915    NSEnumerator * torrentEnum = [fTorrents objectEnumerator], * idEnum;
3916    int torId;
3917    Torrent * torrent;
3918    NSNumber * tempId;
3919    while ((torrent = [torrentEnum nextObject]))
3920    {
3921        torId = [torrent torrentID];
3922       
3923        idEnum = [idlist objectEnumerator];
3924        while ((tempId = [idEnum nextObject]))
3925        {
3926            if ([tempId intValue] == torId)
3927            {
3928                [torrents addObject: torrent];
3929                break;
3930            }
3931        }
3932    }
3933
3934    return torrents;
3935}
3936
3937- (NSArray *) ipcGetTorrentsByHash: (NSArray *) hashlist
3938{
3939    if (!hashlist)
3940        return fTorrents;
3941   
3942    NSMutableArray * torrents = [NSMutableArray array];
3943   
3944    NSEnumerator * torrentEnum = [fTorrents objectEnumerator], * hashEnum;
3945    NSString * torHash, * tempHash;
3946    Torrent * torrent;
3947    while ((torrent = [torrentEnum nextObject]))
3948    {
3949        torHash = [torrent hashString];
3950       
3951        hashEnum = [hashlist objectEnumerator];
3952        while ((tempHash = [hashEnum nextObject]))
3953        {
3954            if ([torHash caseInsensitiveCompare: tempHash] == NSOrderedSame)
3955            {
3956                [torrents addObject: torrent];
3957                break;
3958            }
3959        }
3960    }
3961   
3962    return torrents;
3963}
3964
3965- (BOOL) ipcAddTorrents: (NSArray *) torrents
3966{
3967    int oldCount = [fTorrents count];
3968   
3969    [self openFiles: torrents addType: ADD_NORMAL forcePath: nil];
3970   
3971    return [fTorrents count] > oldCount;
3972}
3973
3974- (BOOL) ipcAddTorrentFile: (NSString *) path directory: (NSString *) directory
3975{
3976    int oldCount = [fTorrents count];
3977   
3978    [self openFiles: [NSArray arrayWithObject: path] addType: ADD_NORMAL forcePath: directory];
3979   
3980    return [fTorrents count] > oldCount;
3981}
3982
3983- (BOOL) ipcAddTorrentFileAutostart: (NSString *) path directory: (NSString *) directory autostart: (BOOL) autostart
3984{
3985    NSArray * torrents = nil;
3986    if (autostart)
3987        torrents = [fTorrents copy];
3988    BOOL success = [self ipcAddTorrentFile: path directory: directory];
3989   
3990    if (success && autostart)
3991    {
3992        NSEnumerator * enumerator = [torrents reverseObjectEnumerator];
3993        Torrent * torrent;
3994        while ((torrent = [enumerator nextObject]))
3995            if (![torrents containsObject: torrent])
3996                break;
3997       
3998        if (torrent)
3999            [torrent startTransfer];
4000        else
4001            success = NO;
4002    }
4003   
4004    [torrents release];
4005    return success;
4006}
4007
4008- (BOOL) ipcAddTorrentData: (NSData *) data directory: (NSString *) directory
4009{
4010    return [self ipcAddTorrentDataAutostart: data directory: directory autostart: [fDefaults boolForKey: @"AutoStartDownload"]];
4011}
4012
4013- (BOOL) ipcAddTorrentDataAutostart: (NSData *) data directory: (NSString *) directory autostart: (BOOL) autostart
4014{
4015    Torrent * torrent;
4016    if ((torrent = [[Torrent alloc] initWithData: data location: directory lib: fLib]))
4017    {
4018        [torrent update];
4019        [fTorrents addObject: torrent];
4020       
4021        if (autostart)
4022            [torrent startTransfer];
4023       
4024        [torrent release];
4025       
4026        [self updateTorrentsInQueue];
4027        return YES;
4028    }
4029    else
4030        return NO;
4031}
4032
4033- (BOOL) ipcStartTorrents: (NSArray *) torrents
4034{
4035    if (!torrents)
4036        [self resumeAllTorrents: self];
4037    else
4038        [self resumeTorrents: torrents];
4039
4040    return YES;
4041}
4042
4043- (BOOL) ipcStopTorrents: (NSArray *) torrents
4044{
4045    if (!torrents)
4046        [self stopAllTorrents: self];
4047    else
4048        [self stopTorrents: torrents];
4049
4050    return YES;
4051}
4052
4053- (BOOL) ipcRemoveTorrents: (NSArray *) torrents
4054{
4055    if (!torrents)
4056        torrents = [NSArray arrayWithArray: fTorrents];
4057    [torrents retain];
4058
4059    [self confirmRemoveTorrents: torrents deleteData: NO deleteTorrent: NO];
4060
4061    return YES;
4062}
4063
4064@end
Note: See TracBrowser for help on using the repository browser.