source: trunk/macosx/Controller.m @ 1765

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

Disable the stop ratio once it is reached (while seeding).

  • Property svn:keywords set to Date Rev Author Id
File size: 104.7 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 1765 2007-04-20 23:07:10Z livings124 $
3 *
4 * Copyright (c) 2005-2007 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 "StringAdditions.h"
32#import "UKKQueue.h"
33#import "ActionMenuSpeedToDisplayLimitTransformer.h"
34#import "ActionMenuRatioToDisplayRatioTransformer.h"
35#import "ExpandedPathToPathTransformer.h"
36#import "ExpandedPathToIconTransformer.h"
37#import "SpeedLimitToTurtleIconTransformer.h"
38
39#import <Sparkle/Sparkle.h>
40
41#define TOOLBAR_OPEN            @"Toolbar Open"
42#define TOOLBAR_REMOVE          @"Toolbar Remove"
43#define TOOLBAR_INFO            @"Toolbar Info"
44#define TOOLBAR_PAUSE_ALL       @"Toolbar Pause All"
45#define TOOLBAR_RESUME_ALL      @"Toolbar Resume All"
46#define TOOLBAR_PAUSE_SELECTED  @"Toolbar Pause Selected"
47#define TOOLBAR_RESUME_SELECTED @"Toolbar Resume Selected"
48#define TOOLBAR_FILTER          @"Toolbar Toggle Filter"
49
50#define GROWL_DOWNLOAD_COMPLETE @"Download Complete"
51#define GROWL_SEEDING_COMPLETE  @"Seeding Complete"
52#define GROWL_AUTO_ADD          @"Torrent Auto Added"
53#define GROWL_AUTO_SPEED_LIMIT  @"Speed Limit Auto Changed"
54
55#define TORRENT_TABLE_VIEW_DATA_TYPE    @"TorrentTableViewDataType"
56
57#define ROW_HEIGHT_REGULAR      65.0
58#define ROW_HEIGHT_SMALL        40.0
59#define WINDOW_REGULAR_WIDTH    468.0
60
61#define UPDATE_UI_SECONDS           1.0
62#define AUTO_SPEED_LIMIT_SECONDS    5.0
63
64#define DOCK_SEEDING_TAG        101
65#define DOCK_DOWNLOADING_TAG    102
66
67#define ANNOUNCE_WAIT_INTERVAL_SECONDS  -60.0
68
69#define WEBSITE_URL @"http://transmission.m0k.org/"
70#define FORUM_URL   @"http://transmission.m0k.org/forum/"
71
72static void sleepCallBack(void * controller, io_service_t y, natural_t messageType, void * messageArgument)
73{
74    Controller * c = controller;
75    [c sleepCallBack: messageType argument: messageArgument];
76}
77
78@implementation Controller
79
80+ (void) initialize
81{
82    //make sure system requirements are met
83    if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_3)
84    {
85        NSAlert * alert = [[NSAlert alloc] init];
86        [alert addButtonWithTitle: NSLocalizedString(@"Quit", "OS update needed -> button")];
87        [alert setMessageText: NSLocalizedString(@"Transmission requires Mac OS X 10.4 or greater.",
88                                                "OS update needed -> title")];
89        [alert setInformativeText: NSLocalizedString(@"Transmission has features that require a more "
90            "up-to-date operating system. The operating system must be updated before this version can run.",
91            "OS update needed -> message")];
92        [alert setAlertStyle: NSWarningAlertStyle];
93       
94        [alert runModal];
95        [alert release];
96       
97        exit(0);
98    }
99   
100    //make sure another Transmission.app isn't running already
101    NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
102    int processIdentifier = [[NSProcessInfo processInfo] processIdentifier];
103
104    NSDictionary * dic;
105    NSEnumerator * enumerator = [[[NSWorkspace sharedWorkspace] launchedApplications] objectEnumerator];
106    while ((dic = [enumerator nextObject]))
107    {
108        if ([[dic objectForKey: @"NSApplicationBundleIdentifier"] isEqualToString: bundleIdentifier]
109            && [[dic objectForKey: @"NSApplicationProcessIdentifier"] intValue] != processIdentifier)
110        {
111            NSAlert * alert = [[NSAlert alloc] init];
112            [alert addButtonWithTitle: NSLocalizedString(@"Quit", "Transmission already running alert -> button")];
113            [alert setMessageText: NSLocalizedString(@"Transmission is already running.",
114                                                    "Transmission already running alert -> title")];
115            [alert setInformativeText: NSLocalizedString(@"There is already a copy of Transmission running. "
116                "This copy cannot be opened until that instance is quit.", "Transmission already running alert -> message")];
117            [alert setAlertStyle: NSWarningAlertStyle];
118           
119            [alert runModal];
120            [alert release];
121           
122            //activate the already running instance, then kill ourselves right away
123            [[NSWorkspace sharedWorkspace] launchApplication: [dic objectForKey: @"NSApplicationPath"]];
124            exit(0);
125        }
126    }
127   
128    [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithContentsOfFile:
129        [[NSBundle mainBundle] pathForResource: @"Defaults" ofType: @"plist"]]];
130   
131    //set custom value transformers
132    ActionMenuSpeedToDisplayLimitTransformer * limitTransformer =
133                        [[[ActionMenuSpeedToDisplayLimitTransformer alloc] init] autorelease];
134    [NSValueTransformer setValueTransformer: limitTransformer forName: @"ActionMenuSpeedToDisplayLimitTransformer"];
135   
136    ActionMenuRatioToDisplayRatioTransformer * ratioTransformer =
137                        [[[ActionMenuRatioToDisplayRatioTransformer alloc] init] autorelease];
138    [NSValueTransformer setValueTransformer: ratioTransformer forName: @"ActionMenuRatioToDisplayRatioTransformer"];
139   
140    ExpandedPathToPathTransformer * pathTransformer =
141                        [[[ExpandedPathToPathTransformer alloc] init] autorelease];
142    [NSValueTransformer setValueTransformer: pathTransformer forName: @"ExpandedPathToPathTransformer"];
143   
144    ExpandedPathToIconTransformer * iconTransformer =
145                        [[[ExpandedPathToIconTransformer alloc] init] autorelease];
146    [NSValueTransformer setValueTransformer: iconTransformer forName: @"ExpandedPathToIconTransformer"];
147   
148    SpeedLimitToTurtleIconTransformer * speedLimitIconTransformer =
149                        [[[SpeedLimitToTurtleIconTransformer alloc] init] autorelease];
150    [NSValueTransformer setValueTransformer: speedLimitIconTransformer forName: @"SpeedLimitToTurtleIconTransformer"];
151}
152
153- (id) init
154{
155    if ((self = [super init]))
156    {
157        fLib = tr_init("macosx");
158       
159        fTorrents = [[NSMutableArray alloc] initWithCapacity: 10];
160        fDisplayedTorrents = [[NSMutableArray alloc] initWithCapacity: 10];
161        fPendingTorrentDownloads = [[NSMutableDictionary alloc] init];
162       
163        fDefaults = [NSUserDefaults standardUserDefaults];
164       
165        fMessageController = [[MessageWindowController alloc] initWithWindowNibName: @"MessageWindow"];
166        fInfoController = [[InfoWindowController alloc] initWithWindowNibName: @"InfoWindow"];
167        fPrefsController = [[PrefsController alloc] initWithWindowNibName: @"PrefsWindow" handle: fLib];
168       
169        fBadger = [[Badger alloc] init];
170       
171        [GrowlApplicationBridge setGrowlDelegate: self];
172       
173        [[UKKQueue sharedFileWatcher] setDelegate: self];
174    }
175    return self;
176}
177
178- (void) dealloc
179{
180    [[NSNotificationCenter defaultCenter] removeObserver: self];
181   
182    [fInfoController release];
183    [fMessageController release];
184    [fPrefsController release];
185   
186    [fToolbar release];
187    [fTorrents release];
188    [fDisplayedTorrents release];
189    [fBadger release];
190   
191    [fSortType release];
192    [fFilterType release];
193   
194    [fAutoImportedNames release];
195    [fPendingTorrentDownloads release];
196   
197    tr_close(fLib);
198    [super dealloc];
199}
200
201- (void) awakeFromNib
202{
203    [fStatusBar setBackgroundImage: [NSImage imageNamed: @"StatusBarBackground.png"]];
204    [fFilterBar setBackgroundImage: [NSImage imageNamed: @"FilterBarBackground.png"]];
205   
206    [fWindow setAcceptsMouseMovedEvents: YES]; //ensure filter buttons display correctly
207
208    fToolbar = [[NSToolbar alloc] initWithIdentifier: @"Transmission Toolbar"];
209    [fToolbar setDelegate: self];
210    [fToolbar setAllowsUserCustomization: YES];
211    [fToolbar setAutosavesConfiguration: YES];
212    [fWindow setToolbar: fToolbar];
213    [fWindow setDelegate: self];
214   
215    [fWindow makeFirstResponder: fTableView];
216    [fWindow setExcludedFromWindowsMenu: YES];
217   
218    //set table size
219    if ([fDefaults boolForKey: @"SmallView"])
220        [fTableView setRowHeight: ROW_HEIGHT_SMALL];
221   
222    //window min height
223    NSSize contentMinSize = [fWindow contentMinSize];
224    contentMinSize.height = [[fWindow contentView] frame].size.height - [fScrollView frame].size.height
225                                + [fTableView rowHeight] + [fTableView intercellSpacing].height;
226    [fWindow setContentMinSize: contentMinSize];
227   
228    //set info and filter keyboard shortcuts
229    unichar rightChar = NSRightArrowFunctionKey, leftChar = NSLeftArrowFunctionKey;
230    [fNextInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & rightChar length: 1]];
231    [fPrevInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & leftChar length: 1]];
232   
233    [fNextFilterItem setKeyEquivalent: [NSString stringWithCharacters: & rightChar length: 1]];
234    [fNextFilterItem setKeyEquivalentModifierMask: NSCommandKeyMask + NSAlternateKeyMask];
235    [fPrevFilterItem setKeyEquivalent: [NSString stringWithCharacters: & leftChar length: 1]];
236    [fPrevFilterItem setKeyEquivalentModifierMask: NSCommandKeyMask + NSAlternateKeyMask];
237   
238    //set up filter bar
239    NSView * contentView = [fWindow contentView];
240    NSSize windowSize = [contentView convertSize: [fWindow frame].size fromView: nil];
241    [fFilterBar setHidden: YES];
242   
243    NSRect filterBarFrame = [fFilterBar frame];
244    filterBarFrame.size.width = windowSize.width;
245    [fFilterBar setFrame: filterBarFrame];
246   
247    [contentView addSubview: fFilterBar];
248    [fFilterBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
249
250    [self showFilterBar: [fDefaults boolForKey: @"FilterBar"] animate: NO];
251   
252    //set up status bar
253    [fStatusBar setHidden: YES];
254   
255    NSRect statusBarFrame = [fStatusBar frame];
256    statusBarFrame.size.width = windowSize.width;
257    [fStatusBar setFrame: statusBarFrame];
258
259    [contentView addSubview: fStatusBar];
260    [fStatusBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
261    [self showStatusBar: [fDefaults boolForKey: @"StatusBar"] animate: NO];
262
263    [fActionButton setToolTip: NSLocalizedString(@"Shortcuts for changing global settings.",
264                                "Main window -> 1st bottom left button (action) tooltip")];
265    [fSpeedLimitButton setToolTip: NSLocalizedString(@"Speed Limit overrides the total bandwidth limits with its own limits.",
266                                "Main window -> 2nd bottom left button (turtle) tooltip")];
267   
268    [fPrefsController setUpdater: fUpdater];
269   
270    [fTableView setTorrents: fDisplayedTorrents];
271    [[fTableView tableColumnWithIdentifier: @"Torrent"] setDataCell: [[TorrentCell alloc] init]];
272
273    [fTableView registerForDraggedTypes: [NSArray arrayWithObjects: NSFilenamesPboardType,
274                                            NSURLPboardType, TORRENT_TABLE_VIEW_DATA_TYPE, nil]];
275
276    //register for sleep notifications
277    IONotificationPortRef notify;
278    io_object_t iterator;
279    if (fRootPort = IORegisterForSystemPower(self, & notify, sleepCallBack, & iterator))
280        CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify), kCFRunLoopCommonModes);
281    else
282        NSLog(@"Could not IORegisterForSystemPower");
283
284    //load previous transfers
285    NSArray * history = [[NSArray alloc] initWithContentsOfFile: [[self applicationSupportFolder]
286                                                stringByAppendingPathComponent: @"Transfers.plist"]];
287   
288    //old version saved transfer info in prefs file
289    if (!history)
290        if ((history = [fDefaults arrayForKey: @"History"]))
291            [history retain];
292    [fDefaults removeObjectForKey: @"History"];
293   
294    if (history)
295    {
296        Torrent * torrent;
297        NSDictionary * historyItem;
298        NSEnumerator * enumerator = [history objectEnumerator];
299        while ((historyItem = [enumerator nextObject]))
300            if ((torrent = [[Torrent alloc] initWithHistory: historyItem lib: fLib]))
301            {
302                [fTorrents addObject: torrent];
303                [torrent release];
304            }
305       
306        [history release];
307    }
308   
309    //set sort
310    fSortType = [[fDefaults stringForKey: @"Sort"] retain];
311   
312    NSMenuItem * currentSortItem, * currentSortActionItem;
313    if ([fSortType isEqualToString: @"Name"])
314    {
315        currentSortItem = fNameSortItem;
316        currentSortActionItem = fNameSortActionItem;
317    }
318    else if ([fSortType isEqualToString: @"State"])
319    {
320        currentSortItem = fStateSortItem;
321        currentSortActionItem = fStateSortActionItem;
322    }
323    else if ([fSortType isEqualToString: @"Progress"])
324    {
325        currentSortItem = fProgressSortItem;
326        currentSortActionItem = fProgressSortActionItem;
327    }
328    else if ([fSortType isEqualToString: @"Date"])
329    {
330        currentSortItem = fDateSortItem;
331        currentSortActionItem = fDateSortActionItem;
332    }
333    else
334    {
335        currentSortItem = fOrderSortItem;
336        currentSortActionItem = fOrderSortActionItem;
337    }
338    [currentSortItem setState: NSOnState];
339    [currentSortActionItem setState: NSOnState];
340   
341    //set filter
342    fFilterType = [[fDefaults stringForKey: @"Filter"] retain];
343
344    BarButton * currentFilterButton;
345    if ([fFilterType isEqualToString: @"Pause"])
346        currentFilterButton = fPauseFilterButton;
347    else if ([fFilterType isEqualToString: @"Seed"])
348        currentFilterButton = fSeedFilterButton;
349    else if ([fFilterType isEqualToString: @"Download"])
350        currentFilterButton = fDownloadFilterButton;
351    else
352        currentFilterButton = fNoFilterButton;
353
354    [currentFilterButton setEnabled: YES];
355   
356    //observe notifications
357    NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
358   
359    [nc addObserver: self selector: @selector(updateUI)
360                    name: @"UpdateUI" object: nil];
361   
362    [nc addObserver: fInfoController selector: @selector(updateInfoSettings)
363                    name: @"UpdateInfoSettings" object: nil];
364   
365    [nc addObserver: self selector: @selector(torrentFinishedDownloading:)
366                    name: @"TorrentFinishedDownloading" object: nil];
367   
368    [nc addObserver: self selector: @selector(updateControlTint:)
369                    name: NSControlTintDidChangeNotification object: nil];
370   
371    [nc addObserver: self selector: @selector(prepareForUpdate:)
372                    name: SUUpdaterWillRestartNotification object: nil];
373    fUpdateInProgress = NO;
374   
375    [nc addObserver: self selector: @selector(autoSpeedLimitChange:)
376                    name: @"AutoSpeedLimitChange" object: nil];
377   
378    [nc addObserver: self selector: @selector(changeAutoImport)
379                    name: @"AutoImportSettingChange" object: nil];
380   
381    [nc addObserver: self selector: @selector(setWindowSizeToFit)
382                    name: @"AutoSizeSettingChange" object: nil];
383   
384    [nc addObserver: fWindow selector: @selector(makeKeyWindow)
385                    name: @"MakeWindowKey" object: nil];
386   
387    //check if torrent should now start
388    [nc addObserver: self selector: @selector(torrentStoppedForRatio:)
389                    name: @"TorrentStoppedForRatio" object: nil];
390   
391    [nc addObserver: self selector: @selector(updateTorrentsInQueue)
392                    name: @"UpdateQueue" object: nil];
393   
394    //change that just impacts the dock badge
395    [nc addObserver: self selector: @selector(resetDockBadge:)
396                    name: @"DockBadgeChange" object: nil];
397
398    //timer to update the interface every second
399    fCompleted = 0;
400    [self updateUI];
401    fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_UI_SECONDS target: self
402        selector: @selector(updateUI) userInfo: nil repeats: YES];
403    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSModalPanelRunLoopMode];
404    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSEventTrackingRunLoopMode];
405   
406    [self applyFilter: nil];
407   
408    [fWindow makeKeyAndOrderFront: nil];
409
410    if ([fDefaults boolForKey: @"InfoVisible"])
411        [self showInfo: nil];
412   
413    //timer to auto toggle speed limit
414    [self autoSpeedLimitChange: nil];
415    fSpeedLimitTimer = [NSTimer scheduledTimerWithTimeInterval: AUTO_SPEED_LIMIT_SECONDS target: self
416        selector: @selector(autoSpeedLimit:) userInfo: nil repeats: YES];
417   
418    //auto importing
419    fAutoImportedNames = [[NSMutableArray alloc] init];
420    [self checkAutoImportDirectory];
421}
422
423- (void) applicationDidFinishLaunching: (NSNotification *) notification
424{
425    [NSApp setServicesProvider: self];
426   
427    //register for dock icon drags
428    [[NSAppleEventManager sharedAppleEventManager] setEventHandler: self
429        andSelector: @selector(handleOpenContentsEvent:replyEvent:)
430        forEventClass: kCoreEventClass andEventID: kAEOpenContents];
431}
432
433- (BOOL) applicationShouldHandleReopen: (NSApplication *) app hasVisibleWindows: (BOOL) visibleWindows
434{
435    if (![fWindow isVisible] && ![[fPrefsController window] isVisible])
436        [fWindow makeKeyAndOrderFront: nil];
437    return NO;
438}
439
440- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) sender
441{
442    if (!fUpdateInProgress && [fDefaults boolForKey: @"CheckQuit"])
443    {
444        int active = 0, downloading = 0;
445        Torrent * torrent;
446        NSEnumerator * enumerator = [fTorrents objectEnumerator];
447        while ((torrent = [enumerator nextObject]))
448            if ([torrent isActive])
449            {
450                active++;
451                if (![torrent isSeeding])
452                    downloading++;
453            }
454
455        if ([fDefaults boolForKey: @"CheckQuitDownloading"] ? downloading > 0 : active > 0)
456        {
457            NSString * message = active == 1
458                ? NSLocalizedString(@"There is an active transfer. Do you really want to quit?",
459                    "Confirm Quit panel -> message")
460                : [NSString stringWithFormat: NSLocalizedString(@"There are %d active transfers. Do you really want to quit?",
461                    "Confirm Quit panel -> message"), active];
462
463            NSBeginAlertSheet(NSLocalizedString(@"Confirm Quit", "Confirm Quit panel -> title"),
464                                NSLocalizedString(@"Quit", "Confirm Quit panel -> button"),
465                                NSLocalizedString(@"Cancel", "Confirm Quit panel -> button"), nil, fWindow, self,
466                                @selector(quitSheetDidEnd:returnCode:contextInfo:), nil, nil, message);
467            return NSTerminateLater;
468        }
469    }
470
471    return NSTerminateNow;
472}
473
474- (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo
475{
476    [NSApp replyToApplicationShouldTerminate: returnCode == NSAlertDefaultReturn];
477}
478
479- (void) applicationWillTerminate: (NSNotification *) notification
480{
481    //remove all torrent downloads
482    NSEnumerator * enumerator = [[fPendingTorrentDownloads allValues] objectEnumerator];
483    NSDictionary * downloadDict;
484    NSURLDownload * download;
485    while ((downloadDict = [enumerator nextObject]))
486    {
487        download = [downloadDict objectForKey: @"Download"];
488        [download cancel];
489        [download release];
490    }
491    [fPendingTorrentDownloads removeAllObjects];
492   
493    //stop timers
494    [fSpeedLimitTimer invalidate];
495    [fTimer invalidate];
496   
497    //save history and stop running torrents
498    [self updateTorrentHistory];
499    [fTorrents makeObjectsPerformSelector: @selector(stopTransferForQuit)];
500   
501    //disable NAT traversal
502    tr_natTraversalEnable(fLib, 0);
503   
504    //remember window states and close all windows
505    [fDefaults setBool: [[fInfoController window] isVisible] forKey: @"InfoVisible"];
506    [[NSApp windows] makeObjectsPerformSelector: @selector(close)];
507    [self showStatusBar: NO animate: NO];
508    [self showFilterBar: NO animate: NO];
509   
510    //clear badge
511    [fBadger clearBadge];
512
513    //end quickly if the app is updating
514    if (fUpdateInProgress)
515        return;
516
517    //wait for running transfers to stop (5 second timeout)
518    NSDate * start = [NSDate date];
519    BOOL timeUp = NO;
520   
521    enumerator = [fTorrents objectEnumerator];
522    Torrent * torrent;
523    while (!timeUp && (torrent = [enumerator nextObject]))
524        while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
525        {
526            usleep(100000);
527            [torrent update];
528        }
529   
530    //wait for NAT to be disabled (same 5 second timeout)
531    while (!([start timeIntervalSinceNow] < -5.0)
532                && tr_handleStatus(fLib)->natTraversalStatus != TR_NAT_TRAVERSAL_DISABLED)
533        usleep(100000);
534}
535
536- (void) handleOpenContentsEvent: (NSAppleEventDescriptor *) event replyEvent: (NSAppleEventDescriptor *) replyEvent
537{
538    NSString * urlString = nil;
539
540    NSAppleEventDescriptor * directObject = [event paramDescriptorForKeyword: keyDirectObject];
541    if ([directObject descriptorType] == typeAEList)
542    {
543        unsigned i;
544        for (i = 1; i <= [directObject numberOfItems]; i++)
545            if ((urlString = [[directObject descriptorAtIndex: i] stringValue]))
546                break;
547    }
548    else
549        urlString = [directObject stringValue];
550   
551    if (urlString)
552        [self openURL: [[[NSURL alloc] initWithString: urlString] autorelease]];
553}
554
555- (void) download: (NSURLDownload *) download decideDestinationWithSuggestedFilename: (NSString *) suggestedName
556{
557    if ([[suggestedName pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
558    {
559        [download cancel];
560       
561        NSRunAlertPanel(NSLocalizedString(@"Torrent download failed",
562            @"Download not a torrent -> title"), [NSString stringWithFormat:
563            NSLocalizedString(@"It appears that the file \"%@\" from %@ is not a torrent file.",
564            @"Download not a torrent -> message"), suggestedName,
565            [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]],
566            NSLocalizedString(@"OK", @"Download not a torrent -> button"), nil, nil);
567       
568        [download release];
569    }
570    else
571        [download setDestination: [NSTemporaryDirectory() stringByAppendingPathComponent: [suggestedName lastPathComponent]]
572                    allowOverwrite: NO];
573}
574
575-(void) download: (NSURLDownload *) download didCreateDestination: (NSString *) path
576{
577    [fPendingTorrentDownloads setObject: [NSDictionary dictionaryWithObjectsAndKeys:
578                    path, @"Path", download, @"Download", nil] forKey: [[download request] URL]];
579}
580
581- (void) download: (NSURLDownload *) download didFailWithError: (NSError *) error
582{
583    NSRunAlertPanel(NSLocalizedString(@"Torrent download failed",
584        @"Torrent download error -> title"), [NSString stringWithFormat:
585        NSLocalizedString(@"The torrent could not be downloaded from %@ because an error occurred (%@).",
586        @"Torrent download failed -> message"),
587        [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding],
588        [error localizedDescription]], NSLocalizedString(@"OK", @"Torrent download failed -> button"), nil, nil);
589   
590    [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]];
591    [download release];
592}
593
594- (void) downloadDidFinish: (NSURLDownload *) download
595{
596    NSString * path = [[fPendingTorrentDownloads objectForKey: [[download request] URL]] objectForKey: @"Path"];
597   
598    [self openFiles: [NSArray arrayWithObject: path] ignoreDownloadFolder:
599        ![[fDefaults stringForKey: @"DownloadChoice"] isEqualToString: @"Constant"] forceDeleteTorrent: YES];
600   
601    [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]];
602    [download release];
603   
604    //delete torrent file if it wasn't already
605    [[NSFileManager defaultManager] removeFileAtPath: path handler: nil];
606}
607
608- (void) application: (NSApplication *) app openFiles: (NSArray *) filenames
609{
610    [self openFiles: filenames ignoreDownloadFolder: NO forceDeleteTorrent: NO];
611}
612
613- (void) openFiles: (NSArray *) filenames ignoreDownloadFolder: (BOOL) ignore forceDeleteTorrent: (BOOL) delete
614{
615    NSString * downloadChoice = [fDefaults stringForKey: @"DownloadChoice"];
616    if (ignore || [downloadChoice isEqualToString: @"Ask"])
617    {
618        [self openFilesAsk: [filenames mutableCopy] forceDeleteTorrent: delete];
619        return;
620    }
621   
622    if ([fDefaults boolForKey: @"UseIncompleteDownloadFolder"]
623        && access([[[fDefaults stringForKey: @"IncompleteDownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0))
624    {
625        NSOpenPanel * panel = [NSOpenPanel openPanel];
626       
627        [panel setPrompt: NSLocalizedString(@"Select", "Default incomplete folder cannot be found alert -> prompt")];
628        [panel setAllowsMultipleSelection: NO];
629        [panel setCanChooseFiles: NO];
630        [panel setCanChooseDirectories: YES];
631        [panel setCanCreateDirectories: YES];
632
633        [panel setMessage: NSLocalizedString(@"The incomplete folder cannot be found."
634                                        " Choose a new location or cancel for none.",
635                                        "Default incomplete folder cannot be found alert -> message")];
636       
637        NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
638                                        filenames, @"Filenames",
639                                        [NSNumber numberWithBool: ignore], @"Ignore",
640                                        [NSNumber numberWithBool: delete], @"Delete", nil];
641       
642        [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
643                didEndSelector: @selector(incompleteChoiceClosed:returnCode:contextInfo:) contextInfo: dict];
644        return;
645    }
646    if ([downloadChoice isEqualToString: @"Constant"]
647        && access([[[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0))
648    {
649        NSOpenPanel * panel = [NSOpenPanel openPanel];
650       
651        [panel setPrompt: NSLocalizedString(@"Select", "Default folder cannot be found alert -> prompt")];
652        [panel setAllowsMultipleSelection: NO];
653        [panel setCanChooseFiles: NO];
654        [panel setCanChooseDirectories: YES];
655        [panel setCanCreateDirectories: YES];
656
657        [panel setMessage: NSLocalizedString(@"The download folder cannot be found. Choose a new location.",
658                                        "Default folder cannot be found alert -> message")];
659       
660        NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
661                                        filenames, @"Filenames",
662                                        [NSNumber numberWithBool: ignore], @"Ignore",
663                                        [NSNumber numberWithBool: delete], @"Delete", nil];
664       
665        [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
666                didEndSelector: @selector(downloadChoiceClosed:returnCode:contextInfo:) contextInfo: dict];
667        return;
668    }
669   
670    Torrent * torrent;
671    NSString * torrentPath;
672    NSEnumerator * enumerator = [filenames objectEnumerator];
673    while ((torrentPath = [enumerator nextObject]))
674    {
675        if (!(torrent = [[Torrent alloc] initWithPath: torrentPath forceDeleteTorrent: delete lib: fLib]))
676            continue;
677
678        //add it to the "File > Open Recent" menu
679        [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
680
681        NSString * folder = [downloadChoice isEqualToString: @"Constant"]
682            ? [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath]
683            : [torrentPath stringByDeletingLastPathComponent];
684       
685        [torrent setDownloadFolder: folder];
686        [torrent update];
687       
688        [fTorrents addObject: torrent];
689        [torrent release];
690    }
691
692    [self updateTorrentsInQueue];
693}
694
695- (void) incompleteChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code contextInfo: (NSDictionary *) dictionary
696{
697    if (code == NSOKButton)
698        [fDefaults setObject: [[openPanel filenames] objectAtIndex: 0] forKey: @"IncompleteDownloadFolder"];
699    else
700        [fDefaults setBool: NO forKey: @"UseIncompleteDownloadFolder"];
701   
702    [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO];
703}
704
705- (void) downloadChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code contextInfo: (NSDictionary *) dictionary
706{
707    if (code == NSOKButton)
708    {
709        [fDefaults setObject: [[openPanel filenames] objectAtIndex: 0] forKey: @"DownloadFolder"];
710        [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO];
711    }
712    else
713        [dictionary release];
714}
715
716- (void) openFilesWithDict: (NSDictionary *) dictionary
717{
718    [self openFiles: [dictionary objectForKey: @"Filenames"]
719        ignoreDownloadFolder: [[dictionary objectForKey: @"Ignore"] boolValue]
720        forceDeleteTorrent: [[dictionary objectForKey: @"Delete"] boolValue]];
721   
722    [dictionary release];
723}
724
725//called by the main open method to show sheet for choosing download location
726- (void) openFilesAsk: (NSMutableArray *) files forceDeleteTorrent: (BOOL) delete
727{
728    NSString * torrentPath;
729    tr_torrent_t * tempTor;
730    int error;
731   
732    //determine next file that can be opened
733    do
734    {
735        if ([files count] == 0) //no files left to open
736        {
737            [files release];
738           
739            [self updateTorrentHistory];
740            return;
741        }
742   
743        torrentPath = [[files objectAtIndex: 0] retain];
744        tempTor = tr_torrentInit(fLib, [torrentPath UTF8String], NULL, 0, &error);
745       
746        [files removeObjectAtIndex: 0];
747    } while (!tempTor);
748
749    NSOpenPanel * panel = [NSOpenPanel openPanel];
750
751    [panel setPrompt: NSLocalizedString(@"Select", "Open torrent -> prompt")];
752    [panel setAllowsMultipleSelection: NO];
753    [panel setCanChooseFiles: NO];
754    [panel setCanChooseDirectories: YES];
755    [panel setCanCreateDirectories: YES];
756
757    [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the download folder for \"%@\"",
758                        "Open torrent -> select destination folder"),
759                        [NSString stringWithUTF8String: tr_torrentInfo(tempTor)->name]]];
760   
761    NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys: torrentPath, @"Path",
762                                    files, @"Files", [NSNumber numberWithBool: delete], @"Delete", nil];
763    [torrentPath release];
764
765    tr_torrentClose(fLib, tempTor);
766    [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
767            didEndSelector: @selector(folderChoiceClosed:returnCode:contextInfo:) contextInfo: dictionary];
768}
769
770- (void) folderChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code contextInfo: (NSDictionary *) dictionary
771{
772    if (code == NSOKButton)
773    {
774        NSString * torrentPath = [dictionary objectForKey: @"Path"];
775        Torrent * torrent = [[Torrent alloc] initWithPath: torrentPath forceDeleteTorrent:
776                                [[dictionary objectForKey: @"Delete"] boolValue] lib: fLib];
777       
778        //add it to the "File > Open Recent" menu
779        [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
780       
781        [torrent setDownloadFolder: [[openPanel filenames] objectAtIndex: 0]];
782        [torrent update];
783       
784        [fTorrents addObject: torrent];
785        [torrent release];
786       
787        [self updateTorrentsInQueue];
788    }
789   
790    [self performSelectorOnMainThread: @selector(openFilesAskWithDict:) withObject: dictionary waitUntilDone: NO];
791}
792
793- (void) openFilesAskWithDict: (NSDictionary *) dictionary
794{
795    [self openFilesAsk: [dictionary objectForKey: @"Files"]
796            forceDeleteTorrent: [[dictionary objectForKey: @"Delete"] boolValue]];
797    [dictionary release];
798}
799
800//called on by applescript
801- (void) open: (NSArray *) files
802{
803    [self performSelectorOnMainThread: @selector(openFiles:) withObject: files waitUntilDone: NO];
804}
805
806- (void) openFiles: (NSArray *) filenames
807{
808    [self openFiles: filenames ignoreDownloadFolder: NO forceDeleteTorrent: NO];
809}
810
811- (void) openShowSheet: (id) sender
812{
813    NSOpenPanel * panel = [NSOpenPanel openPanel];
814
815    [panel setAllowsMultipleSelection: YES];
816    [panel setCanChooseFiles: YES];
817    [panel setCanChooseDirectories: NO];
818
819    [panel beginSheetForDirectory: nil file: nil types: [NSArray arrayWithObject: @"torrent"]
820        modalForWindow: fWindow modalDelegate: self didEndSelector: @selector(openSheetClosed:returnCode:contextInfo:)
821        contextInfo: [NSNumber numberWithBool: sender == fOpenIgnoreDownloadFolder]];
822}
823
824- (void) openSheetClosed: (NSOpenPanel *) panel returnCode: (int) code contextInfo: (NSNumber *) ignore
825{
826    if (code == NSOKButton)
827    {
828        NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:
829                                        [panel filenames], @"Files", ignore, @"Ignore", nil];
830        [self performSelectorOnMainThread: @selector(openFromSheet:) withObject: dictionary waitUntilDone: NO];
831    }
832}
833
834- (void) openFromSheet: (NSDictionary *) dictionary
835{
836    [self openFiles: [dictionary objectForKey: @"Files"]
837        ignoreDownloadFolder: [[dictionary objectForKey: @"Ignore"] boolValue] forceDeleteTorrent: NO];
838   
839    [dictionary release];
840}
841
842- (void) openURL: (NSURL *) url
843{
844    [[NSURLDownload alloc] initWithRequest: [NSURLRequest requestWithURL: url] delegate: self];
845}
846
847- (void) openURLShowSheet: (id) sender
848{
849    [NSApp beginSheet: fURLSheetWindow modalForWindow: fWindow modalDelegate: self
850            didEndSelector: @selector(urlSheetDidEnd:returnCode:contextInfo:) contextInfo: nil];
851}
852
853- (void) openURLEndSheet: (id) sender
854{
855    [fURLSheetWindow orderOut: sender];
856    [NSApp endSheet: fURLSheetWindow returnCode: 1];
857}
858
859- (void) openURLCancelEndSheet: (id) sender
860{
861    [fURLSheetWindow orderOut: sender];
862    [NSApp endSheet: fURLSheetWindow returnCode: 0];
863}
864
865- (void) urlSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo
866{
867    [fURLSheetTextField selectText: self];
868    if (returnCode != 1)
869        return;
870   
871    NSString * urlString = [fURLSheetTextField stringValue];
872    if (![urlString isEqualToString: @""])
873    {
874        if ([urlString rangeOfString: @"://"].location == NSNotFound)
875        {
876            if ([urlString rangeOfString: @"."].location == NSNotFound)
877            {
878                int beforeCom;
879                if ((beforeCom = [urlString rangeOfString: @"/"].location) != NSNotFound)
880                    urlString = [NSString stringWithFormat: @"http://www.%@.com/%@",
881                                    [urlString substringToIndex: beforeCom],
882                                    [urlString substringFromIndex: beforeCom + 1]];
883                else
884                    urlString = [NSString stringWithFormat: @"http://www.%@.com", urlString];
885            }
886            else
887                urlString = [@"http://" stringByAppendingString: urlString];
888        }
889       
890        NSURL * url = [NSURL URLWithString: urlString];
891        [self performSelectorOnMainThread: @selector(openURL:) withObject: url waitUntilDone: NO];
892    }
893}
894
895- (void) resumeSelectedTorrents: (id) sender
896{
897    [self resumeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
898}
899
900- (void) resumeAllTorrents: (id) sender
901{
902    [self resumeTorrents: fTorrents];
903}
904
905- (void) resumeTorrents: (NSArray *) torrents
906{
907    NSEnumerator * enumerator = [torrents objectEnumerator];
908    Torrent * torrent;
909    while ((torrent = [enumerator nextObject]))
910        [torrent setWaitToStart: YES];
911   
912    [self updateTorrentsInQueue];
913}
914
915- (void) resumeSelectedTorrentsNoWait:  (id) sender
916{
917    [self resumeTorrentsNoWait: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
918}
919
920- (void) resumeWaitingTorrents: (id) sender
921{
922    NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [fTorrents count]];
923   
924    NSEnumerator * enumerator = [fTorrents objectEnumerator];
925    Torrent * torrent;
926    while ((torrent = [enumerator nextObject]))
927        if (![torrent isActive] && [torrent waitingToStart])
928            [torrents addObject: torrent];
929   
930    [self resumeTorrentsNoWait: torrents];
931}
932
933- (void) resumeTorrentsNoWait: (NSArray *) torrents
934{
935    //iterate through instead of all at once to ensure no conflicts
936    NSEnumerator * enumerator = [torrents objectEnumerator];
937    Torrent * torrent;
938    while ((torrent = [enumerator nextObject]))
939        [torrent startTransfer];
940   
941    [self updateUI];
942    [self applyFilter: nil];
943    [self updateTorrentHistory];
944}
945
946- (void) stopSelectedTorrents: (id) sender
947{
948    [self stopTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
949}
950
951- (void) stopAllTorrents: (id) sender
952{
953    [self stopTorrents: fTorrents];
954}
955
956- (void) stopTorrents: (NSArray *) torrents
957{
958    //don't want any of these starting then stopping
959    NSEnumerator * enumerator = [torrents objectEnumerator];
960    Torrent * torrent;
961    while ((torrent = [enumerator nextObject]))
962        [torrent setWaitToStart: NO];
963
964    [torrents makeObjectsPerformSelector: @selector(stopTransfer)];
965   
966    [self updateUI];
967    [self applyFilter: nil];
968    [self updateTorrentHistory];
969}
970
971- (void) removeTorrents: (NSArray *) torrents
972        deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
973{
974    [torrents retain];
975    int active = 0, downloading = 0;
976
977    if ([fDefaults boolForKey: @"CheckRemove"])
978    {
979        Torrent * torrent;
980        NSEnumerator * enumerator = [torrents objectEnumerator];
981        while ((torrent = [enumerator nextObject]))
982            if ([torrent isActive])
983            {
984                active++;
985                if (![torrent isSeeding])
986                    downloading++;
987            }
988
989        if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? downloading > 0 : active > 0)
990        {
991            NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
992                torrents, @"Torrents",
993                [NSNumber numberWithBool: deleteData], @"DeleteData",
994                [NSNumber numberWithBool: deleteTorrent], @"DeleteTorrent", nil];
995
996            NSString * title, * message;
997           
998            int selected = [fTableView numberOfSelectedRows];
999            if (selected == 1)
1000            {
1001                title = [NSString stringWithFormat: NSLocalizedString(@"Confirm Removal of \"%@\"",
1002                            "Removal confirm panel -> title"),
1003                            [[fDisplayedTorrents objectAtIndex: [fTableView selectedRow]] name]];
1004                message = NSLocalizedString(@"This transfer is active."
1005                            " Once removed, continuing the transfer will require the torrent file."
1006                            " Do you really want to remove it?", "Removal confirm panel -> message");
1007            }
1008            else
1009            {
1010                title = [NSString stringWithFormat: NSLocalizedString(@"Confirm Removal of %d Transfers",
1011                            "Removal confirm panel -> title"), selected];
1012                if (selected == active)
1013                    message = [NSString stringWithFormat: NSLocalizedString(@"There are %d active transfers.",
1014                                "Removal confirm panel -> message part 1"), active];
1015                else
1016                    message = [NSString stringWithFormat: NSLocalizedString(@"There are %d transfers (%d active).",
1017                                "Removal confirm panel -> message part 1"), selected, active];
1018                message = [message stringByAppendingString:
1019                    NSLocalizedString(@" Once removed, continuing the transfers will require the torrent files."
1020                    " Do you really want to remove them?", "Removal confirm panel -> message part 2")];
1021            }
1022
1023            NSBeginAlertSheet(title, NSLocalizedString(@"Remove", "Removal confirm panel -> button"),
1024                NSLocalizedString(@"Cancel", "Removal confirm panel -> button"), nil, fWindow, self,
1025                nil, @selector(removeSheetDidEnd:returnCode:contextInfo:), dict, message);
1026            return;
1027        }
1028    }
1029   
1030    [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
1031}
1032
1033- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (NSDictionary *) dict
1034{
1035    NSArray * torrents = [dict objectForKey: @"Torrents"];
1036    BOOL deleteData = [[dict objectForKey: @"DeleteData"] boolValue],
1037        deleteTorrent = [[dict objectForKey: @"DeleteTorrent"] boolValue];
1038    [dict release];
1039   
1040    if (returnCode == NSAlertDefaultReturn)
1041        [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
1042    else
1043        [torrents release];
1044}
1045
1046- (void) confirmRemoveTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
1047{
1048    //don't want any of these starting then stopping
1049    NSEnumerator * enumerator = [torrents objectEnumerator];
1050    Torrent * torrent;
1051    while ((torrent = [enumerator nextObject]))
1052        [torrent setWaitToStart: NO];
1053
1054    NSNumber * lowestOrderValue = nil, * currentOrderValue;
1055
1056    enumerator = [torrents objectEnumerator];
1057    while ((torrent = [enumerator nextObject]))
1058    {
1059        [torrent stopTransfer];
1060
1061        if (deleteData)
1062            [torrent trashData];
1063        if (deleteTorrent)
1064            [torrent trashTorrent];
1065       
1066        //determine lowest order value
1067        currentOrderValue = [torrent orderValue];
1068        if (!lowestOrderValue || [lowestOrderValue compare: currentOrderValue] == NSOrderedDescending)
1069            lowestOrderValue = currentOrderValue;
1070       
1071        [fTorrents removeObject: torrent];
1072        [fDisplayedTorrents removeObject: torrent];
1073    }
1074    [torrents release];
1075
1076    //reset the order values if necessary
1077    if ([lowestOrderValue intValue] < [fTorrents count])
1078    {
1079        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1080                                                @"orderValue" ascending: YES] autorelease];
1081        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1082
1083        NSArray * tempTorrents = [fTorrents sortedArrayUsingDescriptors: descriptors];
1084        [descriptors release];
1085
1086        int i;
1087        for (i = [lowestOrderValue intValue]; i < [tempTorrents count]; i++)
1088            [[tempTorrents objectAtIndex: i] setOrderValue: i];
1089    }
1090   
1091    [fTableView deselectAll: nil];
1092   
1093    [self updateUI];
1094    [self applyFilter: nil];
1095   
1096    [self updateTorrentHistory];
1097}
1098
1099- (void) removeNoDelete: (id) sender
1100{
1101    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1102                deleteData: NO deleteTorrent: NO];
1103}
1104
1105- (void) removeDeleteData: (id) sender
1106{
1107    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1108                deleteData: YES deleteTorrent: NO];
1109}
1110
1111- (void) removeDeleteTorrent: (id) sender
1112{
1113    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1114                deleteData: NO deleteTorrent: YES];
1115}
1116
1117- (void) removeDeleteDataAndTorrent: (id) sender
1118{
1119    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1120                deleteData: YES deleteTorrent: YES];
1121}
1122
1123- (void) copyTorrentFile: (id) sender
1124{
1125    [self copyTorrentFileForTorrents: [[NSMutableArray alloc] initWithArray:
1126            [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]]];
1127}
1128
1129- (void) copyTorrentFileForTorrents: (NSMutableArray *) torrents
1130{
1131    if ([torrents count] == 0)
1132    {
1133        [torrents release];
1134        return;
1135    }
1136
1137    Torrent * torrent = [torrents objectAtIndex: 0];
1138
1139    //warn user if torrent file can't be found
1140    if (![[NSFileManager defaultManager] fileExistsAtPath: [torrent torrentLocation]])
1141    {
1142        NSAlert * alert = [[NSAlert alloc] init];
1143        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent file copy alert -> button")];
1144        [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Copy of \"%@\" Cannot Be Created",
1145                                "Torrent file copy alert -> title"), [torrent name]]];
1146        [alert setInformativeText: [NSString stringWithFormat:
1147                NSLocalizedString(@"The torrent file (%@) cannot be found.", "Torrent file copy alert -> message"),
1148                                    [torrent torrentLocation]]];
1149        [alert setAlertStyle: NSWarningAlertStyle];
1150       
1151        [alert runModal];
1152        [alert release];
1153       
1154        [torrents removeObjectAtIndex: 0];
1155        [self copyTorrentFileForTorrents: torrents];
1156    }
1157    else
1158    {
1159        NSSavePanel * panel = [NSSavePanel savePanel];
1160        [panel setRequiredFileType: @"torrent"];
1161        [panel setCanSelectHiddenExtension: YES];
1162       
1163        [panel beginSheetForDirectory: nil file: [torrent name] modalForWindow: fWindow modalDelegate: self
1164            didEndSelector: @selector(saveTorrentCopySheetClosed:returnCode:contextInfo:) contextInfo: torrents];
1165    }
1166}
1167
1168- (void) saveTorrentCopySheetClosed: (NSSavePanel *) panel returnCode: (int) code contextInfo: (NSMutableArray *) torrents
1169{
1170    //if save successful, copy torrent to new location with name of data file
1171    if (code == NSOKButton)
1172        [[NSFileManager defaultManager] copyPath: [[torrents objectAtIndex: 0] torrentLocation]
1173            toPath: [panel filename] handler: nil];
1174   
1175    [torrents removeObjectAtIndex: 0];
1176    [self performSelectorOnMainThread: @selector(copyTorrentFileForTorrents:) withObject: torrents waitUntilDone: NO];
1177}
1178
1179- (void) revealFile: (id) sender
1180{
1181    NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
1182    Torrent * torrent;
1183    while ((torrent = [enumerator nextObject]))
1184        [torrent revealData];
1185}
1186
1187- (void) announceSelectedTorrents: (id) sender
1188{
1189    NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
1190    Torrent * torrent;
1191    NSDate * date;
1192    while ((torrent = [enumerator nextObject]))
1193    {
1194        //time interval returned will be negative
1195        if (!(date = [torrent announceDate]) || [date timeIntervalSinceNow] <= ANNOUNCE_WAIT_INTERVAL_SECONDS)
1196            [torrent announce];
1197    }
1198}
1199
1200- (void) showPreferenceWindow: (id) sender
1201{
1202    NSWindow * window = [fPrefsController window];
1203    if (![window isVisible])
1204        [window center];
1205
1206    [window makeKeyAndOrderFront: nil];
1207}
1208
1209- (void) showInfo: (id) sender
1210{
1211    if ([[fInfoController window] isVisible])
1212        [fInfoController close];
1213    else
1214    {
1215        [fInfoController updateInfoStats];
1216        [[fInfoController window] orderFront: nil];
1217    }
1218}
1219
1220- (void) setInfoTab: (id) sender
1221{
1222    if (sender == fNextInfoTabItem)
1223        [fInfoController setNextTab];
1224    else
1225        [fInfoController setPreviousTab];
1226}
1227
1228- (void) showMessageWindow: (id) sender
1229{
1230    [fMessageController showWindow: nil];
1231}
1232
1233- (void) updateUI
1234{
1235    [fTorrents makeObjectsPerformSelector: @selector(update)];
1236
1237    //resort if necessary or just update the table
1238    if ([fSortType isEqualToString: @"Progress"] || [fSortType isEqualToString: @"State"])
1239        [self sortTorrents];
1240    else
1241        [fTableView reloadData];
1242   
1243    //update the global DL/UL rates
1244    float downloadRate, uploadRate;
1245    tr_torrentRates(fLib, & downloadRate, & uploadRate);
1246    if (![fStatusBar isHidden])
1247    {
1248        [fTotalDLField setStringValue: [NSLocalizedString(@"Total DL: ", "Status bar -> total download")
1249                                        stringByAppendingString: [NSString stringForSpeed: downloadRate]]];
1250        [fTotalULField setStringValue: [NSLocalizedString(@"Total UL: ", "Status bar -> total upload")
1251                                        stringByAppendingString: [NSString stringForSpeed: uploadRate]]];
1252    }
1253
1254    //update non-constant parts of info window
1255    if ([[fInfoController window] isVisible])
1256        [fInfoController updateInfoStats];
1257
1258    //badge dock
1259    [fBadger updateBadgeWithCompleted: fCompleted uploadRate: uploadRate downloadRate: downloadRate];
1260}
1261
1262- (void) updateTorrentsInQueue
1263{
1264    BOOL download = [fDefaults boolForKey: @"Queue"],
1265        seed = [fDefaults boolForKey: @"QueueSeed"];
1266   
1267    //determine the number of downloads needed to start
1268    int desiredDownloadActive = download ? [fDefaults integerForKey: @"QueueDownloadNumber"] : 0,
1269        desiredSeedActive = seed ? [fDefaults integerForKey: @"QueueSeedNumber"] : 0;
1270   
1271    Torrent * torrent;
1272    NSEnumerator * enumerator;
1273    if (download || seed)
1274    {
1275        enumerator = [fTorrents objectEnumerator];
1276        while ((torrent = [enumerator nextObject]))
1277            if ([torrent isActive] && ![torrent isError])
1278            {
1279                if ([torrent allDownloaded])
1280                    desiredSeedActive--;
1281                else
1282                    desiredDownloadActive--;
1283               
1284                if (desiredDownloadActive <= 0 && desiredSeedActive <= 0)
1285                    break;
1286            }
1287    }
1288   
1289    //sort torrents by order value
1290    NSArray * sortedTorrents;
1291    if ([fTorrents count] > 1 && (desiredDownloadActive > 0 || desiredSeedActive > 0))
1292    {
1293        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1294                                                @"orderValue" ascending: YES] autorelease];
1295        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1296       
1297        sortedTorrents = [fTorrents sortedArrayUsingDescriptors: descriptors];
1298        [descriptors release];
1299    }
1300    else
1301        sortedTorrents = fTorrents;
1302
1303    enumerator = [sortedTorrents objectEnumerator];
1304    while ((torrent = [enumerator nextObject]))
1305    {
1306        if (![torrent isActive] && [torrent waitingToStart])
1307        {
1308            if (![torrent allDownloaded])
1309            {
1310                if (!download || desiredDownloadActive > 0)
1311                {
1312                    [torrent startTransfer];
1313                    if ([torrent isActive])
1314                        desiredDownloadActive--;
1315                    [torrent update];
1316                }
1317            }
1318            else
1319            {
1320                if (!seed || desiredSeedActive > 0)
1321                {
1322                    [torrent startTransfer];
1323                    if ([torrent isActive])
1324                        desiredSeedActive--;
1325                    [torrent update];
1326                }
1327            }
1328        }
1329    }
1330   
1331    [self updateUI];
1332    [self applyFilter: nil];
1333    [self updateTorrentHistory];
1334}
1335
1336- (void) torrentFinishedDownloading: (NSNotification *) notification
1337{
1338    Torrent * torrent = [notification object];
1339   
1340    [fInfoController updateInfoStats];
1341   
1342    if ([fDefaults boolForKey: @"PlayDownloadSound"])
1343    {
1344        NSSound * sound;
1345        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"DownloadSound"]]))
1346            [sound play];
1347    }
1348   
1349    NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_DOWNLOAD_COMPLETE, @"Type",
1350                                    [torrent dataLocation] , @"Location", nil];
1351    [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Download Complete", "Growl notification title")
1352                                description: [torrent name] notificationName: GROWL_DOWNLOAD_COMPLETE
1353                                iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
1354   
1355    if (![fWindow isKeyWindow])
1356        fCompleted++;
1357   
1358    if ([fDefaults boolForKey: @"QueueSeed"])
1359    {
1360        [torrent stopTransfer];
1361        [torrent setWaitToStart: YES];
1362    }
1363   
1364    [self updateTorrentsInQueue];
1365}
1366
1367- (void) updateTorrentHistory
1368{
1369    NSMutableArray * history = [NSMutableArray arrayWithCapacity: [fTorrents count]];
1370
1371    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1372    Torrent * torrent;
1373    while ((torrent = [enumerator nextObject]))
1374        [history addObject: [torrent history]];
1375
1376    [history writeToFile: [[self applicationSupportFolder] stringByAppendingPathComponent: @"Transfers.plist"] atomically: YES];
1377}
1378
1379- (void) sortTorrents
1380{
1381    //remember selected rows if needed
1382    NSArray * selectedTorrents = nil;
1383    int numSelected = [fTableView numberOfSelectedRows];
1384    if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
1385        selectedTorrents = [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]];
1386
1387    [self sortTorrentsIgnoreSelected]; //actually sort
1388   
1389    //set selected rows if needed
1390    if (selectedTorrents)
1391    {
1392        Torrent * torrent;
1393        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1394        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1395        while ((torrent = [enumerator nextObject]))
1396            [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1397       
1398        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1399        [indexSet release];
1400    }
1401}
1402
1403//doesn't remember selected rows
1404- (void) sortTorrentsIgnoreSelected
1405{
1406    NSSortDescriptor * nameDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"name"
1407                            ascending: YES selector: @selector(caseInsensitiveCompare:)] autorelease],
1408                    * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"orderValue"
1409                                            ascending: YES] autorelease];
1410
1411    NSArray * descriptors;
1412    if ([fSortType isEqualToString: @"Name"])
1413        descriptors = [[NSArray alloc] initWithObjects: nameDescriptor, orderDescriptor, nil];
1414    else if ([fSortType isEqualToString: @"State"])
1415    {
1416        NSSortDescriptor * stateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1417                                                @"stateSortKey" ascending: NO] autorelease],
1418                        * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1419                                            @"progressSortKey" ascending: NO] autorelease],
1420                        * ratioDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1421                                            @"ratioSortKey" ascending: NO] autorelease];
1422       
1423        descriptors = [[NSArray alloc] initWithObjects: stateDescriptor, progressDescriptor, ratioDescriptor,
1424                                                            nameDescriptor, orderDescriptor, nil];
1425    }
1426    else if ([fSortType isEqualToString: @"Progress"])
1427    {
1428        NSSortDescriptor * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1429                                            @"progressSortKey" ascending: YES] autorelease],
1430                        * ratioDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1431                                            @"ratioSortKey" ascending: YES] autorelease];
1432       
1433        descriptors = [[NSArray alloc] initWithObjects: progressDescriptor, ratioDescriptor,
1434                                                            nameDescriptor, orderDescriptor, nil];
1435    }
1436    else if ([fSortType isEqualToString: @"Date"])
1437    {
1438        NSSortDescriptor * dateDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"dateAdded" ascending: YES] autorelease];
1439   
1440        descriptors = [[NSArray alloc] initWithObjects: dateDescriptor, orderDescriptor, nil];
1441    }
1442    else
1443        descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1444
1445    [fDisplayedTorrents sortUsingDescriptors: descriptors];
1446    [descriptors release];
1447   
1448    [fTableView reloadData];
1449}
1450
1451- (void) setSort: (id) sender
1452{
1453    //get checked items
1454    NSMenuItem * prevSortItem, * prevSortActionItem;
1455    if ([fSortType isEqualToString: @"Name"])
1456    {
1457        prevSortItem = fNameSortItem;
1458        prevSortActionItem = fNameSortActionItem;
1459    }
1460    else if ([fSortType isEqualToString: @"State"])
1461    {
1462        prevSortItem = fStateSortItem;
1463        prevSortActionItem = fStateSortActionItem;
1464    }
1465    else if ([fSortType isEqualToString: @"Progress"])
1466    {
1467        prevSortItem = fProgressSortItem;
1468        prevSortActionItem = fProgressSortActionItem;
1469    }
1470    else if ([fSortType isEqualToString: @"Date"])
1471    {
1472        prevSortItem = fDateSortItem;
1473        prevSortActionItem = fDateSortActionItem;
1474    }
1475    else
1476    {
1477        prevSortItem = fOrderSortItem;
1478        prevSortActionItem = fOrderSortActionItem;
1479    }
1480   
1481    if (sender != prevSortItem && sender != prevSortActionItem)
1482    {
1483        [fSortType release];
1484       
1485        //get new items to check
1486        NSMenuItem * currentSortItem, * currentSortActionItem;
1487        if (sender == fNameSortItem || sender == fNameSortActionItem)
1488        {
1489            currentSortItem = fNameSortItem;
1490            currentSortActionItem = fNameSortActionItem;
1491            fSortType = [[NSString alloc] initWithString: @"Name"];
1492        }
1493        else if (sender == fStateSortItem || sender == fStateSortActionItem)
1494        {
1495            currentSortItem = fStateSortItem;
1496            currentSortActionItem = fStateSortActionItem;
1497            fSortType = [[NSString alloc] initWithString: @"State"];
1498        }
1499        else if (sender == fProgressSortItem || sender == fProgressSortActionItem)
1500        {
1501            currentSortItem = fProgressSortItem;
1502            currentSortActionItem = fProgressSortActionItem;
1503            fSortType = [[NSString alloc] initWithString: @"Progress"];
1504        }
1505        else if (sender == fDateSortItem || sender == fDateSortActionItem)
1506        {
1507            currentSortItem = fDateSortItem;
1508            currentSortActionItem = fDateSortActionItem;
1509            fSortType = [[NSString alloc] initWithString: @"Date"];
1510        }
1511        else
1512        {
1513            currentSortItem = fOrderSortItem;
1514            currentSortActionItem = fOrderSortActionItem;
1515            fSortType = [[NSString alloc] initWithString: @"Order"];
1516        }
1517   
1518        [prevSortItem setState: NSOffState];
1519        [prevSortActionItem setState: NSOffState];
1520        [currentSortItem setState: NSOnState];
1521        [currentSortActionItem setState: NSOnState];
1522       
1523        [fDefaults setObject: fSortType forKey: @"Sort"];
1524    }
1525
1526    [self sortTorrents];
1527}
1528
1529- (void) applyFilter: (id) sender
1530{
1531    //remember selected rows if needed
1532    NSArray * selectedTorrents = [fTableView numberOfSelectedRows] > 0
1533                ? [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] : nil;
1534
1535    NSMutableArray * tempTorrents = [[NSMutableArray alloc] initWithCapacity: [fTorrents count]];
1536
1537    BOOL filtering = YES;
1538    if ([fFilterType isEqualToString: @"Pause"])
1539    {
1540        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1541        Torrent * torrent;
1542        while ((torrent = [enumerator nextObject]))
1543            if (![torrent isActive])
1544                [tempTorrents addObject: torrent];
1545    }
1546    else if ([fFilterType isEqualToString: @"Seed"])
1547    {
1548        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1549        Torrent * torrent;
1550        while ((torrent = [enumerator nextObject]))
1551            if ([torrent isSeeding])
1552                [tempTorrents addObject: torrent];
1553    }
1554    else if ([fFilterType isEqualToString: @"Download"])
1555    {
1556        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1557        Torrent * torrent;
1558        while ((torrent = [enumerator nextObject]))
1559            if ([torrent isActive] && ![torrent isSeeding])
1560                [tempTorrents addObject: torrent];
1561    }
1562    else
1563    {
1564        filtering = NO;
1565        [tempTorrents setArray: fTorrents];
1566    }
1567   
1568    NSString * searchString = [fSearchFilterField stringValue];
1569    if ([searchString length] > 0)
1570    {
1571        filtering = YES;
1572       
1573        int i;
1574        for (i = [tempTorrents count] - 1; i >= 0; i--)
1575            if ([[[tempTorrents objectAtIndex: i] name] rangeOfString: searchString
1576                                        options: NSCaseInsensitiveSearch].location == NSNotFound)
1577                [tempTorrents removeObjectAtIndex: i];
1578    }
1579   
1580    [fDisplayedTorrents setArray: tempTorrents];
1581    [tempTorrents release];
1582   
1583    [self sortTorrentsIgnoreSelected];
1584   
1585    //set selected rows if needed
1586    if (selectedTorrents)
1587    {
1588        Torrent * torrent;
1589        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1590        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1591        unsigned index;
1592        while ((torrent = [enumerator nextObject]))
1593            if ((index = [fDisplayedTorrents indexOfObject: torrent]) != NSNotFound)
1594                [indexSet addIndex: index];
1595       
1596        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1597        [indexSet release];
1598    }
1599   
1600    //set status bar torrent count text
1601    NSMutableString * totalTorrentsString = [[NSMutableString alloc] initWithString: @""];
1602    if (filtering)
1603        [totalTorrentsString appendFormat: @"%d/", [fDisplayedTorrents count]];
1604   
1605    int totalCount = [fTorrents count];
1606    if (totalCount != 1)
1607        [totalTorrentsString appendFormat: NSLocalizedString(@"%d Transfers", "Status bar transfer count"), totalCount];
1608    else
1609        [totalTorrentsString appendFormat: NSLocalizedString(@"1 Transfer", "Status bar transfer count")];
1610   
1611    [fTotalTorrentsField setStringValue: totalTorrentsString];
1612    [totalTorrentsString release];
1613
1614    [self setWindowSizeToFit];
1615}
1616
1617//resets filter and sorts torrents
1618- (void) setFilter: (id) sender
1619{
1620    BarButton * prevFilterButton;
1621    if ([fFilterType isEqualToString: @"Pause"])
1622        prevFilterButton = fPauseFilterButton;
1623    else if ([fFilterType isEqualToString: @"Seed"])
1624        prevFilterButton = fSeedFilterButton;
1625    else if ([fFilterType isEqualToString: @"Download"])
1626        prevFilterButton = fDownloadFilterButton;
1627    else
1628        prevFilterButton = fNoFilterButton;
1629   
1630    if (sender != prevFilterButton)
1631    {
1632        [prevFilterButton setEnabled: NO];
1633        [sender setEnabled: YES];
1634
1635        [fFilterType release];
1636        if (sender == fDownloadFilterButton)
1637            fFilterType = [[NSString alloc] initWithString: @"Download"];
1638        else if (sender == fPauseFilterButton)
1639            fFilterType = [[NSString alloc] initWithString: @"Pause"];
1640        else if (sender == fSeedFilterButton)
1641            fFilterType = [[NSString alloc] initWithString: @"Seed"];
1642        else
1643            fFilterType = [[NSString alloc] initWithString: @"None"];
1644
1645        [fDefaults setObject: fFilterType forKey: @"Filter"];
1646    }
1647
1648    [self applyFilter: nil];
1649}
1650
1651- (void) switchFilter: (id) sender
1652{
1653    NSButton * button;
1654    if ([fFilterType isEqualToString: @"None"])
1655        button = sender == fNextFilterItem ? fDownloadFilterButton : fPauseFilterButton;
1656    else if ([fFilterType isEqualToString: @"Download"])
1657        button = sender == fNextFilterItem ? fSeedFilterButton : fNoFilterButton;
1658    else if ([fFilterType isEqualToString: @"Seed"])
1659        button = sender == fNextFilterItem ? fPauseFilterButton : fDownloadFilterButton;
1660    else if ([fFilterType isEqualToString: @"Pause"])
1661        button = sender == fNextFilterItem ? fNoFilterButton : fSeedFilterButton;
1662    else
1663        button = fNoFilterButton;
1664   
1665    [self setFilter: button];
1666}
1667
1668- (void) updateControlTint: (NSNotification *) notification
1669{
1670    if ([fDefaults boolForKey: @"SpeedLimit"])
1671        [fSpeedLimitButton setImage: [NSColor currentControlTint] == NSBlueControlTint
1672            ? [NSImage imageNamed: @"SpeedLimitButtonBlue.png"] : [NSImage imageNamed: @"SpeedLimitButtonGraphite.png"]];
1673}
1674
1675- (void) applySpeedLimit: (id) sender
1676{
1677    [fPrefsController applySpeedSettings: nil];
1678}
1679
1680- (void) toggleSpeedLimit: (id) sender
1681{
1682    [fDefaults setBool: ![fDefaults boolForKey: @"SpeedLimit"] forKey: @"SpeedLimit"];
1683    [self applySpeedLimit: nil];
1684}
1685
1686- (void) autoSpeedLimitChange: (NSNotification *) notification
1687{
1688    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
1689        return;
1690 
1691    NSCalendarDate * onDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
1692                        [[fDefaults objectForKey: @"SpeedLimitAutoOnDate"] timeIntervalSinceReferenceDate]],
1693        * offDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
1694                        [[fDefaults objectForKey: @"SpeedLimitAutoOffDate"] timeIntervalSinceReferenceDate]],
1695        * nowDate = [NSCalendarDate calendarDate];
1696   
1697    //check if should be on if within range
1698    BOOL shouldBeOn;
1699   
1700    int onTime = [onDate hourOfDay] * 60 + [onDate minuteOfHour],
1701        offTime = [offDate hourOfDay] * 60 + [offDate minuteOfHour],
1702        nowTime = [nowDate hourOfDay] * 60 + [nowDate minuteOfHour];
1703   
1704    if (onTime == offTime)
1705        shouldBeOn = NO;
1706    else if (onTime < offTime)
1707        shouldBeOn = onTime <= nowTime && nowTime < offTime;
1708    else
1709        shouldBeOn = onTime <= nowTime || nowTime < offTime;
1710   
1711    if ([fDefaults boolForKey: @"SpeedLimit"] != shouldBeOn)
1712        [self toggleSpeedLimit: nil];
1713}
1714
1715- (void) autoSpeedLimit: (NSTimer *) timer
1716{
1717    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
1718        return;
1719   
1720    //only toggle if within first few seconds of minutes
1721    NSCalendarDate * nowDate = [NSCalendarDate calendarDate];
1722    if ([nowDate secondOfMinute] < AUTO_SPEED_LIMIT_SECONDS)
1723        return;
1724   
1725    NSCalendarDate * offDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
1726                        [[fDefaults objectForKey: @"SpeedLimitAutoOffDate"] timeIntervalSinceReferenceDate]];
1727   
1728    BOOL toggle;
1729    if ([fDefaults boolForKey: @"SpeedLimit"])
1730        toggle = [nowDate hourOfDay] == [offDate hourOfDay] && [nowDate minuteOfHour] == [offDate minuteOfHour];
1731    else
1732    {
1733        NSCalendarDate * onDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
1734                        [[fDefaults objectForKey: @"SpeedLimitAutoOnDate"] timeIntervalSinceReferenceDate]];
1735        toggle = ([nowDate hourOfDay] == [onDate hourOfDay] && [nowDate minuteOfHour] == [onDate minuteOfHour])
1736                    && !([onDate hourOfDay] == [offDate hourOfDay] && [onDate minuteOfHour] == [offDate minuteOfHour]);
1737    }
1738   
1739    if (toggle)
1740    {
1741        [self toggleSpeedLimit: nil];
1742       
1743        [GrowlApplicationBridge notifyWithTitle: [fDefaults boolForKey: @"SpeedLimit"]
1744                ? NSLocalizedString(@"Speed Limit Auto Enabled", "Growl notification title")
1745                : NSLocalizedString(@"Speed Limit Auto Disabled", "Growl notification title")
1746            description: NSLocalizedString(@"Bandwidth settings changed", "Growl notification description")
1747            notificationName: GROWL_AUTO_SPEED_LIMIT iconData: nil priority: 0 isSticky: NO clickContext: nil];
1748    }
1749}
1750
1751- (void) setLimitGlobalEnabled: (id) sender
1752{
1753    [fPrefsController applySpeedSettings: nil];
1754}
1755
1756- (void) setQuickLimitGlobal: (id) sender
1757{
1758    [fDefaults setInteger: [[sender title] intValue] forKey: [sender menu] == fUploadMenu ? @"UploadLimit" : @"DownloadLimit"];
1759    [fDefaults setBool: YES forKey: [sender menu] == fUploadMenu ? @"CheckUpload" : @"CheckDownload"];
1760   
1761    [fPrefsController updateLimitFields];
1762    [fPrefsController applySpeedSettings: nil];
1763}
1764
1765- (void) setQuickRatioGlobal: (id) sender
1766{
1767    [fDefaults setBool: YES forKey: @"RatioCheck"];
1768    [fDefaults setFloat: [[sender title] floatValue] forKey: @"RatioLimit"];
1769   
1770    [fPrefsController updateRatioStopField];
1771}
1772
1773- (void) torrentStoppedForRatio: (NSNotification *) notification
1774{
1775    Torrent * torrent = [notification object];
1776   
1777    [self updateTorrentsInQueue];
1778    [fInfoController updateInfoStats];
1779    [fInfoController updateRatioForTorrent: torrent];
1780   
1781    if ([fDefaults boolForKey: @"PlaySeedingSound"])
1782    {
1783        NSSound * sound;
1784        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]]))
1785            [sound play];
1786    }
1787   
1788    NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_SEEDING_COMPLETE, @"Type",
1789                                    [torrent dataLocation], @"Location", nil];
1790    [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Seeding Complete", "Growl notification title")
1791                        description: [torrent name] notificationName: GROWL_SEEDING_COMPLETE
1792                        iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
1793}
1794
1795-(void) watcher: (id<UKFileWatcher>) watcher receivedNotification: (NSString *) notification forPath: (NSString *) path
1796{
1797    if ([notification isEqualToString: UKFileWatcherWriteNotification])
1798        [self checkAutoImportDirectory];
1799}
1800
1801- (void) changeAutoImport
1802{
1803    [fAutoImportedNames removeAllObjects];
1804    [self checkAutoImportDirectory];
1805}
1806
1807- (void) checkAutoImportDirectory
1808{
1809    if (![fDefaults boolForKey: @"AutoImport"])
1810        return;
1811       
1812    NSString * path = [[fDefaults stringForKey: @"AutoImportDirectory"] stringByExpandingTildeInPath];
1813   
1814    NSArray * importedNames;
1815    if (!(importedNames = [[NSFileManager defaultManager] directoryContentsAtPath: path]))
1816        return;
1817   
1818    //only import those that have not been imported yet
1819    NSMutableArray * newNames = [importedNames mutableCopy];
1820    [newNames removeObjectsInArray: fAutoImportedNames];
1821    [fAutoImportedNames setArray: importedNames];
1822   
1823    NSString * file;
1824    int i;
1825    for (i = [newNames count] - 1; i >= 0; i--)
1826    {
1827        file = [newNames objectAtIndex: i];
1828        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
1829            [newNames removeObjectAtIndex: i];
1830        else
1831            [newNames replaceObjectAtIndex: i withObject: [path stringByAppendingPathComponent: file]];
1832    }
1833   
1834    NSEnumerator * enumerator;
1835    if (![[fDefaults stringForKey: @"DownloadChoice"] isEqualToString: @"Ask"])
1836    {
1837        enumerator = [newNames objectEnumerator];
1838        int count;
1839        while ((file = [enumerator nextObject]))
1840        {
1841            count = [fTorrents count];
1842            [self openFiles: [NSArray arrayWithObject: file]];
1843           
1844            //check if torrent was opened
1845            if ([fTorrents count] > count)
1846                [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Torrent File Auto Added",
1847                    "Growl notification title") description: [file lastPathComponent]
1848                    notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO clickContext: nil];
1849        }
1850    }
1851    else
1852        [self openFiles: newNames];
1853   
1854    //create temporary torrents to check if an import fails because of an error
1855    enumerator = [newNames objectEnumerator];
1856    int error;
1857    while ((file = [enumerator nextObject]))
1858    {
1859        tr_torrent_t * tempTor = tr_torrentInit(fLib, [file UTF8String], NULL, 0, &error);
1860       
1861        if (tempTor)
1862            tr_torrentClose(fLib, tempTor);
1863        else if (error != TR_EUNSUPPORTED && error != TR_EDUPLICATE)
1864            [fAutoImportedNames removeObjectIdenticalTo: [file lastPathComponent]]; //can try to import later
1865        else;
1866    }
1867   
1868    [newNames release];
1869}
1870
1871- (int) numberOfRowsInTableView: (NSTableView *) tableview
1872{
1873    return [fDisplayedTorrents count];
1874}
1875
1876- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (int) row
1877{
1878    return [[fDisplayedTorrents objectAtIndex: row] infoForCurrentView];
1879}
1880
1881- (BOOL) tableView: (NSTableView *) tableView writeRowsWithIndexes: (NSIndexSet *) indexes
1882    toPasteboard: (NSPasteboard *) pasteboard
1883{
1884    //only allow reordering of rows if sorting by order with no filter
1885    if ([fSortType isEqualToString: @"Order"] && [fFilterType isEqualToString: @"None"]
1886            && [[fSearchFilterField stringValue] length] == 0)
1887    {
1888        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
1889        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexes]
1890                                forType: TORRENT_TABLE_VIEW_DATA_TYPE];
1891        return YES;
1892    }
1893    return NO;
1894}
1895
1896- (NSDragOperation) tableView: (NSTableView *) t validateDrop: (id <NSDraggingInfo>) info
1897    proposedRow: (int) row proposedDropOperation: (NSTableViewDropOperation) operation
1898{
1899    NSPasteboard * pasteboard = [info draggingPasteboard];
1900    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1901    {
1902        //check if any files to add have "torrent" as an extension
1903        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1904        NSString * file;
1905        while ((file = [enumerator nextObject]))
1906            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1907            {
1908                [fTableView setDropRow: -1 dropOperation: NSTableViewDropOn];
1909                return NSDragOperationGeneric;
1910            }
1911    }
1912    else if ([[pasteboard types] containsObject: NSURLPboardType])
1913    {
1914        [fTableView setDropRow: -1 dropOperation: NSTableViewDropOn];
1915        return NSDragOperationGeneric;
1916    }
1917    else if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
1918    {
1919        [fTableView setDropRow: row dropOperation: NSTableViewDropAbove];
1920        return NSDragOperationGeneric;
1921    }
1922    else;
1923   
1924    return NSDragOperationNone;
1925}
1926
1927- (BOOL) tableView: (NSTableView *) t acceptDrop: (id <NSDraggingInfo>) info
1928    row: (int) newRow dropOperation: (NSTableViewDropOperation) operation
1929{
1930    NSPasteboard * pasteboard = [info draggingPasteboard];
1931    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1932    {
1933        //create an array of files with the "torrent" extension
1934        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
1935        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1936        NSString * file;
1937        while ((file = [enumerator nextObject]))
1938            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1939                [filesToOpen addObject: file];
1940   
1941        [self application: NSApp openFiles: filesToOpen];
1942        [filesToOpen release];
1943    }
1944    else if ([[pasteboard types] containsObject: NSURLPboardType])
1945    {
1946        NSURL * url;
1947        if ((url = [NSURL URLFromPasteboard: pasteboard]))
1948            [self openURL: url];
1949    }
1950    else if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
1951    {
1952        //remember selected rows if needed
1953        NSArray * selectedTorrents = nil;
1954        int numSelected = [fTableView numberOfSelectedRows];
1955        if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
1956            selectedTorrents = [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]];
1957   
1958        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData:
1959                                [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
1960       
1961        //move torrent in array
1962        NSArray * movingTorrents = [[fDisplayedTorrents objectsAtIndexes: indexes] retain];
1963        [fDisplayedTorrents removeObjectsInArray: movingTorrents];
1964       
1965        //determine the insertion index now that transfers to move have been removed
1966        int i, decrease = 0;
1967        for (i = [indexes firstIndex]; i < newRow && i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
1968            decrease++;
1969       
1970        //insert objects at new location
1971        for (i = 0; i < [movingTorrents count]; i++)
1972            [fDisplayedTorrents insertObject: [movingTorrents objectAtIndex: i] atIndex: newRow - decrease + i];
1973       
1974        [movingTorrents release];
1975       
1976        //redo order values
1977        int low = [indexes firstIndex], high = [indexes lastIndex];
1978        if (newRow < low)
1979            low = newRow;
1980        else if (newRow > high + 1)
1981            high = newRow - 1;
1982        else;
1983       
1984        for (i = low; i <= high; i++)
1985            [[fDisplayedTorrents objectAtIndex: i] setOrderValue: i];
1986       
1987        [fTableView reloadData];
1988       
1989        //set selected rows if needed
1990        if (selectedTorrents)
1991        {
1992            Torrent * torrent;
1993            NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1994            NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1995            while ((torrent = [enumerator nextObject]))
1996                [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1997           
1998            [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1999            [indexSet release];
2000        }
2001    }
2002    else;
2003   
2004    return YES;
2005}
2006
2007- (void) tableViewSelectionDidChange: (NSNotification *) notification
2008{
2009    [fInfoController updateInfoForTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
2010}
2011
2012- (void) toggleSmallView: (id) sender
2013{
2014    BOOL makeSmall = [fDefaults boolForKey: @"SmallView"];
2015   
2016    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
2017   
2018    //window min height
2019    NSSize contentMinSize = [fWindow contentMinSize],
2020            contentSize = [[fWindow contentView] frame].size;
2021    contentMinSize.height = contentSize.height - [fScrollView frame].size.height
2022                            + [fTableView rowHeight] + [fTableView intercellSpacing].height;
2023    [fWindow setContentMinSize: contentMinSize];
2024   
2025    //resize for larger min height if not set to auto size
2026    if (![fDefaults boolForKey: @"AutoSize"])
2027    {
2028        if (!makeSmall && contentSize.height < contentMinSize.height)
2029        {
2030            NSRect frame = [fWindow frame];
2031            float heightChange = contentMinSize.height - contentSize.height;
2032            frame.size.height += heightChange;
2033            frame.origin.y -= heightChange;
2034           
2035            [fWindow setFrame: frame display: YES];
2036            [fTableView reloadData];
2037        }
2038    }
2039    else
2040        [self setWindowSizeToFit];
2041}
2042
2043- (void) toggleStatusBar: (id) sender
2044{
2045    [self showStatusBar: [fStatusBar isHidden] animate: YES];
2046    [fDefaults setBool: ![fStatusBar isHidden] forKey: @"StatusBar"];
2047}
2048
2049- (NSRect) windowFrameByAddingHeight: (float) height checkLimits: (BOOL) check
2050{
2051    //convert pixels to points
2052    NSRect windowFrame = [fWindow frame];
2053    NSSize windowSize = [fScrollView convertSize: windowFrame.size fromView: nil];
2054    windowSize.height += height;
2055   
2056    if (check)
2057    {
2058        NSSize minSize = [fScrollView convertSize: [fWindow minSize] fromView: nil];
2059       
2060        if (windowSize.height < minSize.height)
2061            windowSize.height = minSize.height;
2062        else
2063        {
2064            NSSize maxSize = [fScrollView convertSize: [[fWindow screen] visibleFrame].size fromView: nil];
2065            if ([fStatusBar isHidden])
2066                maxSize.height -= [fStatusBar frame].size.height;
2067            if ([fFilterBar isHidden])
2068                maxSize.height -= [fFilterBar frame].size.height;
2069            if (windowSize.height > maxSize.height)
2070                windowSize.height = maxSize.height;
2071        }
2072    }
2073
2074    //convert points to pixels
2075    windowSize = [fScrollView convertSize: windowSize toView: nil];
2076
2077    windowFrame.origin.y -= (windowSize.height - windowFrame.size.height);
2078    windowFrame.size.height = windowSize.height;
2079    return windowFrame;
2080}
2081
2082- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
2083{
2084    if (show != [fStatusBar isHidden])
2085        return;
2086
2087    if (show)
2088        [fStatusBar setHidden: NO];
2089
2090    NSRect frame;
2091    float heightChange = [fStatusBar frame].size.height;
2092    if (!show)
2093        heightChange *= -1;
2094   
2095    //allow bar to show even if not enough room
2096    if (show && ![fDefaults boolForKey: @"AutoSize"])
2097    {
2098        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2099        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2100        if (change < 0.0)
2101        {
2102            frame = [fWindow frame];
2103            frame.size.height += change;
2104            frame.origin.y -= change;
2105            [fWindow setFrame: frame display: NO animate: NO];
2106        }
2107    }
2108
2109    [self updateUI];
2110   
2111    //set views to not autoresize
2112    unsigned int statsMask = [fStatusBar autoresizingMask];
2113    unsigned int filterMask = [fFilterBar autoresizingMask];
2114    unsigned int scrollMask = [fScrollView autoresizingMask];
2115    [fStatusBar setAutoresizingMask: 0];
2116    [fFilterBar setAutoresizingMask: 0];
2117    [fScrollView setAutoresizingMask: 0];
2118   
2119    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2120    [fWindow setFrame: frame display: YES animate: animate];
2121   
2122    //re-enable autoresize
2123    [fStatusBar setAutoresizingMask: statsMask];
2124    [fFilterBar setAutoresizingMask: filterMask];
2125    [fScrollView setAutoresizingMask: scrollMask];
2126   
2127    //change min size
2128    NSSize minSize = [fWindow contentMinSize];
2129    minSize.height += heightChange;
2130    [fWindow setContentMinSize: minSize];
2131   
2132    if (!show)
2133        [fStatusBar setHidden: YES];
2134}
2135
2136- (void) toggleFilterBar: (id) sender
2137{
2138    //disable filtering when hiding
2139    if (![fFilterBar isHidden])
2140    {
2141        [fSearchFilterField setStringValue: @""];
2142        [self setFilter: fNoFilterButton];
2143    }
2144
2145    [self showFilterBar: [fFilterBar isHidden] animate: YES];
2146    [fDefaults setBool: ![fFilterBar isHidden] forKey: @"FilterBar"];
2147}
2148
2149- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
2150{
2151    if (show != [fFilterBar isHidden])
2152        return;
2153
2154    if (show)
2155        [fFilterBar setHidden: NO];
2156
2157    NSRect frame;
2158    float heightChange = [fFilterBar frame].size.height;
2159    if (!show)
2160        heightChange *= -1;
2161   
2162    //allow bar to show even if not enough room
2163    if (show && ![fDefaults boolForKey: @"AutoSize"])
2164    {
2165        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2166        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2167        if (change < 0.0)
2168        {
2169            frame = [fWindow frame];
2170            frame.size.height += change;
2171            frame.origin.y -= change;
2172            [fWindow setFrame: frame display: NO animate: NO];
2173        }
2174    }
2175
2176    //set views to not autoresize
2177    unsigned int filterMask = [fFilterBar autoresizingMask];
2178    unsigned int scrollMask = [fScrollView autoresizingMask];
2179    [fFilterBar setAutoresizingMask: 0];
2180    [fScrollView setAutoresizingMask: 0];
2181   
2182    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2183    [fWindow setFrame: frame display: YES animate: animate];
2184   
2185    //re-enable autoresize
2186    [fFilterBar setAutoresizingMask: filterMask];
2187    [fScrollView setAutoresizingMask: scrollMask];
2188   
2189    //change min size
2190    NSSize minSize = [fWindow contentMinSize];
2191    minSize.height += heightChange;
2192    [fWindow setContentMinSize: minSize];
2193   
2194    if (!show)
2195    {
2196        [fFilterBar setHidden: YES];
2197        [fWindow makeFirstResponder: fTableView];
2198    }
2199}
2200
2201- (void) toggleAdvancedBar: (id) sender
2202{
2203    [fTableView display];
2204}
2205
2206- (void) doNothing: (id) sender {}
2207
2208- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier:
2209    (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
2210{
2211    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
2212
2213    if ([ident isEqualToString: TOOLBAR_OPEN])
2214    {
2215        [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")];
2216        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")];
2217        [item setToolTip: NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip")];
2218        [item setImage: [NSImage imageNamed: @"Open.png"]];
2219        [item setTarget: self];
2220        [item setAction: @selector(openShowSheet:)];
2221        [item setAutovalidates: NO];
2222    }
2223    else if ([ident isEqualToString: TOOLBAR_REMOVE])
2224    {
2225        [item setLabel: NSLocalizedString(@"Remove", "Remove toolbar item -> label")];
2226        [item setPaletteLabel: NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label")];
2227        [item setToolTip: NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip")];
2228        [item setImage: [NSImage imageNamed: @"Remove.png"]];
2229        [item setTarget: self];
2230        [item setAction: @selector(removeNoDelete:)];
2231    }
2232    else if ([ident isEqualToString: TOOLBAR_INFO])
2233    {
2234        [item setLabel: NSLocalizedString(@"Inspector", "Inspector toolbar item -> label")];
2235        [item setPaletteLabel: NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label")];
2236        [item setToolTip: NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip")];
2237        [item setImage: [NSImage imageNamed: @"Info.png"]];
2238        [item setTarget: self];
2239        [item setAction: @selector(showInfo:)];
2240        [item setAutovalidates: NO];
2241    }
2242    else if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
2243    {
2244        [item setLabel: NSLocalizedString(@"Pause All", "Pause All toolbar item -> label")];
2245        [item setPaletteLabel: [item label]];
2246        [item setToolTip: NSLocalizedString(@"Pause all transfers", "Pause All toolbar item -> tooltip")];
2247        [item setImage: [NSImage imageNamed: @"PauseAll.png"]];
2248        [item setTarget: self];
2249        [item setAction: @selector(stopAllTorrents:)];
2250    }
2251    else if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
2252    {
2253        [item setLabel: NSLocalizedString(@"Resume All", "Resume All toolbar item -> label")];
2254        [item setPaletteLabel: [item label]];
2255        [item setToolTip: NSLocalizedString(@"Resume all transfers", "Resume All toolbar item -> tooltip")];
2256        [item setImage: [NSImage imageNamed: @"ResumeAll.png"]];
2257        [item setTarget: self];
2258        [item setAction: @selector(resumeAllTorrents:)];
2259    }
2260    else if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
2261    {
2262        [item setLabel: NSLocalizedString(@"Pause", "Pause toolbar item -> label")];
2263        [item setPaletteLabel: NSLocalizedString(@"Pause Selected", "Pause toolbar item -> palette label")];
2264        [item setToolTip: NSLocalizedString(@"Pause selected transfers", "Pause toolbar item -> tooltip")];
2265        [item setImage: [NSImage imageNamed: @"PauseSelected.png"]];
2266        [item setTarget: self];
2267        [item setAction: @selector(stopSelectedTorrents:)];
2268    }
2269    else if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2270    {
2271        [item setLabel: NSLocalizedString(@"Resume", "Resume toolbar item -> label")];
2272        [item setPaletteLabel: NSLocalizedString(@"Resume Selected", "Resume toolbar item -> palette label")];
2273        [item setToolTip: NSLocalizedString(@"Resume selected transfers", "Resume toolbar item -> tooltip")];
2274        [item setImage: [NSImage imageNamed: @"ResumeSelected.png"]];
2275        [item setTarget: self];
2276        [item setAction: @selector(resumeSelectedTorrents:)];
2277    }
2278    else if ([ident isEqualToString: TOOLBAR_FILTER])
2279    {
2280        [item setLabel: NSLocalizedString(@"Filter", "Filter toolbar item -> label")];
2281        [item setPaletteLabel: NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label")];
2282        [item setToolTip: NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip")];
2283        [item setImage: [NSImage imageNamed: @"Filter.png"]];
2284        [item setTarget: self];
2285        [item setAction: @selector(toggleFilterBar:)];
2286        [item setAutovalidates: NO];
2287    }
2288    else
2289    {
2290        [item release];
2291        return nil;
2292    }
2293
2294    return item;
2295}
2296
2297- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t
2298{
2299    return [NSArray arrayWithObjects:
2300            TOOLBAR_OPEN, TOOLBAR_REMOVE,
2301            TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED,
2302            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, TOOLBAR_FILTER, TOOLBAR_INFO,
2303            NSToolbarSeparatorItemIdentifier,
2304            NSToolbarSpaceItemIdentifier,
2305            NSToolbarFlexibleSpaceItemIdentifier,
2306            NSToolbarCustomizeToolbarItemIdentifier, nil];
2307}
2308
2309- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t
2310{
2311    return [NSArray arrayWithObjects:
2312            TOOLBAR_OPEN, TOOLBAR_REMOVE,
2313            NSToolbarSeparatorItemIdentifier,
2314            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
2315            NSToolbarFlexibleSpaceItemIdentifier,
2316            TOOLBAR_FILTER, TOOLBAR_INFO, nil];
2317}
2318
2319- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
2320{
2321    NSString * ident = [toolbarItem itemIdentifier];
2322
2323    //enable remove item
2324    if ([ident isEqualToString: TOOLBAR_REMOVE])
2325        return [fTableView numberOfSelectedRows] > 0;
2326
2327    //enable pause all item
2328    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
2329    {
2330        Torrent * torrent;
2331        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2332        while ((torrent = [enumerator nextObject]))
2333            if ([torrent isActive] || [torrent waitingToStart])
2334                return YES;
2335        return NO;
2336    }
2337
2338    //enable resume all item
2339    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
2340    {
2341        Torrent * torrent;
2342        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2343        while ((torrent = [enumerator nextObject]))
2344            if ([torrent isPaused] && ![torrent waitingToStart])
2345                return YES;
2346        return NO;
2347    }
2348
2349    //enable pause item
2350    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
2351    {
2352        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2353        Torrent * torrent;
2354        while ((torrent = [enumerator nextObject]))
2355        {
2356            if ([torrent isActive] || [torrent waitingToStart])
2357                return YES;
2358        }
2359        return NO;
2360    }
2361   
2362    //enable resume item
2363    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2364    {
2365        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2366        Torrent * torrent;
2367        while ((torrent = [enumerator nextObject]))
2368        {
2369            if ([torrent isPaused] && ![torrent waitingToStart])
2370                return YES;
2371        }
2372        return NO;
2373    }
2374
2375    return YES;
2376}
2377
2378- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
2379{
2380    SEL action = [menuItem action];
2381
2382    //only enable some items if it is in a context menu or the window is useable
2383    BOOL canUseTable = [fWindow isKeyWindow] || [[menuItem menu] supermenu] != [NSApp mainMenu];
2384
2385    //enable open items
2386    if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:))
2387        return [fWindow attachedSheet] == nil;
2388   
2389    //enable sort and advanced bar items
2390    if (action == @selector(setSort:) || action == @selector(toggleAdvancedBar:) || action == @selector(toggleSmallView:))
2391        return [fWindow isVisible];
2392
2393    //enable show info
2394    if (action == @selector(showInfo:))
2395    {
2396        NSString * title = [[fInfoController window] isVisible] ? NSLocalizedString(@"Hide Inspector",
2397                            "View menu -> Inspector") : NSLocalizedString(@"Show Inspector", "View menu -> Inspector");
2398        if (![[menuItem title] isEqualToString: title])
2399                [menuItem setTitle: title];
2400
2401        return YES;
2402    }
2403   
2404    //enable prev/next inspector tab
2405    if (action == @selector(setInfoTab:))
2406        return [[fInfoController window] isVisible];
2407   
2408    //enable toggle status bar
2409    if (action == @selector(toggleStatusBar:))
2410    {
2411        NSString * title = [fStatusBar isHidden] ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar")
2412                            : NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar");
2413        if (![[menuItem title] isEqualToString: title])
2414            [menuItem setTitle: title];
2415
2416        return [fWindow isVisible];
2417    }
2418   
2419    //enable toggle filter bar
2420    if (action == @selector(toggleFilterBar:))
2421    {
2422        NSString * title = [fFilterBar isHidden] ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar")
2423                            : NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar");
2424        if (![[menuItem title] isEqualToString: title])
2425            [menuItem setTitle: title];
2426
2427        return [fWindow isVisible];
2428    }
2429   
2430    //enable prev/next filter button
2431    if (action == @selector(switchFilter:))
2432        return [fWindow isVisible] && ![fFilterBar isHidden];
2433
2434    //enable reveal in finder
2435    if (action == @selector(revealFile:))
2436        return canUseTable && [fTableView numberOfSelectedRows] > 0;
2437
2438    //enable remove items
2439    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
2440        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteDataAndTorrent:))
2441    {
2442        BOOL warning = NO,
2443            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
2444            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteDataAndTorrent:);
2445       
2446        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2447        Torrent * torrent;
2448        while ((torrent = [enumerator nextObject]))
2449        {
2450            if (!warning && [torrent isActive])
2451            {
2452                warning = onlyDownloading ? ![torrent isSeeding] : YES;
2453                if (warning && canDelete)
2454                    break;
2455            }
2456            if (!canDelete && [torrent publicTorrent])
2457            {
2458                canDelete = YES;
2459                if (warning)
2460                    break;
2461            }
2462        }
2463   
2464        //append or remove ellipsis when needed
2465        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
2466        if (warning && [fDefaults boolForKey: @"CheckRemove"])
2467        {
2468            if (![title hasSuffix: ellipsis])
2469                [menuItem setTitle: [title stringByAppendingEllipsis]];
2470        }
2471        else
2472        {
2473            if ([title hasSuffix: ellipsis])
2474                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
2475        }
2476       
2477        return canUseTable && canDelete && [fTableView numberOfSelectedRows] > 0;
2478    }
2479
2480    //enable pause all item
2481    if (action == @selector(stopAllTorrents:))
2482    {
2483        Torrent * torrent;
2484        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2485        while ((torrent = [enumerator nextObject]))
2486            if ([torrent isActive] || [torrent waitingToStart])
2487                return YES;
2488        return NO;
2489    }
2490   
2491    //enable resume all item
2492    if (action == @selector(resumeAllTorrents:))
2493    {
2494        Torrent * torrent;
2495        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2496        while ((torrent = [enumerator nextObject]))
2497            if ([torrent isPaused] && ![torrent waitingToStart])
2498                return YES;
2499        return NO;
2500    }
2501   
2502    //enable resume all waiting item
2503    if (action == @selector(resumeWaitingTorrents:))
2504    {
2505        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
2506            return NO;
2507   
2508        Torrent * torrent;
2509        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2510        while ((torrent = [enumerator nextObject]))
2511            if ([torrent isPaused] && [torrent waitingToStart])
2512                return YES;
2513        return NO;
2514    }
2515   
2516    //enable resume selected waiting item
2517    if (action == @selector(resumeSelectedTorrentsNoWait:))
2518    {
2519        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
2520            return NO;
2521   
2522        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2523        Torrent * torrent;
2524        while ((torrent = [enumerator nextObject]))
2525            if ([torrent isPaused])
2526                return YES;
2527        return NO;
2528    }
2529
2530    //enable pause item
2531    if (action == @selector(stopSelectedTorrents:))
2532    {
2533        if (!canUseTable)
2534            return NO;
2535   
2536        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2537        Torrent * torrent;
2538        while ((torrent = [enumerator nextObject]))
2539            if ([torrent isActive] || [torrent waitingToStart])
2540                return YES;
2541        return NO;
2542    }
2543   
2544    //enable resume item
2545    if (action == @selector(resumeSelectedTorrents:))
2546    {
2547        if (!canUseTable)
2548            return NO;
2549   
2550        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2551        Torrent * torrent;
2552        while ((torrent = [enumerator nextObject]))
2553            if ([torrent isPaused] && ![torrent waitingToStart])
2554                return YES;
2555        return NO;
2556    }
2557   
2558    //enable announce item
2559    if (action == @selector(announceSelectedTorrents:))
2560    {
2561        if (!canUseTable)
2562            return NO;
2563       
2564        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2565        Torrent * torrent;
2566        NSDate * date;
2567        while ((torrent = [enumerator nextObject]))
2568        {
2569            //time interval returned will be negative
2570            if ([torrent isActive] &&
2571                    (!(date = [torrent announceDate]) || [date timeIntervalSinceNow] <= ANNOUNCE_WAIT_INTERVAL_SECONDS))
2572                return YES;
2573        }
2574        return NO;
2575    }
2576   
2577    //enable copy torrent file item
2578    if (action == @selector(copyTorrentFile:))
2579        return canUseTable && [fTableView numberOfSelectedRows] > 0;
2580
2581    return YES;
2582}
2583
2584- (void) sleepCallBack: (natural_t) messageType argument: (void *) messageArgument
2585{
2586    NSEnumerator * enumerator;
2587    Torrent * torrent;
2588    BOOL active;
2589
2590    switch (messageType)
2591    {
2592        case kIOMessageSystemWillSleep:
2593            //close all connections before going to sleep and remember we should resume when we wake up
2594            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
2595
2596            //wait for running transfers to stop (5 second timeout)
2597            NSDate * start = [NSDate date];
2598            BOOL timeUp = NO;
2599           
2600            NSEnumerator * enumerator = [fTorrents objectEnumerator];
2601            Torrent * torrent;
2602            while (!timeUp && (torrent = [enumerator nextObject]))
2603                while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
2604                {
2605                    usleep(100000);
2606                    [torrent update];
2607                }
2608
2609            IOAllowPowerChange(fRootPort, (long) messageArgument);
2610            break;
2611
2612        case kIOMessageCanSystemSleep:
2613            //pevent idle sleep unless all paused
2614            active = NO;
2615            enumerator = [fTorrents objectEnumerator];
2616            while ((torrent = [enumerator nextObject]))
2617                if ([torrent isActive])
2618                {
2619                    active = YES;
2620                    break;
2621                }
2622
2623            if (active)
2624                IOCancelPowerChange(fRootPort, (long) messageArgument);
2625            else
2626                IOAllowPowerChange(fRootPort, (long) messageArgument);
2627            break;
2628
2629        case kIOMessageSystemHasPoweredOn:
2630            //resume sleeping transfers after we wake up
2631            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
2632            [self autoSpeedLimitChange: nil];
2633            break;
2634    }
2635}
2636
2637- (NSMenu *) applicationDockMenu: (NSApplication *) sender
2638{
2639    int seeding = 0, downloading = 0;
2640    NSEnumerator * enumerator = [fTorrents objectEnumerator];
2641    Torrent * torrent;
2642    while ((torrent = [enumerator nextObject]))
2643        if ([torrent isActive])
2644        {
2645            if ([torrent allDownloaded])
2646                seeding++;
2647            else
2648                downloading++;
2649        }
2650   
2651    NSMenuItem * seedingItem = [fDockMenu itemWithTag: DOCK_SEEDING_TAG],
2652            * downloadingItem = [fDockMenu itemWithTag: DOCK_DOWNLOADING_TAG];
2653   
2654    BOOL hasSeparator = seedingItem || downloadingItem;
2655   
2656    if (seeding > 0)
2657    {
2658        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Seeding",
2659                                                        "Dock item - Seeding"), seeding];
2660        if (!seedingItem)
2661        {
2662            seedingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
2663            [seedingItem setTag: DOCK_SEEDING_TAG];
2664            [fDockMenu insertItem: seedingItem atIndex: 0];
2665        }
2666        else
2667            [seedingItem setTitle: title];
2668    }
2669    else
2670    {
2671        if (seedingItem)
2672            [fDockMenu removeItem: seedingItem];
2673    }
2674   
2675    if (downloading > 0)
2676    {
2677        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Downloading",
2678                                                        "Dock item - Downloading"), downloading];
2679        if (!downloadingItem)
2680        {
2681            downloadingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
2682            [downloadingItem setTag: DOCK_DOWNLOADING_TAG];
2683            [fDockMenu insertItem: downloadingItem atIndex: seeding > 0 ? 1 : 0];
2684        }
2685        else
2686            [downloadingItem setTitle: title];
2687    }
2688    else
2689    {
2690        if (downloadingItem)
2691            [fDockMenu removeItem: downloadingItem];
2692    }
2693   
2694    if (seeding > 0 || downloading > 0)
2695    {
2696        if (!hasSeparator)
2697            [fDockMenu insertItem: [NSMenuItem separatorItem] atIndex: seeding > 0 && downloading > 0 ? 2 : 1];
2698    }
2699    else
2700    {
2701        if (hasSeparator)
2702            [fDockMenu removeItemAtIndex: 0];
2703    }
2704   
2705    return fDockMenu;
2706}
2707
2708- (void) resetDockBadge: (NSNotification *) notification
2709{
2710    float downloadRate, uploadRate;
2711    tr_torrentRates(fLib, & downloadRate, & uploadRate);
2712   
2713    [fBadger updateBadgeWithCompleted: fCompleted uploadRate: uploadRate downloadRate: downloadRate];
2714}
2715
2716- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
2717{
2718    //if auto size is enabled, the current frame shouldn't need to change
2719    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
2720   
2721    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
2722    return frame;
2723}
2724
2725- (void) setWindowSizeToFit
2726{
2727    if ([fDefaults boolForKey: @"AutoSize"])
2728    {
2729        [fScrollView setHasVerticalScroller: NO];
2730        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
2731        [fScrollView setHasVerticalScroller: YES];
2732    }
2733}
2734
2735- (NSRect) sizedWindowFrame
2736{
2737    float heightChange = [fDisplayedTorrents count] * ([fTableView rowHeight] +
2738            [fTableView intercellSpacing].height) - [fScrollView frame].size.height;
2739    return [self windowFrameByAddingHeight: heightChange checkLimits: YES];
2740}
2741
2742- (void) showMainWindow: (id) sender
2743{
2744    [fWindow makeKeyAndOrderFront: nil];
2745}
2746
2747- (void) windowDidBecomeKey: (NSNotification *) notification
2748{
2749    //reset dock badge for completed
2750    if (fCompleted > 0)
2751    {
2752        fCompleted = 0;
2753        [self resetDockBadge: nil];
2754    }
2755}
2756
2757- (NSSize) windowWillResize: (NSWindow *) sender toSize: (NSSize) proposedFrameSize
2758{
2759    //only resize horizontally if autosize is enabled
2760    if ([fDefaults boolForKey: @"AutoSize"])
2761        proposedFrameSize.height = [fWindow frame].size.height;
2762    return proposedFrameSize;
2763}
2764
2765- (void) windowDidResize: (NSNotification *) notification
2766{
2767    //hide search filter if it overlaps filter buttons
2768    [fSearchFilterField setHidden: NSMaxX([fPauseFilterButton frame]) + 2.0 > [fSearchFilterField frame].origin.x];
2769}
2770
2771- (void) linkHomepage: (id) sender
2772{
2773    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
2774}
2775
2776- (void) linkForums: (id) sender
2777{
2778    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
2779}
2780
2781- (void) prepareForUpdate: (NSNotification *) notification
2782{
2783    fUpdateInProgress = YES;
2784}
2785
2786- (NSDictionary *) registrationDictionaryForGrowl
2787{
2788    NSArray * notifications = [NSArray arrayWithObjects: GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE,
2789                                                            GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT, nil];
2790    return [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL,
2791                                notifications, GROWL_NOTIFICATIONS_DEFAULT, nil];
2792}
2793
2794- (void) growlNotificationWasClicked: (id) clickContext
2795{
2796    if (!clickContext || ![clickContext isKindOfClass: [NSDictionary class]])
2797        return;
2798   
2799    NSString * type = [clickContext objectForKey: @"Type"], * location;
2800    if (([type isEqualToString: GROWL_DOWNLOAD_COMPLETE] || [type isEqualToString: GROWL_SEEDING_COMPLETE])
2801            && (location = [clickContext objectForKey: @"Location"]))
2802        [[NSWorkspace sharedWorkspace] selectFile: location inFileViewerRootedAtPath: nil];
2803}
2804
2805- (NSString *) applicationSupportFolder
2806{
2807    return [[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0]
2808                stringByAppendingPathComponent: [[NSProcessInfo processInfo] processName]];
2809}
2810
2811@end
Note: See TracBrowser for help on using the repository browser.