source: trunk/macosx/Controller.m @ 4936

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

expand a torrent's group when removing it, since either the whole group is being removed or it is already expanded

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