source: trunk/macosx/Controller.m @ 1707

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

get real date for sort

  • Property svn:keywords set to Date Rev Author Id
File size: 104.4 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 1707 2007-04-12 13:49:01Z 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   
1780    if ([fDefaults boolForKey: @"PlaySeedingSound"])
1781    {
1782        NSSound * sound;
1783        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]]))
1784            [sound play];
1785    }
1786   
1787    NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_SEEDING_COMPLETE, @"Type",
1788                                    [torrent dataLocation], @"Location", nil];
1789    [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Seeding Complete", "Growl notification title")
1790                        description: [torrent name] notificationName: GROWL_SEEDING_COMPLETE
1791                        iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
1792}
1793
1794-(void) watcher: (id<UKFileWatcher>) watcher receivedNotification: (NSString *) notification forPath: (NSString *) path
1795{
1796    if ([notification isEqualToString: UKFileWatcherWriteNotification])
1797        [self checkAutoImportDirectory];
1798}
1799
1800- (void) changeAutoImport
1801{
1802    [fAutoImportedNames removeAllObjects];
1803    [self checkAutoImportDirectory];
1804}
1805
1806- (void) checkAutoImportDirectory
1807{
1808    if (![fDefaults boolForKey: @"AutoImport"])
1809        return;
1810       
1811    NSString * path = [[fDefaults stringForKey: @"AutoImportDirectory"] stringByExpandingTildeInPath];
1812   
1813    NSArray * importedNames;
1814    if (!(importedNames = [[NSFileManager defaultManager] directoryContentsAtPath: path]))
1815        return;
1816   
1817    //only import those that have not been imported yet
1818    NSMutableArray * newNames = [importedNames mutableCopy];
1819    [newNames removeObjectsInArray: fAutoImportedNames];
1820    [fAutoImportedNames setArray: importedNames];
1821   
1822    NSString * file;
1823    int i;
1824    for (i = [newNames count] - 1; i >= 0; i--)
1825    {
1826        file = [newNames objectAtIndex: i];
1827        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
1828            [newNames removeObjectAtIndex: i];
1829        else
1830            [newNames replaceObjectAtIndex: i withObject: [path stringByAppendingPathComponent: file]];
1831    }
1832   
1833    NSEnumerator * enumerator;
1834    if (![[fDefaults stringForKey: @"DownloadChoice"] isEqualToString: @"Ask"])
1835    {
1836        enumerator = [newNames objectEnumerator];
1837        int count;
1838        while ((file = [enumerator nextObject]))
1839        {
1840            count = [fTorrents count];
1841            [self openFiles: [NSArray arrayWithObject: file]];
1842           
1843            //check if torrent was opened
1844            if ([fTorrents count] > count)
1845                [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Torrent File Auto Added",
1846                    "Growl notification title") description: [file lastPathComponent]
1847                    notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO clickContext: nil];
1848        }
1849    }
1850    else
1851        [self openFiles: newNames];
1852   
1853    //create temporary torrents to check if an import fails because of an error
1854    enumerator = [newNames objectEnumerator];
1855    int error;
1856    while ((file = [enumerator nextObject]))
1857    {
1858        tr_torrent_t * tempTor = tr_torrentInit(fLib, [file UTF8String], NULL, 0, &error);
1859       
1860        if (tempTor)
1861            tr_torrentClose(fLib, tempTor);
1862        else if (error != TR_EUNSUPPORTED && error != TR_EDUPLICATE)
1863            [fAutoImportedNames removeObjectIdenticalTo: [file lastPathComponent]]; //can try to import later
1864        else;
1865    }
1866   
1867    [newNames release];
1868}
1869
1870- (int) numberOfRowsInTableView: (NSTableView *) tableview
1871{
1872    return [fDisplayedTorrents count];
1873}
1874
1875- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (int) row
1876{
1877    return [[fDisplayedTorrents objectAtIndex: row] infoForCurrentView];
1878}
1879
1880- (BOOL) tableView: (NSTableView *) tableView writeRowsWithIndexes: (NSIndexSet *) indexes
1881    toPasteboard: (NSPasteboard *) pasteboard
1882{
1883    //only allow reordering of rows if sorting by order with no filter
1884    if ([fSortType isEqualToString: @"Order"] && [fFilterType isEqualToString: @"None"]
1885            && [[fSearchFilterField stringValue] length] == 0)
1886    {
1887        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
1888        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexes]
1889                                forType: TORRENT_TABLE_VIEW_DATA_TYPE];
1890        return YES;
1891    }
1892    return NO;
1893}
1894
1895- (NSDragOperation) tableView: (NSTableView *) t validateDrop: (id <NSDraggingInfo>) info
1896    proposedRow: (int) row proposedDropOperation: (NSTableViewDropOperation) operation
1897{
1898    NSPasteboard * pasteboard = [info draggingPasteboard];
1899    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1900    {
1901        //check if any files to add have "torrent" as an extension
1902        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1903        NSString * file;
1904        while ((file = [enumerator nextObject]))
1905            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1906            {
1907                [fTableView setDropRow: -1 dropOperation: NSTableViewDropOn];
1908                return NSDragOperationGeneric;
1909            }
1910    }
1911    else if ([[pasteboard types] containsObject: NSURLPboardType])
1912    {
1913        [fTableView setDropRow: -1 dropOperation: NSTableViewDropOn];
1914        return NSDragOperationGeneric;
1915    }
1916    else if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
1917    {
1918        [fTableView setDropRow: row dropOperation: NSTableViewDropAbove];
1919        return NSDragOperationGeneric;
1920    }
1921    else;
1922   
1923    return NSDragOperationNone;
1924}
1925
1926- (BOOL) tableView: (NSTableView *) t acceptDrop: (id <NSDraggingInfo>) info
1927    row: (int) newRow dropOperation: (NSTableViewDropOperation) operation
1928{
1929    NSPasteboard * pasteboard = [info draggingPasteboard];
1930    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1931    {
1932        //create an array of files with the "torrent" extension
1933        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
1934        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1935        NSString * file;
1936        while ((file = [enumerator nextObject]))
1937            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1938                [filesToOpen addObject: file];
1939   
1940        [self application: NSApp openFiles: filesToOpen];
1941        [filesToOpen release];
1942    }
1943    else if ([[pasteboard types] containsObject: NSURLPboardType])
1944    {
1945        NSURL * url;
1946        if ((url = [NSURL URLFromPasteboard: pasteboard]))
1947            [self openURL: url];
1948    }
1949    else if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
1950    {
1951        //remember selected rows if needed
1952        NSArray * selectedTorrents = nil;
1953        int numSelected = [fTableView numberOfSelectedRows];
1954        if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
1955            selectedTorrents = [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]];
1956   
1957        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData:
1958                                [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
1959       
1960        //move torrent in array
1961        NSArray * movingTorrents = [[fDisplayedTorrents objectsAtIndexes: indexes] retain];
1962        [fDisplayedTorrents removeObjectsInArray: movingTorrents];
1963       
1964        //determine the insertion index now that transfers to move have been removed
1965        int i, decrease = 0;
1966        for (i = [indexes firstIndex]; i < newRow && i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
1967            decrease++;
1968       
1969        //insert objects at new location
1970        for (i = 0; i < [movingTorrents count]; i++)
1971            [fDisplayedTorrents insertObject: [movingTorrents objectAtIndex: i] atIndex: newRow - decrease + i];
1972       
1973        [movingTorrents release];
1974       
1975        //redo order values
1976        int low = [indexes firstIndex], high = [indexes lastIndex];
1977        if (newRow < low)
1978            low = newRow;
1979        else if (newRow > high + 1)
1980            high = newRow - 1;
1981        else;
1982       
1983        for (i = low; i <= high; i++)
1984            [[fDisplayedTorrents objectAtIndex: i] setOrderValue: i];
1985       
1986        [fTableView reloadData];
1987       
1988        //set selected rows if needed
1989        if (selectedTorrents)
1990        {
1991            Torrent * torrent;
1992            NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1993            NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1994            while ((torrent = [enumerator nextObject]))
1995                [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1996           
1997            [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1998            [indexSet release];
1999        }
2000    }
2001    else;
2002   
2003    return YES;
2004}
2005
2006- (void) tableViewSelectionDidChange: (NSNotification *) notification
2007{
2008    [fInfoController updateInfoForTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
2009}
2010
2011- (void) toggleSmallView: (id) sender
2012{
2013    BOOL makeSmall = [fDefaults boolForKey: @"SmallView"];
2014   
2015    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
2016   
2017    //window min height
2018    NSSize contentMinSize = [fWindow contentMinSize],
2019            contentSize = [[fWindow contentView] frame].size;
2020    contentMinSize.height = contentSize.height - [fScrollView frame].size.height
2021                            + [fTableView rowHeight] + [fTableView intercellSpacing].height;
2022    [fWindow setContentMinSize: contentMinSize];
2023   
2024    //resize for larger min height if not set to auto size
2025    if (![fDefaults boolForKey: @"AutoSize"])
2026    {
2027        if (!makeSmall && contentSize.height < contentMinSize.height)
2028        {
2029            NSRect frame = [fWindow frame];
2030            float heightChange = contentMinSize.height - contentSize.height;
2031            frame.size.height += heightChange;
2032            frame.origin.y -= heightChange;
2033           
2034            [fWindow setFrame: frame display: YES];
2035            [fTableView reloadData];
2036        }
2037    }
2038    else
2039        [self setWindowSizeToFit];
2040}
2041
2042- (void) toggleStatusBar: (id) sender
2043{
2044    [self showStatusBar: [fStatusBar isHidden] animate: YES];
2045    [fDefaults setBool: ![fStatusBar isHidden] forKey: @"StatusBar"];
2046}
2047
2048- (NSRect) windowFrameByAddingHeight: (float) height checkLimits: (BOOL) check
2049{
2050    //convert pixels to points
2051    NSRect windowFrame = [fWindow frame];
2052    NSSize windowSize = [fScrollView convertSize: windowFrame.size fromView: nil];
2053    windowSize.height += height;
2054   
2055    if (check)
2056    {
2057        NSSize minSize = [fScrollView convertSize: [fWindow minSize] fromView: nil];
2058       
2059        if (windowSize.height < minSize.height)
2060            windowSize.height = minSize.height;
2061        else
2062        {
2063            NSSize maxSize = [fScrollView convertSize: [[fWindow screen] visibleFrame].size fromView: nil];
2064            if ([fStatusBar isHidden])
2065                maxSize.height -= [fStatusBar frame].size.height;
2066            if ([fFilterBar isHidden])
2067                maxSize.height -= [fFilterBar frame].size.height;
2068            if (windowSize.height > maxSize.height)
2069                windowSize.height = maxSize.height;
2070        }
2071    }
2072
2073    //convert points to pixels
2074    windowSize = [fScrollView convertSize: windowSize toView: nil];
2075
2076    windowFrame.origin.y -= (windowSize.height - windowFrame.size.height);
2077    windowFrame.size.height = windowSize.height;
2078    return windowFrame;
2079}
2080
2081- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
2082{
2083    if (show != [fStatusBar isHidden])
2084        return;
2085
2086    if (show)
2087        [fStatusBar setHidden: NO];
2088
2089    NSRect frame;
2090    float heightChange = [fStatusBar frame].size.height;
2091    if (!show)
2092        heightChange *= -1;
2093   
2094    //allow bar to show even if not enough room
2095    if (show && ![fDefaults boolForKey: @"AutoSize"])
2096    {
2097        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2098        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2099        if (change < 0.0)
2100        {
2101            frame = [fWindow frame];
2102            frame.size.height += change;
2103            frame.origin.y -= change;
2104            [fWindow setFrame: frame display: NO animate: NO];
2105        }
2106    }
2107
2108    [self updateUI];
2109   
2110    //set views to not autoresize
2111    unsigned int statsMask = [fStatusBar autoresizingMask];
2112    unsigned int filterMask = [fFilterBar autoresizingMask];
2113    unsigned int scrollMask = [fScrollView autoresizingMask];
2114    [fStatusBar setAutoresizingMask: 0];
2115    [fFilterBar setAutoresizingMask: 0];
2116    [fScrollView setAutoresizingMask: 0];
2117   
2118    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2119    [fWindow setFrame: frame display: YES animate: animate];
2120   
2121    //re-enable autoresize
2122    [fStatusBar setAutoresizingMask: statsMask];
2123    [fFilterBar setAutoresizingMask: filterMask];
2124    [fScrollView setAutoresizingMask: scrollMask];
2125   
2126    //change min size
2127    NSSize minSize = [fWindow contentMinSize];
2128    minSize.height += heightChange;
2129    [fWindow setContentMinSize: minSize];
2130   
2131    if (!show)
2132        [fStatusBar setHidden: YES];
2133}
2134
2135- (void) toggleFilterBar: (id) sender
2136{
2137    //disable filtering when hiding
2138    if (![fFilterBar isHidden])
2139    {
2140        [fSearchFilterField setStringValue: @""];
2141        [self setFilter: fNoFilterButton];
2142    }
2143
2144    [self showFilterBar: [fFilterBar isHidden] animate: YES];
2145    [fDefaults setBool: ![fFilterBar isHidden] forKey: @"FilterBar"];
2146}
2147
2148- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
2149{
2150    if (show != [fFilterBar isHidden])
2151        return;
2152
2153    if (show)
2154        [fFilterBar setHidden: NO];
2155
2156    NSRect frame;
2157    float heightChange = [fFilterBar frame].size.height;
2158    if (!show)
2159        heightChange *= -1;
2160   
2161    //allow bar to show even if not enough room
2162    if (show && ![fDefaults boolForKey: @"AutoSize"])
2163    {
2164        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2165        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2166        if (change < 0.0)
2167        {
2168            frame = [fWindow frame];
2169            frame.size.height += change;
2170            frame.origin.y -= change;
2171            [fWindow setFrame: frame display: NO animate: NO];
2172        }
2173    }
2174
2175    //set views to not autoresize
2176    unsigned int filterMask = [fFilterBar autoresizingMask];
2177    unsigned int scrollMask = [fScrollView autoresizingMask];
2178    [fFilterBar setAutoresizingMask: 0];
2179    [fScrollView setAutoresizingMask: 0];
2180   
2181    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2182    [fWindow setFrame: frame display: YES animate: animate];
2183   
2184    //re-enable autoresize
2185    [fFilterBar setAutoresizingMask: filterMask];
2186    [fScrollView setAutoresizingMask: scrollMask];
2187   
2188    //change min size
2189    NSSize minSize = [fWindow contentMinSize];
2190    minSize.height += heightChange;
2191    [fWindow setContentMinSize: minSize];
2192   
2193    if (!show)
2194    {
2195        [fFilterBar setHidden: YES];
2196        [fWindow makeFirstResponder: fTableView];
2197    }
2198}
2199
2200- (void) toggleAdvancedBar: (id) sender
2201{
2202    [fTableView display];
2203}
2204
2205- (void) doNothing: (id) sender {}
2206
2207- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier:
2208    (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
2209{
2210    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
2211
2212    if ([ident isEqualToString: TOOLBAR_OPEN])
2213    {
2214        [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")];
2215        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")];
2216        [item setToolTip: NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip")];
2217        [item setImage: [NSImage imageNamed: @"Open.png"]];
2218        [item setTarget: self];
2219        [item setAction: @selector(openShowSheet:)];
2220        [item setAutovalidates: NO];
2221    }
2222    else if ([ident isEqualToString: TOOLBAR_REMOVE])
2223    {
2224        [item setLabel: NSLocalizedString(@"Remove", "Remove toolbar item -> label")];
2225        [item setPaletteLabel: NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label")];
2226        [item setToolTip: NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip")];
2227        [item setImage: [NSImage imageNamed: @"Remove.png"]];
2228        [item setTarget: self];
2229        [item setAction: @selector(removeNoDelete:)];
2230    }
2231    else if ([ident isEqualToString: TOOLBAR_INFO])
2232    {
2233        [item setLabel: NSLocalizedString(@"Inspector", "Inspector toolbar item -> label")];
2234        [item setPaletteLabel: NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label")];
2235        [item setToolTip: NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip")];
2236        [item setImage: [NSImage imageNamed: @"Info.png"]];
2237        [item setTarget: self];
2238        [item setAction: @selector(showInfo:)];
2239        [item setAutovalidates: NO];
2240    }
2241    else if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
2242    {
2243        [item setLabel: NSLocalizedString(@"Pause All", "Pause All toolbar item -> label")];
2244        [item setPaletteLabel: [item label]];
2245        [item setToolTip: NSLocalizedString(@"Pause all transfers", "Pause All toolbar item -> tooltip")];
2246        [item setImage: [NSImage imageNamed: @"PauseAll.png"]];
2247        [item setTarget: self];
2248        [item setAction: @selector(stopAllTorrents:)];
2249    }
2250    else if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
2251    {
2252        [item setLabel: NSLocalizedString(@"Resume All", "Resume All toolbar item -> label")];
2253        [item setPaletteLabel: [item label]];
2254        [item setToolTip: NSLocalizedString(@"Resume all transfers", "Resume All toolbar item -> tooltip")];
2255        [item setImage: [NSImage imageNamed: @"ResumeAll.png"]];
2256        [item setTarget: self];
2257        [item setAction: @selector(resumeAllTorrents:)];
2258    }
2259    else if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
2260    {
2261        [item setLabel: NSLocalizedString(@"Pause", "Pause toolbar item -> label")];
2262        [item setPaletteLabel: NSLocalizedString(@"Pause Selected", "Pause toolbar item -> palette label")];
2263        [item setToolTip: NSLocalizedString(@"Pause selected transfers", "Pause toolbar item -> tooltip")];
2264        [item setImage: [NSImage imageNamed: @"PauseSelected.png"]];
2265        [item setTarget: self];
2266        [item setAction: @selector(stopSelectedTorrents:)];
2267    }
2268    else if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2269    {
2270        [item setLabel: NSLocalizedString(@"Resume", "Resume toolbar item -> label")];
2271        [item setPaletteLabel: NSLocalizedString(@"Resume Selected", "Resume toolbar item -> palette label")];
2272        [item setToolTip: NSLocalizedString(@"Resume selected transfers", "Resume toolbar item -> tooltip")];
2273        [item setImage: [NSImage imageNamed: @"ResumeSelected.png"]];
2274        [item setTarget: self];
2275        [item setAction: @selector(resumeSelectedTorrents:)];
2276    }
2277    else if ([ident isEqualToString: TOOLBAR_FILTER])
2278    {
2279        [item setLabel: NSLocalizedString(@"Filter", "Filter toolbar item -> label")];
2280        [item setPaletteLabel: NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label")];
2281        [item setToolTip: NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip")];
2282        [item setImage: [NSImage imageNamed: @"Filter.png"]];
2283        [item setTarget: self];
2284        [item setAction: @selector(toggleFilterBar:)];
2285        [item setAutovalidates: NO];
2286    }
2287    else
2288    {
2289        [item release];
2290        return nil;
2291    }
2292
2293    return item;
2294}
2295
2296- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t
2297{
2298    return [NSArray arrayWithObjects:
2299            TOOLBAR_OPEN, TOOLBAR_REMOVE,
2300            TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED,
2301            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, TOOLBAR_FILTER, TOOLBAR_INFO,
2302            NSToolbarSeparatorItemIdentifier,
2303            NSToolbarSpaceItemIdentifier,
2304            NSToolbarFlexibleSpaceItemIdentifier,
2305            NSToolbarCustomizeToolbarItemIdentifier, nil];
2306}
2307
2308- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t
2309{
2310    return [NSArray arrayWithObjects:
2311            TOOLBAR_OPEN, TOOLBAR_REMOVE,
2312            NSToolbarSeparatorItemIdentifier,
2313            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
2314            NSToolbarFlexibleSpaceItemIdentifier,
2315            TOOLBAR_FILTER, TOOLBAR_INFO, nil];
2316}
2317
2318- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
2319{
2320    NSString * ident = [toolbarItem itemIdentifier];
2321
2322    //enable remove item
2323    if ([ident isEqualToString: TOOLBAR_REMOVE])
2324        return [fTableView numberOfSelectedRows] > 0;
2325
2326    //enable pause all item
2327    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
2328    {
2329        Torrent * torrent;
2330        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2331        while ((torrent = [enumerator nextObject]))
2332            if ([torrent isActive] || [torrent waitingToStart])
2333                return YES;
2334        return NO;
2335    }
2336
2337    //enable resume all item
2338    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
2339    {
2340        Torrent * torrent;
2341        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2342        while ((torrent = [enumerator nextObject]))
2343            if ([torrent isPaused] && ![torrent waitingToStart])
2344                return YES;
2345        return NO;
2346    }
2347
2348    //enable pause item
2349    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
2350    {
2351        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2352        Torrent * torrent;
2353        while ((torrent = [enumerator nextObject]))
2354        {
2355            if ([torrent isActive] || [torrent waitingToStart])
2356                return YES;
2357        }
2358        return NO;
2359    }
2360   
2361    //enable resume item
2362    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2363    {
2364        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2365        Torrent * torrent;
2366        while ((torrent = [enumerator nextObject]))
2367        {
2368            if ([torrent isPaused] && ![torrent waitingToStart])
2369                return YES;
2370        }
2371        return NO;
2372    }
2373
2374    return YES;
2375}
2376
2377- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
2378{
2379    SEL action = [menuItem action];
2380
2381    //only enable some items if it is in a context menu or the window is useable
2382    BOOL canUseTable = [fWindow isKeyWindow] || [[menuItem menu] supermenu] != [NSApp mainMenu];
2383
2384    //enable open items
2385    if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:))
2386        return [fWindow attachedSheet] == nil;
2387   
2388    //enable sort and advanced bar items
2389    if (action == @selector(setSort:) || action == @selector(toggleAdvancedBar:) || action == @selector(toggleSmallView:))
2390        return [fWindow isVisible];
2391
2392    //enable show info
2393    if (action == @selector(showInfo:))
2394    {
2395        NSString * title = [[fInfoController window] isVisible] ? NSLocalizedString(@"Hide Inspector",
2396                            "View menu -> Inspector") : NSLocalizedString(@"Show Inspector", "View menu -> Inspector");
2397        if (![[menuItem title] isEqualToString: title])
2398                [menuItem setTitle: title];
2399
2400        return YES;
2401    }
2402   
2403    //enable prev/next inspector tab
2404    if (action == @selector(setInfoTab:))
2405        return [[fInfoController window] isVisible];
2406   
2407    //enable toggle status bar
2408    if (action == @selector(toggleStatusBar:))
2409    {
2410        NSString * title = [fStatusBar isHidden] ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar")
2411                            : NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar");
2412        if (![[menuItem title] isEqualToString: title])
2413            [menuItem setTitle: title];
2414
2415        return [fWindow isVisible];
2416    }
2417   
2418    //enable toggle filter bar
2419    if (action == @selector(toggleFilterBar:))
2420    {
2421        NSString * title = [fFilterBar isHidden] ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar")
2422                            : NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar");
2423        if (![[menuItem title] isEqualToString: title])
2424            [menuItem setTitle: title];
2425
2426        return [fWindow isVisible];
2427    }
2428   
2429    //enable prev/next filter button
2430    if (action == @selector(switchFilter:))
2431        return [fWindow isVisible] && ![fFilterBar isHidden];
2432
2433    //enable reveal in finder
2434    if (action == @selector(revealFile:))
2435        return canUseTable && [fTableView numberOfSelectedRows] > 0;
2436
2437    //enable remove items
2438    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
2439        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteDataAndTorrent:))
2440    {
2441        BOOL warning = NO,
2442            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
2443            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteDataAndTorrent:);
2444       
2445        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2446        Torrent * torrent;
2447        while ((torrent = [enumerator nextObject]))
2448        {
2449            if (!warning && [torrent isActive])
2450            {
2451                warning = onlyDownloading ? ![torrent isSeeding] : YES;
2452                if (warning && canDelete)
2453                    break;
2454            }
2455            if (!canDelete && [torrent publicTorrent])
2456            {
2457                canDelete = YES;
2458                if (warning)
2459                    break;
2460            }
2461        }
2462   
2463        //append or remove ellipsis when needed
2464        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
2465        if (warning && [fDefaults boolForKey: @"CheckRemove"])
2466        {
2467            if (![title hasSuffix: ellipsis])
2468                [menuItem setTitle: [title stringByAppendingEllipsis]];
2469        }
2470        else
2471        {
2472            if ([title hasSuffix: ellipsis])
2473                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
2474        }
2475       
2476        return canUseTable && canDelete && [fTableView numberOfSelectedRows] > 0;
2477    }
2478
2479    //enable pause all item
2480    if (action == @selector(stopAllTorrents:))
2481    {
2482        Torrent * torrent;
2483        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2484        while ((torrent = [enumerator nextObject]))
2485            if ([torrent isActive] || [torrent waitingToStart])
2486                return YES;
2487        return NO;
2488    }
2489   
2490    //enable resume all item
2491    if (action == @selector(resumeAllTorrents:))
2492    {
2493        Torrent * torrent;
2494        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2495        while ((torrent = [enumerator nextObject]))
2496            if ([torrent isPaused] && ![torrent waitingToStart])
2497                return YES;
2498        return NO;
2499    }
2500   
2501    //enable resume all waiting item
2502    if (action == @selector(resumeWaitingTorrents:))
2503    {
2504        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
2505            return NO;
2506   
2507        Torrent * torrent;
2508        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2509        while ((torrent = [enumerator nextObject]))
2510            if ([torrent isPaused] && [torrent waitingToStart])
2511                return YES;
2512        return NO;
2513    }
2514   
2515    //enable resume selected waiting item
2516    if (action == @selector(resumeSelectedTorrentsNoWait:))
2517    {
2518        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
2519            return NO;
2520   
2521        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2522        Torrent * torrent;
2523        while ((torrent = [enumerator nextObject]))
2524            if ([torrent isPaused])
2525                return YES;
2526        return NO;
2527    }
2528
2529    //enable pause item
2530    if (action == @selector(stopSelectedTorrents:))
2531    {
2532        if (!canUseTable)
2533            return NO;
2534   
2535        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2536        Torrent * torrent;
2537        while ((torrent = [enumerator nextObject]))
2538            if ([torrent isActive] || [torrent waitingToStart])
2539                return YES;
2540        return NO;
2541    }
2542   
2543    //enable resume item
2544    if (action == @selector(resumeSelectedTorrents:))
2545    {
2546        if (!canUseTable)
2547            return NO;
2548   
2549        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2550        Torrent * torrent;
2551        while ((torrent = [enumerator nextObject]))
2552            if ([torrent isPaused] && ![torrent waitingToStart])
2553                return YES;
2554        return NO;
2555    }
2556   
2557    //enable announce item
2558    if (action == @selector(announceSelectedTorrents:))
2559    {
2560        if (!canUseTable)
2561            return NO;
2562       
2563        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2564        Torrent * torrent;
2565        NSDate * date;
2566        while ((torrent = [enumerator nextObject]))
2567        {
2568            //time interval returned will be negative
2569            if ([torrent isActive] &&
2570                    (!(date = [torrent announceDate]) || [date timeIntervalSinceNow] <= ANNOUNCE_WAIT_INTERVAL_SECONDS))
2571                return YES;
2572        }
2573        return NO;
2574    }
2575   
2576    //enable copy torrent file item
2577    if (action == @selector(copyTorrentFile:))
2578        return canUseTable && [fTableView numberOfSelectedRows] > 0;
2579
2580    return YES;
2581}
2582
2583- (void) sleepCallBack: (natural_t) messageType argument: (void *) messageArgument
2584{
2585    NSEnumerator * enumerator;
2586    Torrent * torrent;
2587    BOOL active;
2588
2589    switch (messageType)
2590    {
2591        case kIOMessageSystemWillSleep:
2592            //close all connections before going to sleep and remember we should resume when we wake up
2593            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
2594
2595            //wait for running transfers to stop (5 second timeout)
2596            NSDate * start = [NSDate date];
2597            BOOL timeUp = NO;
2598           
2599            NSEnumerator * enumerator = [fTorrents objectEnumerator];
2600            Torrent * torrent;
2601            while (!timeUp && (torrent = [enumerator nextObject]))
2602                while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
2603                {
2604                    usleep(100000);
2605                    [torrent update];
2606                }
2607
2608            IOAllowPowerChange(fRootPort, (long) messageArgument);
2609            break;
2610
2611        case kIOMessageCanSystemSleep:
2612            //pevent idle sleep unless all paused
2613            active = NO;
2614            enumerator = [fTorrents objectEnumerator];
2615            while ((torrent = [enumerator nextObject]))
2616                if ([torrent isActive])
2617                {
2618                    active = YES;
2619                    break;
2620                }
2621
2622            if (active)
2623                IOCancelPowerChange(fRootPort, (long) messageArgument);
2624            else
2625                IOAllowPowerChange(fRootPort, (long) messageArgument);
2626            break;
2627
2628        case kIOMessageSystemHasPoweredOn:
2629            //resume sleeping transfers after we wake up
2630            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
2631            break;
2632    }
2633}
2634
2635- (NSMenu *) applicationDockMenu: (NSApplication *) sender
2636{
2637    int seeding = 0, downloading = 0;
2638    NSEnumerator * enumerator = [fTorrents objectEnumerator];
2639    Torrent * torrent;
2640    while ((torrent = [enumerator nextObject]))
2641        if ([torrent isActive])
2642        {
2643            if ([torrent allDownloaded])
2644                seeding++;
2645            else
2646                downloading++;
2647        }
2648   
2649    NSMenuItem * seedingItem = [fDockMenu itemWithTag: DOCK_SEEDING_TAG],
2650            * downloadingItem = [fDockMenu itemWithTag: DOCK_DOWNLOADING_TAG];
2651   
2652    BOOL hasSeparator = seedingItem || downloadingItem;
2653   
2654    if (seeding > 0)
2655    {
2656        NSString * title = [NSString stringWithFormat: @"%d Seeding", seeding];
2657        if (!seedingItem)
2658        {
2659            seedingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
2660            [seedingItem setTag: DOCK_SEEDING_TAG];
2661            [fDockMenu insertItem: seedingItem atIndex: 0];
2662        }
2663        else
2664            [seedingItem setTitle: title];
2665    }
2666    else
2667    {
2668        if (seedingItem)
2669            [fDockMenu removeItem: seedingItem];
2670    }
2671   
2672    if (downloading > 0)
2673    {
2674        NSString * title = [NSString stringWithFormat: @"%d Downloading", downloading];
2675        if (!downloadingItem)
2676        {
2677            downloadingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
2678            [downloadingItem setTag: DOCK_DOWNLOADING_TAG];
2679            [fDockMenu insertItem: downloadingItem atIndex: seeding > 0 ? 1 : 0];
2680        }
2681        else
2682            [downloadingItem setTitle: title];
2683    }
2684    else
2685    {
2686        if (downloadingItem)
2687            [fDockMenu removeItem: downloadingItem];
2688    }
2689   
2690    if (seeding > 0 || downloading > 0)
2691    {
2692        if (!hasSeparator)
2693            [fDockMenu insertItem: [NSMenuItem separatorItem] atIndex: seeding > 0 && downloading > 0 ? 2 : 1];
2694    }
2695    else
2696    {
2697        if (hasSeparator)
2698            [fDockMenu removeItemAtIndex: 0];
2699    }
2700   
2701    return fDockMenu;
2702}
2703
2704- (void) resetDockBadge: (NSNotification *) notification
2705{
2706    float downloadRate, uploadRate;
2707    tr_torrentRates(fLib, & downloadRate, & uploadRate);
2708   
2709    [fBadger updateBadgeWithCompleted: fCompleted uploadRate: uploadRate downloadRate: downloadRate];
2710}
2711
2712- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
2713{
2714    //if auto size is enabled, the current frame shouldn't need to change
2715    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
2716   
2717    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
2718    return frame;
2719}
2720
2721- (void) setWindowSizeToFit
2722{
2723    if ([fDefaults boolForKey: @"AutoSize"])
2724    {
2725        [fScrollView setHasVerticalScroller: NO];
2726        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
2727        [fScrollView setHasVerticalScroller: YES];
2728    }
2729}
2730
2731- (NSRect) sizedWindowFrame
2732{
2733    float heightChange = [fDisplayedTorrents count] * ([fTableView rowHeight] +
2734            [fTableView intercellSpacing].height) - [fScrollView frame].size.height;
2735    return [self windowFrameByAddingHeight: heightChange checkLimits: YES];
2736}
2737
2738- (void) showMainWindow: (id) sender
2739{
2740    [fWindow makeKeyAndOrderFront: nil];
2741}
2742
2743- (void) windowDidBecomeKey: (NSNotification *) notification
2744{
2745    //reset dock badge for completed
2746    if (fCompleted > 0)
2747    {
2748        fCompleted = 0;
2749        [self resetDockBadge: nil];
2750    }
2751}
2752
2753- (NSSize) windowWillResize: (NSWindow *) sender toSize: (NSSize) proposedFrameSize
2754{
2755    //only resize horizontally if autosize is enabled
2756    if ([fDefaults boolForKey: @"AutoSize"])
2757        proposedFrameSize.height = [fWindow frame].size.height;
2758    return proposedFrameSize;
2759}
2760
2761- (void) windowDidResize: (NSNotification *) notification
2762{
2763    //hide search filter if it overlaps filter buttons
2764    [fSearchFilterField setHidden: NSMaxX([fPauseFilterButton frame]) + 2.0 > [fSearchFilterField frame].origin.x];
2765}
2766
2767- (void) linkHomepage: (id) sender
2768{
2769    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
2770}
2771
2772- (void) linkForums: (id) sender
2773{
2774    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
2775}
2776
2777- (void) prepareForUpdate: (NSNotification *) notification
2778{
2779    fUpdateInProgress = YES;
2780}
2781
2782- (NSDictionary *) registrationDictionaryForGrowl
2783{
2784    NSArray * notifications = [NSArray arrayWithObjects: GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE,
2785                                                            GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT, nil];
2786    return [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL,
2787                                notifications, GROWL_NOTIFICATIONS_DEFAULT, nil];
2788}
2789
2790- (void) growlNotificationWasClicked: (id) clickContext
2791{
2792    if (!clickContext || ![clickContext isKindOfClass: [NSDictionary class]])
2793        return;
2794   
2795    NSString * type = [clickContext objectForKey: @"Type"], * location;
2796    if (([type isEqualToString: GROWL_DOWNLOAD_COMPLETE] || [type isEqualToString: GROWL_SEEDING_COMPLETE])
2797            && (location = [clickContext objectForKey: @"Location"]))
2798        [[NSWorkspace sharedWorkspace] selectFile: location inFileViewerRootedAtPath: nil];
2799}
2800
2801- (NSString *) applicationSupportFolder
2802{
2803    return [[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0]
2804                stringByAppendingPathComponent: [[NSProcessInfo processInfo] processName]];
2805}
2806
2807@end
Note: See TracBrowser for help on using the repository browser.