source: trunk/macosx/Controller.m @ 1582

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

don't launch if the os is less then Tiger

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