source: trunk/macosx/Controller.m @ 3194

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

use LSMinimumSystemVersion instead of custom version check, because on < 10.4 it will never reach that custom code

  • Property svn:keywords set to Date Rev Author Id
File size: 126.1 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 3194 2007-09-26 23:49:21Z livings124 $
3 *
4 * Copyright (c) 2005-2007 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#import <IOKit/IOMessage.h>
26
27#import "Controller.h"
28#import "Torrent.h"
29#import "TorrentCell.h"
30#import "TorrentTableView.h"
31#import "CreatorWindowController.h"
32#import "AboutWindowController.h"
33#import "NSStringAdditions.h"
34#import "UKKQueue.h"
35#import "ActionMenuSpeedToDisplayLimitTransformer.h"
36#import "ActionMenuRatioToDisplayRatioTransformer.h"
37#import "ExpandedPathToPathTransformer.h"
38#import "ExpandedPathToIconTransformer.h"
39#import "SpeedLimitToTurtleIconTransformer.h"
40
41#import <Sparkle/Sparkle.h>
42
43#define TOOLBAR_CREATE          @"Toolbar Create"
44#define TOOLBAR_OPEN            @"Toolbar Open"
45#define TOOLBAR_REMOVE          @"Toolbar Remove"
46#define TOOLBAR_INFO            @"Toolbar Info"
47#define TOOLBAR_PAUSE_ALL       @"Toolbar Pause All"
48#define TOOLBAR_RESUME_ALL      @"Toolbar Resume All"
49#define TOOLBAR_PAUSE_SELECTED  @"Toolbar Pause Selected"
50#define TOOLBAR_RESUME_SELECTED @"Toolbar Resume Selected"
51#define TOOLBAR_FILTER          @"Toolbar Toggle Filter"
52
53#define SORT_DATE       @"Date"
54#define SORT_NAME       @"Name"
55#define SORT_STATE      @"State"
56#define SORT_PROGRESS   @"Progress"
57#define SORT_TRACKER    @"Tracker"
58#define SORT_ORDER      @"Order"
59
60#define FILTER_NONE     @"None"
61#define FILTER_DOWNLOAD @"Download"
62#define FILTER_SEED     @"Seed"
63#define FILTER_PAUSE    @"Pause"
64
65#define FILTER_TYPE_NAME    @"Name"
66#define FILTER_TYPE_TRACKER @"Tracker"
67
68#define FILTER_TYPE_TAG_NAME    401
69#define FILTER_TYPE_TAG_TRACKER 402
70
71#define GROWL_DOWNLOAD_COMPLETE @"Download Complete"
72#define GROWL_SEEDING_COMPLETE  @"Seeding Complete"
73#define GROWL_AUTO_ADD          @"Torrent Auto Added"
74#define GROWL_AUTO_SPEED_LIMIT  @"Speed Limit Auto Changed"
75
76#define TORRENT_TABLE_VIEW_DATA_TYPE    @"TorrentTableViewDataType"
77
78#define ROW_HEIGHT_REGULAR      65.0
79#define ROW_HEIGHT_SMALL        40.0
80#define WINDOW_REGULAR_WIDTH    468.0
81
82#define SEARCH_FILTER_MIN_WIDTH 55.0
83#define SEARCH_FILTER_MAX_WIDTH 115.0
84
85#define UPDATE_UI_SECONDS           1.0
86#define AUTO_SPEED_LIMIT_SECONDS    5.0
87
88#define DOCK_SEEDING_TAG        101
89#define DOCK_DOWNLOADING_TAG    102
90
91#define WEBSITE_URL @"http://transmission.m0k.org/"
92#define FORUM_URL   @"http://transmission.m0k.org/forum/"
93#define DONATE_URL   @"http://transmission.m0k.org/donate.php"
94
95void sleepCallBack(void * controller, io_service_t y, natural_t messageType, void * messageArgument)
96{
97    [(Controller *)controller sleepCallBack: messageType argument: messageArgument];
98}
99
100@implementation Controller
101
102+ (void) initialize
103{
104    //make sure another Transmission.app isn't running already
105    NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
106    int processIdentifier = [[NSProcessInfo processInfo] processIdentifier];
107
108    NSDictionary * dic;
109    NSEnumerator * enumerator = [[[NSWorkspace sharedWorkspace] launchedApplications] objectEnumerator];
110    while ((dic = [enumerator nextObject]))
111    {
112        if ([[dic objectForKey: @"NSApplicationBundleIdentifier"] isEqualToString: bundleIdentifier]
113            && [[dic objectForKey: @"NSApplicationProcessIdentifier"] intValue] != processIdentifier)
114        {
115            NSAlert * alert = [[NSAlert alloc] init];
116            [alert addButtonWithTitle: NSLocalizedString(@"Quit", "Transmission already running alert -> button")];
117            [alert setMessageText: NSLocalizedString(@"Transmission is already running.",
118                                                    "Transmission already running alert -> title")];
119            [alert setInformativeText: NSLocalizedString(@"There is already a copy of Transmission running. "
120                "This copy cannot be opened until that instance is quit.", "Transmission already running alert -> message")];
121            [alert setAlertStyle: NSWarningAlertStyle];
122           
123            [alert runModal];
124            [alert release];
125           
126            //kill ourselves right away
127            exit(0);
128        }
129    }
130   
131    [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithContentsOfFile:
132        [[NSBundle mainBundle] pathForResource: @"Defaults" ofType: @"plist"]]];
133   
134    //set custom value transformers
135    ActionMenuSpeedToDisplayLimitTransformer * limitTransformer =
136                        [[[ActionMenuSpeedToDisplayLimitTransformer alloc] init] autorelease];
137    [NSValueTransformer setValueTransformer: limitTransformer forName: @"ActionMenuSpeedToDisplayLimitTransformer"];
138   
139    ActionMenuRatioToDisplayRatioTransformer * ratioTransformer =
140                        [[[ActionMenuRatioToDisplayRatioTransformer alloc] init] autorelease];
141    [NSValueTransformer setValueTransformer: ratioTransformer forName: @"ActionMenuRatioToDisplayRatioTransformer"];
142   
143    ExpandedPathToPathTransformer * pathTransformer =
144                        [[[ExpandedPathToPathTransformer alloc] init] autorelease];
145    [NSValueTransformer setValueTransformer: pathTransformer forName: @"ExpandedPathToPathTransformer"];
146   
147    ExpandedPathToIconTransformer * iconTransformer =
148                        [[[ExpandedPathToIconTransformer alloc] init] autorelease];
149    [NSValueTransformer setValueTransformer: iconTransformer forName: @"ExpandedPathToIconTransformer"];
150   
151    SpeedLimitToTurtleIconTransformer * speedLimitIconTransformer =
152                        [[[SpeedLimitToTurtleIconTransformer alloc] init] autorelease];
153    [NSValueTransformer setValueTransformer: speedLimitIconTransformer forName: @"SpeedLimitToTurtleIconTransformer"];
154}
155
156- (id) init
157{
158    if ((self = [super init]))
159    {
160        fLib = tr_init("macosx");
161       
162        [NSApp setDelegate: self];
163       
164        fTorrents = [[NSMutableArray alloc] init];
165        fDisplayedTorrents = [[NSMutableArray alloc] init];
166       
167        fDefaults = [NSUserDefaults standardUserDefaults];
168       
169        fMessageController = [[MessageWindowController alloc] initWithWindowNibName: @"MessageWindow"];
170        fInfoController = [[InfoWindowController alloc] initWithWindowNibName: @"InfoWindow"];
171        fPrefsController = [[PrefsController alloc] initWithWindowNibName: @"PrefsWindow" handle: fLib];
172       
173        fBadger = [[Badger alloc] initWithLib: fLib];
174       
175        fIPCController = [[IPCController alloc] init];
176        [fIPCController setDelegate: self];
177        fRemoteQuit = NO;
178       
179        [GrowlApplicationBridge setGrowlDelegate: self];
180        [[UKKQueue sharedFileWatcher] setDelegate: self];
181    }
182    return self;
183}
184
185- (void) dealloc
186{
187    [[NSNotificationCenter defaultCenter] removeObserver: self];
188   
189    [fInfoController release];
190    [fMessageController release];
191    [fPrefsController release];
192   
193    [fTorrents release];
194    [fDisplayedTorrents release];
195   
196    [fBadger release];
197    [fOverlayWindow release];
198    [fIPCController release];
199   
200    [fAutoImportedNames release];
201    [fPendingTorrentDownloads release];
202    [fTempTorrentFiles release];
203   
204    tr_close(fLib);
205    [super dealloc];
206}
207
208- (void) awakeFromNib
209{
210    [fBottomBar setBackgroundImage: [NSImage imageNamed: @"BottomBorder.png"]];
211   
212    NSToolbar * toolbar = [[NSToolbar alloc] initWithIdentifier: @"Transmission Toolbar"];
213    [toolbar setDelegate: self];
214    [toolbar setAllowsUserCustomization: YES];
215    [toolbar setAutosavesConfiguration: YES];
216    [fWindow setToolbar: toolbar];
217    [toolbar release];
218   
219    [fWindow setDelegate: self]; //do manually to avoid placement issue
220   
221    [fWindow makeFirstResponder: fTableView];
222    [fWindow setExcludedFromWindowsMenu: YES];
223    [fWindow setAcceptsMouseMovedEvents: YES]; //ensure filter buttons display correctly
224   
225    //set table size
226    if ([fDefaults boolForKey: @"SmallView"])
227        [fTableView setRowHeight: ROW_HEIGHT_SMALL];
228   
229    //window min height
230    NSSize contentMinSize = [fWindow contentMinSize];
231    contentMinSize.height = [[fWindow contentView] frame].size.height - [fScrollView frame].size.height
232                                + [fTableView rowHeight] + [fTableView intercellSpacing].height;
233    [fWindow setContentMinSize: contentMinSize];
234   
235    //set info and filter keyboard shortcuts
236    unichar rightChar = NSRightArrowFunctionKey, leftChar = NSLeftArrowFunctionKey;
237    [fNextInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & rightChar length: 1]];
238    [fPrevInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & leftChar length: 1]];
239   
240    [fNextFilterItem setKeyEquivalent: [NSString stringWithCharacters: & rightChar length: 1]];
241    [fNextFilterItem setKeyEquivalentModifierMask: NSCommandKeyMask | NSAlternateKeyMask];
242    [fPrevFilterItem setKeyEquivalent: [NSString stringWithCharacters: & leftChar length: 1]];
243    [fPrevFilterItem setKeyEquivalentModifierMask: NSCommandKeyMask | NSAlternateKeyMask];
244   
245    //set up filter bar
246    NSView * contentView = [fWindow contentView];
247    NSSize windowSize = [contentView convertSize: [fWindow frame].size fromView: nil];
248    [fFilterBar setHidden: YES];
249   
250    NSRect filterBarFrame = [fFilterBar frame];
251    filterBarFrame.size.width = windowSize.width;
252    [fFilterBar setFrame: filterBarFrame];
253   
254    [contentView addSubview: fFilterBar];
255    [fFilterBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
256
257    [self showFilterBar: [fDefaults boolForKey: @"FilterBar"] animate: NO];
258   
259    //set up status bar
260    [fStatusBar setHidden: YES];
261   
262    NSRect statusBarFrame = [fStatusBar frame];
263    statusBarFrame.size.width = windowSize.width;
264    [fStatusBar setFrame: statusBarFrame];
265
266    [contentView addSubview: fStatusBar];
267    [fStatusBar setFrameOrigin: NSMakePoint(0, NSMaxY([contentView frame]))];
268    [self showStatusBar: [fDefaults boolForKey: @"StatusBar"] animate: NO];
269
270    [fActionButton setToolTip: NSLocalizedString(@"Shortcuts for changing global settings.",
271                                "Main window -> 1st bottom left button (action) tooltip")];
272    [fSpeedLimitButton setToolTip: NSLocalizedString(@"Speed Limit overrides the total bandwidth limits with its own limits.",
273                                "Main window -> 2nd bottom left button (turtle) tooltip")];
274   
275    [fPrefsController setUpdater: fUpdater];
276   
277    [fTableView setTorrents: fDisplayedTorrents];
278    [[fTableView tableColumnWithIdentifier: @"Torrent"] setDataCell: [[TorrentCell alloc] init]];
279
280    [fTableView registerForDraggedTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE]];
281    [fWindow registerForDraggedTypes: [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]];
282
283    //register for sleep notifications
284    IONotificationPortRef notify;
285    io_object_t iterator;
286    if ((fRootPort = IORegisterForSystemPower(self, & notify, sleepCallBack, & iterator)) != 0)
287        CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify), kCFRunLoopCommonModes);
288    else
289        NSLog(@"Could not IORegisterForSystemPower");
290   
291    //load previous transfers
292    NSArray * history = [[NSArray alloc] initWithContentsOfFile:
293        [NSHomeDirectory() stringByAppendingPathComponent: @"/Library/Application Support/Transmission/Transfers.plist"]];
294   
295    //old version saved transfer info in prefs file
296    if (!history)
297    {
298        if ((history = [fDefaults arrayForKey: @"History"]))
299            [history retain];
300        [fDefaults removeObjectForKey: @"History"];
301    }
302   
303    if (history)
304    {
305        Torrent * torrent;
306        NSDictionary * historyItem;
307        NSEnumerator * enumerator = [history objectEnumerator];
308        while ((historyItem = [enumerator nextObject]))
309            if ((torrent = [[Torrent alloc] initWithHistory: historyItem lib: fLib]))
310            {
311                [fTorrents addObject: torrent];
312                [torrent release];
313            }
314       
315        [history release];
316    }
317   
318    //set sort
319    NSString * sortType = [fDefaults stringForKey: @"Sort"];
320   
321    NSMenuItem * currentSortItem, * currentSortActionItem;
322    if ([sortType isEqualToString: SORT_NAME])
323    {
324        currentSortItem = fNameSortItem;
325        currentSortActionItem = fNameSortActionItem;
326    }
327    else if ([sortType isEqualToString: SORT_STATE])
328    {
329        currentSortItem = fStateSortItem;
330        currentSortActionItem = fStateSortActionItem;
331    }
332    else if ([sortType isEqualToString: SORT_PROGRESS])
333    {
334        currentSortItem = fProgressSortItem;
335        currentSortActionItem = fProgressSortActionItem;
336    }
337    else if ([sortType isEqualToString: SORT_TRACKER])
338    {
339        currentSortItem = fTrackerSortItem;
340        currentSortActionItem = fTrackerSortActionItem;
341    }
342    else if ([sortType isEqualToString: SORT_ORDER])
343    {
344        currentSortItem = fOrderSortItem;
345        currentSortActionItem = fOrderSortActionItem;
346    }
347    else
348    {
349        //safety
350        if (![sortType isEqualToString: SORT_DATE])
351            [fDefaults setObject: SORT_DATE forKey: @"Sort"];
352        currentSortItem = fDateSortItem;
353        currentSortActionItem = fDateSortActionItem;
354    }
355    [currentSortItem setState: NSOnState];
356    [currentSortActionItem setState: NSOnState];
357   
358    //set filter
359    NSString * filterType = [fDefaults stringForKey: @"Filter"];
360   
361    FilterBarButton * currentFilterButton;
362    if ([filterType isEqualToString: FILTER_PAUSE])
363        currentFilterButton = fPauseFilterButton;
364    else if ([filterType isEqualToString: FILTER_SEED])
365        currentFilterButton = fSeedFilterButton;
366    else if ([filterType isEqualToString: FILTER_DOWNLOAD])
367        currentFilterButton = fDownloadFilterButton;
368    else
369    {
370        //safety
371        if (![filterType isEqualToString: FILTER_NONE])
372            [fDefaults setObject: FILTER_NONE forKey: @"Filter"];
373        currentFilterButton = fNoFilterButton;
374    }
375    [currentFilterButton setState: NSOnState];
376   
377    //set filter search type
378    NSString * filterSearchType = [fDefaults stringForKey: @"FilterSearchType"];
379   
380    NSMenu * filterSearchMenu = [[fSearchFilterField cell] searchMenuTemplate];
381    NSString * filterSearchTypeTitle;
382    if ([filterSearchType isEqualToString: FILTER_TYPE_TRACKER])
383        filterSearchTypeTitle = [[filterSearchMenu itemWithTag: FILTER_TYPE_TAG_TRACKER] title];
384    else
385    {
386        //safety
387        if (![filterType isEqualToString: FILTER_TYPE_NAME])
388            [fDefaults setObject: FILTER_TYPE_NAME forKey: @"FilterSearchType"];
389        filterSearchTypeTitle = [[filterSearchMenu itemWithTag: FILTER_TYPE_TAG_NAME] title];
390    }
391    [[fSearchFilterField cell] setPlaceholderString: filterSearchTypeTitle];
392   
393    //observe notifications
394    NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
395   
396    [nc addObserver: self selector: @selector(updateUI)
397                    name: @"UpdateUI" object: nil];
398   
399    [nc addObserver: fInfoController selector: @selector(updateInfoSettings)
400                    name: @"UpdateInfoSettings" object: nil];
401   
402    [nc addObserver: self selector: @selector(torrentFinishedDownloading:)
403                    name: @"TorrentFinishedDownloading" object: nil];
404   
405    [nc addObserver: self selector: @selector(torrentRestartedDownloading:)
406                    name: @"TorrentRestartedDownloading" object: nil];
407   
408    //avoids need of setting delegate
409    [nc addObserver: self selector: @selector(torrentTableViewSelectionDidChange:)
410                    name: NSTableViewSelectionDidChangeNotification object: fTableView];
411   
412    [nc addObserver: self selector: @selector(updateControlTint:)
413                    name: NSControlTintDidChangeNotification object: nil];
414   
415    [nc addObserver: self selector: @selector(prepareForUpdate:)
416                    name: SUUpdaterWillRestartNotification object: nil];
417    fUpdateInProgress = NO;
418   
419    [nc addObserver: self selector: @selector(autoSpeedLimitChange:)
420                    name: @"AutoSpeedLimitChange" object: nil];
421   
422    [nc addObserver: self selector: @selector(changeAutoImport)
423                    name: @"AutoImportSettingChange" object: nil];
424   
425    [nc addObserver: self selector: @selector(setWindowSizeToFit)
426                    name: @"AutoSizeSettingChange" object: nil];
427   
428    [nc addObserver: fWindow selector: @selector(makeKeyWindow)
429                    name: @"MakeWindowKey" object: nil];
430   
431    //check if torrent should now start
432    [nc addObserver: self selector: @selector(torrentStoppedForRatio:)
433                    name: @"TorrentStoppedForRatio" object: nil];
434   
435    [nc addObserver: self selector: @selector(updateTorrentsInQueue)
436                    name: @"UpdateQueue" object: nil];
437   
438    //change that just impacts the dock badge
439    [nc addObserver: self selector: @selector(updateDockBadge:)
440                    name: @"DockBadgeChange" object: nil];
441   
442    //open newly created torrent file
443    [nc addObserver: self selector: @selector(beginCreateFile:)
444                    name: @"BeginCreateTorrentFile" object: nil];
445   
446    //open newly created torrent file
447    [nc addObserver: self selector: @selector(openCreatedFile:)
448                    name: @"OpenCreatedTorrentFile" object: nil];
449
450    //timer to update the interface every second
451    [self updateUI];
452    fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_UI_SECONDS target: self
453        selector: @selector(updateUI) userInfo: nil repeats: YES];
454    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSModalPanelRunLoopMode];
455    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSEventTrackingRunLoopMode];
456   
457    [self applyFilter: nil];
458   
459    [fWindow makeKeyAndOrderFront: nil];
460   
461    if ([fDefaults boolForKey: @"InfoVisible"])
462        [self showInfo: nil];
463   
464    //timer to auto toggle speed limit
465    [self autoSpeedLimitChange: nil];
466    fSpeedLimitTimer = [NSTimer scheduledTimerWithTimeInterval: AUTO_SPEED_LIMIT_SECONDS target: self
467        selector: @selector(autoSpeedLimit) userInfo: nil repeats: YES];
468}
469
470- (void) applicationDidFinishLaunching: (NSNotification *) notification
471{
472    [NSApp setServicesProvider: self];
473   
474    //register for dock icon drags
475    [[NSAppleEventManager sharedAppleEventManager] setEventHandler: self
476        andSelector: @selector(handleOpenContentsEvent:replyEvent:)
477        forEventClass: kCoreEventClass andEventID: kAEOpenContents];
478   
479    //auto importing
480    [self checkAutoImportDirectory];
481   
482    //debug warning
483    if ([fDefaults boolForKey: @"WarningDebug"] && [fDefaults integerForKey: @"MessageLevel"] == TR_MSG_DBG)
484    {
485        NSAlert * alert = [[NSAlert alloc] init];
486        [alert setMessageText: NSLocalizedString(@"The Message Log is set to \"Debug\"", "Debug log alert -> message")];
487        [alert setInformativeText: NSLocalizedString(@"Continuous use at this level might increase memory usage."
488                                    " This setting can be changed in the Message Log window (accessible from the Window menu).",
489                                    "Debug log alert -> informative message")];
490        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Debug log alert -> button")];
491        [alert addButtonWithTitle: NSLocalizedString(@"Don't Alert Again", "Debug log alert -> button")];
492       
493        if ([alert runModal] == NSAlertSecondButtonReturn)
494            [fDefaults setBool: NO forKey: @"WarningDebug"];
495        [alert release];
496    }
497}
498
499- (BOOL) applicationShouldHandleReopen: (NSApplication *) app hasVisibleWindows: (BOOL) visibleWindows
500{
501    if (![fWindow isVisible] && ![[fPrefsController window] isVisible])
502        [fWindow makeKeyAndOrderFront: nil];
503    return NO;
504}
505
506- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) sender
507{
508    if (!fUpdateInProgress && !fRemoteQuit && [fDefaults boolForKey: @"CheckQuit"])
509    {
510        int active = 0, downloading = 0;
511        Torrent * torrent;
512        NSEnumerator * enumerator = [fTorrents objectEnumerator];
513        while ((torrent = [enumerator nextObject]))
514            if ([torrent isActive])
515            {
516                active++;
517                if (![torrent isSeeding])
518                    downloading++;
519            }
520
521        if ([fDefaults boolForKey: @"CheckQuitDownloading"] ? downloading > 0 : active > 0)
522        {
523            NSString * message = active == 1
524                ? NSLocalizedString(@"There is an active transfer. Do you really want to quit?",
525                    "Confirm Quit panel -> message")
526                : [NSString stringWithFormat: NSLocalizedString(@"There are %d active transfers. Do you really want to quit?",
527                    "Confirm Quit panel -> message"), active];
528
529            NSBeginAlertSheet(NSLocalizedString(@"Confirm Quit", "Confirm Quit panel -> title"),
530                                NSLocalizedString(@"Quit", "Confirm Quit panel -> button"),
531                                NSLocalizedString(@"Cancel", "Confirm Quit panel -> button"), nil, fWindow, self,
532                                @selector(quitSheetDidEnd:returnCode:contextInfo:), nil, nil, message);
533            return NSTerminateLater;
534        }
535    }
536
537    return NSTerminateNow;
538}
539
540- (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo
541{
542    [NSApp replyToApplicationShouldTerminate: returnCode == NSAlertDefaultReturn];
543}
544
545- (void) applicationWillTerminate: (NSNotification *) notification
546{
547    //remove all torrent downloads
548    if (fPendingTorrentDownloads)
549    {
550        NSEnumerator * downloadEnumerator = [[fPendingTorrentDownloads allValues] objectEnumerator];
551        NSDictionary * downloadDict;
552        NSURLDownload * download;
553        while ((downloadDict = [downloadEnumerator nextObject]))
554        {
555            download = [downloadDict objectForKey: @"Download"];
556            [download cancel];
557            [download release];
558        }
559        [fPendingTorrentDownloads removeAllObjects];
560    }
561   
562    //remove all remaining torrent files in the temporary directory
563    if (fTempTorrentFiles)
564    {
565        NSEnumerator * torrentEnumerator = [fTempTorrentFiles objectEnumerator];
566        NSString * path;
567        while ((path = [torrentEnumerator nextObject]))
568            [[NSFileManager defaultManager] removeFileAtPath: path handler: nil];
569        [fTempTorrentFiles removeAllObjects];
570    }
571   
572    //stop timers
573    [fTimer invalidate];
574    [fSpeedLimitTimer invalidate];
575    if (fAutoImportTimer)
576    {   
577        if ([fAutoImportTimer isValid])
578            [fAutoImportTimer invalidate];
579        [fAutoImportTimer release];
580    }
581   
582    //remember window states and close all windows
583    [fDefaults setBool: [[fInfoController window] isVisible] forKey: @"InfoVisible"];
584    [[NSApp windows] makeObjectsPerformSelector: @selector(close)];
585    [self showStatusBar: NO animate: NO];
586    [self showFilterBar: NO animate: NO];
587   
588    //save history
589    [self updateTorrentHistory];
590   
591    //make sure torrents are closed
592    NSEnumerator * enumerator = [fTorrents objectEnumerator];
593    Torrent * torrent;
594    while ((torrent = [enumerator nextObject]))
595        [torrent closeTorrent];
596   
597    [fDisplayedTorrents removeAllObjects];
598    [fTorrents removeAllObjects];
599   
600    //disable NAT traversal
601    tr_natTraversalEnable(fLib, 0);
602   
603    //clear badge
604    [fBadger clearBadge];
605
606    //wait for running transfers to stop and for NAT to be disabled (5 second timeout)
607    NSDate * start = [NSDate date];
608    while ([start timeIntervalSinceNow] >= -5.0
609            && (tr_torrentCount(fLib) > 0 || tr_handleStatus(fLib)->natTraversalStatus != TR_NAT_TRAVERSAL_DISABLED))
610        usleep(100000);
611}
612
613- (void) handleOpenContentsEvent: (NSAppleEventDescriptor *) event replyEvent: (NSAppleEventDescriptor *) replyEvent
614{
615    NSString * urlString = nil;
616
617    NSAppleEventDescriptor * directObject = [event paramDescriptorForKeyword: keyDirectObject];
618    if ([directObject descriptorType] == typeAEList)
619    {
620        unsigned i;
621        for (i = 1; i <= [directObject numberOfItems]; i++)
622            if ((urlString = [[directObject descriptorAtIndex: i] stringValue]))
623                break;
624    }
625    else
626        urlString = [directObject stringValue];
627   
628    if (urlString)
629        [self openURL: [[[NSURL alloc] initWithString: urlString] autorelease]];
630}
631
632- (void) download: (NSURLDownload *) download decideDestinationWithSuggestedFilename: (NSString *) suggestedName
633{
634    if ([[suggestedName pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
635    {
636        [download cancel];
637       
638        NSRunAlertPanel(NSLocalizedString(@"Torrent download failed",
639            @"Download not a torrent -> title"), [NSString stringWithFormat:
640            NSLocalizedString(@"It appears that the file \"%@\" from %@ is not a torrent file.",
641            @"Download not a torrent -> message"), suggestedName,
642            [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]],
643            NSLocalizedString(@"OK", @"Download not a torrent -> button"), nil, nil);
644       
645        [download release];
646    }
647    else
648        [download setDestination: [NSTemporaryDirectory() stringByAppendingPathComponent: [suggestedName lastPathComponent]]
649                    allowOverwrite: NO];
650}
651
652-(void) download: (NSURLDownload *) download didCreateDestination: (NSString *) path
653{
654    if (!fPendingTorrentDownloads)
655        fPendingTorrentDownloads = [[NSMutableDictionary alloc] init];
656   
657    [fPendingTorrentDownloads setObject: [NSDictionary dictionaryWithObjectsAndKeys:
658                    path, @"Path", download, @"Download", nil] forKey: [[download request] URL]];
659}
660
661- (void) download: (NSURLDownload *) download didFailWithError: (NSError *) error
662{
663    NSRunAlertPanel(NSLocalizedString(@"Torrent download failed",
664        @"Torrent download error -> title"), [NSString stringWithFormat:
665        NSLocalizedString(@"The torrent could not be downloaded from %@ because an error occurred (%@).",
666        @"Torrent download failed -> message"),
667        [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding],
668        [error localizedDescription]], NSLocalizedString(@"OK", @"Torrent download failed -> button"), nil, nil);
669   
670    [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]];
671    [download release];
672}
673
674- (void) downloadDidFinish: (NSURLDownload *) download
675{
676    NSString * path = [[fPendingTorrentDownloads objectForKey: [[download request] URL]] objectForKey: @"Path"];
677   
678    [self openFiles: [NSArray arrayWithObject: path] forcePath: nil ignoreDownloadFolder:
679        ![[fDefaults stringForKey: @"DownloadChoice"] isEqualToString: @"Constant"] deleteTorrentFile: TORRENT_FILE_DELETE];
680   
681    [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]];
682    [download release];
683   
684    //delete temp torrent file on quit
685    if (!fTempTorrentFiles)
686        fTempTorrentFiles = [[NSMutableArray alloc] init];
687    [fTempTorrentFiles addObject: path];
688}
689
690- (void) application: (NSApplication *) app openFiles: (NSArray *) filenames
691{
692    [self openFiles: filenames forcePath: nil ignoreDownloadFolder: NO deleteTorrentFile: TORRENT_FILE_DEFAULT];
693}
694
695- (void) openFiles: (NSArray *) filenames forcePath: (NSString *) path ignoreDownloadFolder: (BOOL) ignore
696        deleteTorrentFile: (torrentFileState) deleteTorrent
697{
698    NSString * downloadChoice = [fDefaults stringForKey: @"DownloadChoice"];
699    if (ignore || (!path && [downloadChoice isEqualToString: @"Ask"]))
700    {
701        [self openFilesAsk: [filenames mutableCopy] deleteTorrentFile: deleteTorrent];
702        return;
703    }
704   
705    #warning make submethod
706    if (!path && [fDefaults boolForKey: @"UseIncompleteDownloadFolder"]
707        && access([[[fDefaults stringForKey: @"IncompleteDownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0))
708    {
709        NSOpenPanel * panel = [NSOpenPanel openPanel];
710       
711        [panel setPrompt: NSLocalizedString(@"Select", "Default incomplete folder cannot be used alert -> prompt")];
712        [panel setAllowsMultipleSelection: NO];
713        [panel setCanChooseFiles: NO];
714        [panel setCanChooseDirectories: YES];
715        [panel setCanCreateDirectories: YES];
716
717        [panel setMessage: NSLocalizedString(@"The incomplete folder cannot be used. Choose a new location or cancel for none.",
718                                        "Default incomplete folder cannot be used alert -> message")];
719       
720        NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
721                                        filenames, @"Filenames",
722                                        [NSNumber numberWithBool: ignore], @"Ignore",
723                                        [NSNumber numberWithInt: deleteTorrent], @"DeleteTorrent", nil];
724       
725        [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
726                didEndSelector: @selector(incompleteChoiceClosed:returnCode:contextInfo:) contextInfo: dict];
727        return;
728    }
729    if (!path && [downloadChoice isEqualToString: @"Constant"]
730        && access([[[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0))
731    {
732        NSOpenPanel * panel = [NSOpenPanel openPanel];
733       
734        [panel setPrompt: NSLocalizedString(@"Select", "Default folder cannot be used alert -> prompt")];
735        [panel setAllowsMultipleSelection: NO];
736        [panel setCanChooseFiles: NO];
737        [panel setCanChooseDirectories: YES];
738        [panel setCanCreateDirectories: YES];
739
740        [panel setMessage: NSLocalizedString(@"The download folder cannot be used. Choose a new location.",
741                                        "Default folder cannot be used alert -> message")];
742       
743        NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
744                                        filenames, @"Filenames",
745                                        [NSNumber numberWithBool: ignore], @"Ignore",
746                                        [NSNumber numberWithInt: deleteTorrent], @"DeleteTorrent", nil];
747       
748        [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
749                didEndSelector: @selector(downloadChoiceClosed:returnCode:contextInfo:) contextInfo: dict];
750        return;
751    }
752   
753    Torrent * torrent;
754    NSString * torrentPath;
755    tr_info info;
756    NSEnumerator * enumerator = [filenames objectEnumerator];
757    while ((torrentPath = [enumerator nextObject]))
758    {
759        NSString * location;
760        if (path)
761            location = path;
762        else if ([downloadChoice isEqualToString: @"Constant"])
763            location = [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath];
764        else
765            location = [torrentPath stringByDeletingLastPathComponent];
766       
767        if (tr_torrentParse(fLib, [torrentPath UTF8String], NULL, &info) == TR_EDUPLICATE)
768        {
769            [self duplicateOpenAlert: [NSString stringWithUTF8String: info.name]];
770            tr_metainfoFree(&info);
771            continue;
772        }
773        tr_metainfoFree(&info);
774       
775        if (!(torrent = [[Torrent alloc] initWithPath: torrentPath location: location deleteTorrentFile: deleteTorrent lib: fLib]))
776            continue;
777       
778        //add it to the "File > Open Recent" menu
779        [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
780       
781        [torrent update];
782        [fTorrents addObject: torrent];
783        [torrent release];
784    }
785
786    [self updateTorrentsInQueue];
787}
788
789- (void) openCreatedFile: (NSNotification *) notification
790{
791    NSDictionary * dict = [notification userInfo];
792    [self openFiles: [NSArray arrayWithObject: [dict objectForKey: @"File"]] forcePath: [dict objectForKey: @"Path"]
793            ignoreDownloadFolder: NO deleteTorrentFile: TORRENT_FILE_SAVE];
794    [dict release];
795}
796
797- (void) incompleteChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code contextInfo: (NSDictionary *) dictionary
798{
799    if (code == NSOKButton)
800        [fDefaults setObject: [[openPanel filenames] objectAtIndex: 0] forKey: @"IncompleteDownloadFolder"];
801    else
802        [fDefaults setBool: NO forKey: @"UseIncompleteDownloadFolder"];
803   
804    [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO];
805}
806
807- (void) downloadChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code contextInfo: (NSDictionary *) dictionary
808{
809    if (code == NSOKButton)
810    {
811        [fDefaults setObject: [[openPanel filenames] objectAtIndex: 0] forKey: @"DownloadFolder"];
812        [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO];
813    }
814    else
815        [dictionary release];
816}
817
818- (void) openFilesWithDict: (NSDictionary *) dictionary
819{
820    [self openFiles: [dictionary objectForKey: @"Filenames"] forcePath: nil
821        ignoreDownloadFolder: [[dictionary objectForKey: @"Ignore"] boolValue]
822        deleteTorrentFile: [[dictionary objectForKey: @"DeleteTorrent"] intValue]];
823   
824    [dictionary release];
825}
826
827//called by the main open method to show sheet for choosing download location
828- (void) openFilesAsk: (NSMutableArray *) files deleteTorrentFile: (torrentFileState) deleteTorrent
829{
830    //determine the next file that can be opened
831    NSString * torrentPath;
832    int canAdd;
833    tr_info info;
834    while ([files count] > 0)
835    {
836        torrentPath = [[files objectAtIndex: 0] retain];
837        canAdd = tr_torrentParse(fLib, [torrentPath UTF8String], NULL, &info);
838        if (canAdd == TR_OK)
839            break;
840        else if (canAdd == TR_EDUPLICATE)
841            [self duplicateOpenAlert: [NSString stringWithUTF8String: info.name]];
842        else;
843       
844        tr_metainfoFree(&info);
845        [files removeObjectAtIndex: 0];
846    }
847   
848    //no files left to open
849    if ([files count] <= 0)
850    {
851        [files release];
852        [self updateTorrentHistory];
853        return;
854    }
855   
856    [files removeObjectAtIndex: 0];
857   
858    NSOpenPanel * panel = [NSOpenPanel openPanel];
859
860    [panel setPrompt: NSLocalizedString(@"Select", "Open torrent -> prompt")];
861    [panel setAllowsMultipleSelection: NO];
862    [panel setCanChooseFiles: NO];
863    [panel setCanChooseDirectories: YES];
864    [panel setCanCreateDirectories: YES];
865   
866    [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the download folder for \"%@\"",
867                        "Open torrent -> select destination folder"), [NSString stringWithUTF8String: info.name]]];
868    tr_metainfoFree(&info);
869   
870    NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys: torrentPath, @"Path",
871                                    files, @"Files", [NSNumber numberWithInt: deleteTorrent], @"DeleteTorrent", nil];
872    [torrentPath release];
873   
874    [panel beginSheetForDirectory: nil file: nil types: nil modalForWindow: fWindow modalDelegate: self
875            didEndSelector: @selector(folderChoiceClosed:returnCode:contextInfo:) contextInfo: dictionary];
876}
877
878- (void) folderChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code contextInfo: (NSDictionary *) dictionary
879{
880    if (code == NSOKButton)
881    {
882        NSString * torrentPath = [dictionary objectForKey: @"Path"];
883        Torrent * torrent = [[Torrent alloc] initWithPath: torrentPath location: [[openPanel filenames] objectAtIndex: 0]
884                            deleteTorrentFile: [[dictionary objectForKey: @"DeleteTorrent"] intValue] lib: fLib];
885       
886        //add it to the "File > Open Recent" menu
887        [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
888       
889        [torrent update];
890        [fTorrents addObject: torrent];
891        [torrent release];
892       
893        [self updateTorrentsInQueue];
894    }
895   
896    [self performSelectorOnMainThread: @selector(openFilesAskWithDict:) withObject: dictionary waitUntilDone: NO];
897}
898
899- (void) openFilesAskWithDict: (NSDictionary *) dictionary
900{
901    [self openFilesAsk: [dictionary objectForKey: @"Files"]
902            deleteTorrentFile: [[dictionary objectForKey: @"DeleteTorrent"] intValue]];
903    [dictionary release];
904}
905
906//called on by applescript
907- (void) open: (NSArray *) files
908{
909    [self performSelectorOnMainThread: @selector(openFiles:) withObject: files waitUntilDone: NO];
910}
911
912- (void) openFiles: (NSArray *) filenames
913{
914    [self openFiles: filenames forcePath: nil ignoreDownloadFolder: NO deleteTorrentFile: TORRENT_FILE_DEFAULT];
915}
916
917- (void) openShowSheet: (id) sender
918{
919    NSOpenPanel * panel = [NSOpenPanel openPanel];
920
921    [panel setAllowsMultipleSelection: YES];
922    [panel setCanChooseFiles: YES];
923    [panel setCanChooseDirectories: NO];
924
925    [panel beginSheetForDirectory: nil file: nil types: [NSArray arrayWithObject: @"torrent"]
926        modalForWindow: fWindow modalDelegate: self didEndSelector: @selector(openSheetClosed:returnCode:contextInfo:)
927        contextInfo: [NSNumber numberWithBool: sender == fOpenIgnoreDownloadFolder]];
928}
929
930- (void) openSheetClosed: (NSOpenPanel *) panel returnCode: (int) code contextInfo: (NSNumber *) ignore
931{
932    if (code == NSOKButton)
933    {
934        NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:
935                                        [panel filenames], @"Files", ignore, @"Ignore", nil];
936        [self performSelectorOnMainThread: @selector(openFromSheet:) withObject: dictionary waitUntilDone: NO];
937    }
938}
939
940- (void) openFromSheet: (NSDictionary *) dictionary
941{
942    [self openFiles: [dictionary objectForKey: @"Files"] forcePath: nil
943        ignoreDownloadFolder: [[dictionary objectForKey: @"Ignore"] boolValue] deleteTorrentFile: TORRENT_FILE_DEFAULT];
944   
945    [dictionary release];
946}
947
948- (void) duplicateOpenAlert: (NSString *) name
949{
950    if (![fDefaults boolForKey: @"WarningDuplicate"])
951        return;
952   
953    NSAlert * alert = [[NSAlert alloc] init];
954    [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"A transfer of \"%@\" is already running.",
955                            "Open duplicate alert -> title"), name]];
956    [alert setInformativeText:
957            NSLocalizedString(@"The torrent file cannot be opened because it is a duplicate of an already running transfer.",
958                            "Open duplicate alert -> message")];
959    [alert addButtonWithTitle: NSLocalizedString(@"OK", "Open duplicate alert -> button")];
960    [alert addButtonWithTitle: NSLocalizedString(@"Don't Alert Again", "Open duplicate alert -> button")];
961    [alert setAlertStyle: NSWarningAlertStyle];
962   
963    if ([alert runModal] == NSAlertSecondButtonReturn)
964        [fDefaults setBool: NO forKey: @"WarningDuplicate"];
965    [alert release];
966}
967
968- (void) openURL: (NSURL *) url
969{
970    [[NSURLDownload alloc] initWithRequest: [NSURLRequest requestWithURL: url] delegate: self];
971}
972
973- (void) openURLShowSheet: (id) sender
974{
975    [NSApp beginSheet: fURLSheetWindow modalForWindow: fWindow modalDelegate: self
976            didEndSelector: @selector(urlSheetDidEnd:returnCode:contextInfo:) contextInfo: nil];
977}
978
979- (void) openURLEndSheet: (id) sender
980{
981    [fURLSheetWindow orderOut: sender];
982    [NSApp endSheet: fURLSheetWindow returnCode: 1];
983}
984
985- (void) openURLCancelEndSheet: (id) sender
986{
987    [fURLSheetWindow orderOut: sender];
988    [NSApp endSheet: fURLSheetWindow returnCode: 0];
989}
990
991- (void) urlSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo
992{
993    [fURLSheetTextField selectText: self];
994    if (returnCode != 1)
995        return;
996   
997    NSString * urlString = [fURLSheetTextField stringValue];
998    if (![urlString isEqualToString: @""])
999    {
1000        if ([urlString rangeOfString: @"://"].location == NSNotFound)
1001        {
1002            if ([urlString rangeOfString: @"."].location == NSNotFound)
1003            {
1004                int beforeCom;
1005                if ((beforeCom = [urlString rangeOfString: @"/"].location) != NSNotFound)
1006                    urlString = [NSString stringWithFormat: @"http://www.%@.com/%@",
1007                                    [urlString substringToIndex: beforeCom],
1008                                    [urlString substringFromIndex: beforeCom + 1]];
1009                else
1010                    urlString = [NSString stringWithFormat: @"http://www.%@.com", urlString];
1011            }
1012            else
1013                urlString = [@"http://" stringByAppendingString: urlString];
1014        }
1015       
1016        NSURL * url = [NSURL URLWithString: urlString];
1017        [self performSelectorOnMainThread: @selector(openURL:) withObject: url waitUntilDone: NO];
1018    }
1019}
1020
1021- (void) createFile: (id) sender
1022{
1023    [CreatorWindowController createTorrentFile: fLib];
1024}
1025
1026- (void) resumeSelectedTorrents: (id) sender
1027{
1028    [self resumeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
1029}
1030
1031- (void) resumeAllTorrents: (id) sender
1032{
1033    [self resumeTorrents: fTorrents];
1034}
1035
1036- (void) resumeTorrents: (NSArray *) torrents
1037{
1038    NSEnumerator * enumerator = [torrents objectEnumerator];
1039    Torrent * torrent;
1040    while ((torrent = [enumerator nextObject]))
1041        [torrent setWaitToStart: YES];
1042   
1043    [self updateTorrentsInQueue];
1044}
1045
1046- (void) resumeSelectedTorrentsNoWait:  (id) sender
1047{
1048    [self resumeTorrentsNoWait: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
1049}
1050
1051- (void) resumeWaitingTorrents: (id) sender
1052{
1053    NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [fTorrents count]];
1054   
1055    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1056    Torrent * torrent;
1057    while ((torrent = [enumerator nextObject]))
1058        if (![torrent isActive] && [torrent waitingToStart])
1059            [torrents addObject: torrent];
1060   
1061    [self resumeTorrentsNoWait: torrents];
1062}
1063
1064- (void) resumeTorrentsNoWait: (NSArray *) torrents
1065{
1066    //iterate through instead of all at once to ensure no conflicts
1067    NSEnumerator * enumerator = [torrents objectEnumerator];
1068    Torrent * torrent;
1069    while ((torrent = [enumerator nextObject]))
1070        [torrent startTransfer];
1071   
1072    [self updateUI];
1073    [self applyFilter: nil];
1074    [self updateTorrentHistory];
1075}
1076
1077- (void) stopSelectedTorrents: (id) sender
1078{
1079    [self stopTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
1080}
1081
1082- (void) stopAllTorrents: (id) sender
1083{
1084    [self stopTorrents: fTorrents];
1085}
1086
1087- (void) stopTorrents: (NSArray *) torrents
1088{
1089    //don't want any of these starting then stopping
1090    NSEnumerator * enumerator = [torrents objectEnumerator];
1091    Torrent * torrent;
1092    while ((torrent = [enumerator nextObject]))
1093        [torrent setWaitToStart: NO];
1094
1095    [torrents makeObjectsPerformSelector: @selector(stopTransfer)];
1096   
1097    [self updateUI];
1098    [self applyFilter: nil];
1099    [self updateTorrentHistory];
1100}
1101
1102- (void) removeTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
1103{
1104    [torrents retain];
1105    int active = 0, downloading = 0;
1106
1107    if ([fDefaults boolForKey: @"CheckRemove"])
1108    {
1109        Torrent * torrent;
1110        NSEnumerator * enumerator = [torrents objectEnumerator];
1111        while ((torrent = [enumerator nextObject]))
1112            if ([torrent isActive])
1113            {
1114                active++;
1115                if (![torrent isSeeding])
1116                    downloading++;
1117            }
1118
1119        if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? downloading > 0 : active > 0)
1120        {
1121            NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
1122                torrents, @"Torrents",
1123                [NSNumber numberWithBool: deleteData], @"DeleteData",
1124                [NSNumber numberWithBool: deleteTorrent], @"DeleteTorrent", nil];
1125
1126            NSString * title, * message;
1127           
1128            int selected = [fTableView numberOfSelectedRows];
1129            if (selected == 1)
1130            {
1131                NSString * torrentName = [[fDisplayedTorrents objectAtIndex: [fTableView selectedRow]] name];
1132               
1133                if (!deleteData && !deleteTorrent)
1134                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of \"%@\" from the transfer list.",
1135                                "Removal confirm panel -> title"), torrentName];
1136                else if (deleteData && !deleteTorrent)
1137                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of \"%@\" from the transfer list"
1138                                " and trash data file.", "Removal confirm panel -> title"), torrentName];
1139                else if (!deleteData && deleteTorrent)
1140                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of \"%@\" from the transfer list"
1141                                " and trash torrent file.", "Removal confirm panel -> title"), torrentName];
1142                else
1143                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of \"%@\" from the transfer list"
1144                            " and trash both data and torrent files.", "Removal confirm panel -> title"), torrentName];
1145               
1146                message = NSLocalizedString(@"This transfer is active."
1147                            " Once removed, continuing the transfer will require the torrent file."
1148                            " Do you really want to remove it?", "Removal confirm panel -> message");
1149            }
1150            else
1151            {
1152                if (!deleteData && !deleteTorrent)
1153                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of %d transfers"
1154                                " from the transfer list.", "Removal confirm panel -> title"), selected];
1155                else if (deleteData && !deleteTorrent)
1156                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of %d transfers"
1157                                " from the transfer list and trash data file.", "Removal confirm panel -> title"), selected];
1158                else if (!deleteData && deleteTorrent)
1159                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of %d transfers"
1160                                " from the transfer list and trash torrent file.", "Removal confirm panel -> title"), selected];
1161                else
1162                    title = [NSString stringWithFormat: NSLocalizedString(@"Confirm removal of %d transfers"
1163                                " from the transfer list and trash both data and torrent files.",
1164                                "Removal confirm panel -> title"), selected];
1165               
1166                if (selected == active)
1167                    message = [NSString stringWithFormat: NSLocalizedString(@"There are %d active transfers.",
1168                                "Removal confirm panel -> message part 1"), active];
1169                else
1170                    message = [NSString stringWithFormat: NSLocalizedString(@"There are %d transfers (%d active).",
1171                                "Removal confirm panel -> message part 1"), selected, active];
1172                message = [message stringByAppendingString:
1173                    NSLocalizedString(@" Once removed, continuing the transfers will require the torrent files."
1174                    " Do you really want to remove them?", "Removal confirm panel -> message part 2")];
1175            }
1176
1177            NSBeginAlertSheet(title, NSLocalizedString(@"Remove", "Removal confirm panel -> button"),
1178                NSLocalizedString(@"Cancel", "Removal confirm panel -> button"), nil, fWindow, self,
1179                nil, @selector(removeSheetDidEnd:returnCode:contextInfo:), dict, message);
1180            return;
1181        }
1182    }
1183   
1184    [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
1185}
1186
1187- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (NSDictionary *) dict
1188{
1189    NSArray * torrents = [dict objectForKey: @"Torrents"];
1190    BOOL deleteData = [[dict objectForKey: @"DeleteData"] boolValue],
1191        deleteTorrent = [[dict objectForKey: @"DeleteTorrent"] boolValue];
1192    [dict release];
1193   
1194    if (returnCode == NSAlertDefaultReturn)
1195        [self confirmRemoveTorrents: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
1196    else
1197        [torrents release];
1198}
1199
1200- (void) confirmRemoveTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
1201{
1202    //don't want any of these starting then stopping
1203    NSEnumerator * enumerator = [torrents objectEnumerator];
1204    Torrent * torrent;
1205    while ((torrent = [enumerator nextObject]))
1206        [torrent setWaitToStart: NO];
1207
1208    NSNumber * lowestOrderValue = nil, * currentOrderValue;
1209
1210    enumerator = [torrents objectEnumerator];
1211    while ((torrent = [enumerator nextObject]))
1212    {
1213        [torrent stopTransfer];
1214
1215        if (deleteData)
1216            [torrent trashData];
1217        if (deleteTorrent)
1218            [torrent trashTorrent];
1219       
1220        //determine lowest order value
1221        currentOrderValue = [torrent orderValue];
1222        if (!lowestOrderValue || [lowestOrderValue compare: currentOrderValue] == NSOrderedDescending)
1223            lowestOrderValue = currentOrderValue;
1224       
1225        [torrent closeRemoveTorrent];
1226        [fTorrents removeObject: torrent];
1227        [fDisplayedTorrents removeObject: torrent];
1228    }
1229    [torrents release];
1230
1231    //reset the order values if necessary
1232    if ([lowestOrderValue intValue] < [fTorrents count])
1233    {
1234        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1235                                                @"orderValue" ascending: YES] autorelease];
1236        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1237
1238        NSArray * tempTorrents = [fTorrents sortedArrayUsingDescriptors: descriptors];
1239        [descriptors release];
1240
1241        int i;
1242        for (i = [lowestOrderValue intValue]; i < [tempTorrents count]; i++)
1243            [[tempTorrents objectAtIndex: i] setOrderValue: i];
1244    }
1245   
1246    [fTableView deselectAll: nil];
1247   
1248    [self updateUI];
1249    [self applyFilter: nil];
1250    [self updateTorrentHistory];
1251}
1252
1253- (void) removeNoDelete: (id) sender
1254{
1255    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1256                deleteData: NO deleteTorrent: NO];
1257}
1258
1259- (void) removeDeleteData: (id) sender
1260{
1261    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1262                deleteData: YES deleteTorrent: NO];
1263}
1264
1265- (void) removeDeleteTorrent: (id) sender
1266{
1267    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1268                deleteData: NO deleteTorrent: YES];
1269}
1270
1271- (void) removeDeleteDataAndTorrent: (id) sender
1272{
1273    [self removeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]
1274                deleteData: YES deleteTorrent: YES];
1275}
1276
1277- (void) moveDataFiles: (id) sender
1278{
1279    NSOpenPanel * panel = [NSOpenPanel openPanel];
1280    [panel setPrompt: NSLocalizedString(@"Select", "Move torrent -> prompt")];
1281    [panel setAllowsMultipleSelection: NO];
1282    [panel setCanChooseFiles: NO];
1283    [panel setCanChooseDirectories: YES];
1284    [panel setCanCreateDirectories: YES];
1285   
1286    NSArray * torrents = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] retain];
1287    int count = [torrents count];
1288    if (count == 1)
1289        [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the new folder for \"%@\".",
1290                            "Move torrent -> select destination folder"), [[torrents objectAtIndex: 0] name]]];
1291    else
1292        [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the new folder for %d data files.",
1293                            "Move torrent -> select destination folder"), count]];
1294       
1295    [panel beginSheetForDirectory: nil file: nil modalForWindow: fWindow modalDelegate: self
1296        didEndSelector: @selector(moveDataFileChoiceClosed:returnCode:contextInfo:) contextInfo: torrents];
1297}
1298
1299- (void) moveDataFileChoiceClosed: (NSOpenPanel *) panel returnCode: (int) code contextInfo: (NSArray *) torrents
1300{
1301    if (code == NSOKButton)
1302    {
1303        NSEnumerator * enumerator = [torrents objectEnumerator];
1304        Torrent * torrent;
1305        while ((torrent = [enumerator nextObject]))
1306            [torrent moveTorrentDataFileTo: [[panel filenames] objectAtIndex: 0]];
1307    }
1308   
1309    [torrents release];
1310}
1311
1312- (void) copyTorrentFiles: (id) sender
1313{
1314    [self copyTorrentFileForTorrents: [[NSMutableArray alloc] initWithArray:
1315            [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]]];
1316}
1317
1318- (void) copyTorrentFileForTorrents: (NSMutableArray *) torrents
1319{
1320    if ([torrents count] <= 0)
1321    {
1322        [torrents release];
1323        return;
1324    }
1325
1326    Torrent * torrent = [torrents objectAtIndex: 0];
1327
1328    //warn user if torrent file can't be found
1329    if (![[NSFileManager defaultManager] fileExistsAtPath: [torrent torrentLocation]])
1330    {
1331        NSAlert * alert = [[NSAlert alloc] init];
1332        [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent file copy alert -> button")];
1333        [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Copy of \"%@\" Cannot Be Created",
1334                                "Torrent file copy alert -> title"), [torrent name]]];
1335        [alert setInformativeText: [NSString stringWithFormat:
1336                NSLocalizedString(@"The torrent file (%@) cannot be found.", "Torrent file copy alert -> message"),
1337                                    [torrent torrentLocation]]];
1338        [alert setAlertStyle: NSWarningAlertStyle];
1339       
1340        [alert runModal];
1341        [alert release];
1342       
1343        [torrents removeObjectAtIndex: 0];
1344        [self copyTorrentFileForTorrents: torrents];
1345    }
1346    else
1347    {
1348        NSSavePanel * panel = [NSSavePanel savePanel];
1349        [panel setRequiredFileType: @"torrent"];
1350        [panel setCanSelectHiddenExtension: YES];
1351       
1352        [panel beginSheetForDirectory: nil file: [torrent name] modalForWindow: fWindow modalDelegate: self
1353            didEndSelector: @selector(saveTorrentCopySheetClosed:returnCode:contextInfo:) contextInfo: torrents];
1354    }
1355}
1356
1357- (void) saveTorrentCopySheetClosed: (NSSavePanel *) panel returnCode: (int) code contextInfo: (NSMutableArray *) torrents
1358{
1359    //copy torrent to new location with name of data file
1360    if (code == NSOKButton)
1361        [[torrents objectAtIndex: 0] copyTorrentFileTo: [panel filename]];
1362   
1363    [torrents removeObjectAtIndex: 0];
1364    [self performSelectorOnMainThread: @selector(copyTorrentFileForTorrents:) withObject: torrents waitUntilDone: NO];
1365}
1366
1367- (void) revealFile: (id) sender
1368{
1369    NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
1370    Torrent * torrent;
1371    while ((torrent = [enumerator nextObject]))
1372        [torrent revealData];
1373}
1374
1375- (void) announceSelectedTorrents: (id) sender
1376{
1377    NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
1378    Torrent * torrent;
1379    while ((torrent = [enumerator nextObject]))
1380    {
1381        if ([torrent canManualAnnounce])
1382            [torrent manualAnnounce];
1383    }
1384}
1385
1386- (void) resetCacheForSelectedTorrents: (id) sender
1387{
1388    NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
1389    Torrent * torrent;
1390    while ((torrent = [enumerator nextObject]))
1391        [torrent resetCache];
1392   
1393    [self applyFilter: nil];
1394}
1395
1396- (void) showPreferenceWindow: (id) sender
1397{
1398    NSWindow * window = [fPrefsController window];
1399    if (![window isVisible])
1400        [window center];
1401
1402    [window makeKeyAndOrderFront: nil];
1403}
1404
1405- (void) showAboutWindow: (id) sender
1406{
1407    [[AboutWindowController aboutController] showWindow: nil];
1408}
1409
1410- (void) showInfo: (id) sender
1411{
1412    if ([[fInfoController window] isVisible])
1413        [fInfoController close];
1414    else
1415    {
1416        [fInfoController updateInfoStats];
1417        [[fInfoController window] orderFront: nil];
1418    }
1419}
1420
1421- (void) setInfoTab: (id) sender
1422{
1423    if (sender == fNextInfoTabItem)
1424        [fInfoController setNextTab];
1425    else
1426        [fInfoController setPreviousTab];
1427}
1428
1429- (void) showMessageWindow: (id) sender
1430{
1431    [fMessageController showWindow: nil];
1432}
1433
1434- (void) updateUI
1435{
1436    [fTorrents makeObjectsPerformSelector: @selector(update)];
1437   
1438    if (![NSApp isHidden])
1439    {
1440        if ([fWindow isVisible])
1441        {
1442            //resort if necessary or just update the table
1443            NSString * sortType = [fDefaults stringForKey: @"Sort"];
1444            if ([sortType isEqualToString: SORT_PROGRESS] || [sortType isEqualToString: SORT_STATE]
1445                    || [sortType isEqualToString: SORT_TRACKER])
1446                [self sortTorrents];
1447            else
1448                [fTableView reloadData];
1449           
1450            //update the global DL/UL rates
1451            if (![fStatusBar isHidden])
1452            {
1453                float downloadRate, uploadRate;
1454                tr_torrentRates(fLib, & downloadRate, & uploadRate);
1455               
1456                [fTotalDLField setStringValue: [NSLocalizedString(@"Total DL: ", "Status bar -> total download")
1457                                                stringByAppendingString: [NSString stringForSpeed: downloadRate]]];
1458                [fTotalULField setStringValue: [NSLocalizedString(@"Total UL: ", "Status bar -> total upload")
1459                                                stringByAppendingString: [NSString stringForSpeed: uploadRate]]];
1460            }
1461        }
1462
1463        //update non-constant parts of info window
1464        if ([[fInfoController window] isVisible])
1465            [fInfoController updateInfoStats];
1466    }
1467
1468    //badge dock
1469    [fBadger updateBadge];
1470}
1471
1472- (void) updateTorrentsInQueue
1473{
1474    BOOL download = [fDefaults boolForKey: @"Queue"],
1475        seed = [fDefaults boolForKey: @"QueueSeed"];
1476   
1477    int desiredDownloadActive = [self numToStartFromQueue: YES],
1478        desiredSeedActive = [self numToStartFromQueue: NO];
1479   
1480    //sort torrents by order value
1481    NSArray * sortedTorrents;
1482    if ([fTorrents count] > 1 && (desiredDownloadActive > 0 || desiredSeedActive > 0))
1483    {
1484        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1485                                                @"orderValue" ascending: YES] autorelease];
1486        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1487       
1488        sortedTorrents = [fTorrents sortedArrayUsingDescriptors: descriptors];
1489        [descriptors release];
1490    }
1491    else
1492        sortedTorrents = fTorrents;
1493
1494    Torrent * torrent;
1495    NSEnumerator * enumerator = [sortedTorrents objectEnumerator];
1496    while ((torrent = [enumerator nextObject]))
1497    {
1498        if (![torrent isActive] && ![torrent isChecking] && [torrent waitingToStart])
1499        {
1500            if (![torrent allDownloaded])
1501            {
1502                if (!download || desiredDownloadActive > 0)
1503                {
1504                    [torrent startTransfer];
1505                    if ([torrent isActive])
1506                        desiredDownloadActive--;
1507                    [torrent update];
1508                }
1509            }
1510            else
1511            {
1512                if (!seed || desiredSeedActive > 0)
1513                {
1514                    [torrent startTransfer];
1515                    if ([torrent isActive])
1516                        desiredSeedActive--;
1517                    [torrent update];
1518                }
1519            }
1520        }
1521    }
1522   
1523    [self updateUI];
1524    [self applyFilter: nil];
1525    [self updateTorrentHistory];
1526}
1527
1528- (int) numToStartFromQueue: (BOOL) downloadQueue
1529{
1530    if (![fDefaults boolForKey: downloadQueue ? @"Queue" : @"QueueSeed"])
1531        return 0;
1532   
1533    int desired = [fDefaults integerForKey: downloadQueue ? @"QueueDownloadNumber" : @"QueueSeedNumber"];
1534       
1535    Torrent * torrent;
1536    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1537    while ((torrent = [enumerator nextObject]))
1538    {
1539        if ([torrent isChecking])
1540        {
1541            desired--;
1542            if (desired <= 0)
1543                return 0;
1544        }
1545        else if ([torrent isActive] && ![torrent isStalled] && ![torrent isError])
1546        {
1547            if ([torrent allDownloaded] != downloadQueue)
1548            {
1549                desired--;
1550                if (desired <= 0)
1551                    return 0;
1552            }
1553        }
1554        else;
1555    }
1556   
1557    return desired;
1558}
1559
1560- (void) torrentFinishedDownloading: (NSNotification *) notification
1561{
1562    Torrent * torrent = [notification object];
1563    if ([torrent isActive])
1564    {
1565        if ([fDefaults boolForKey: @"PlayDownloadSound"])
1566        {
1567            NSSound * sound;
1568            if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"DownloadSound"]]))
1569                [sound play];
1570        }
1571       
1572        NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_DOWNLOAD_COMPLETE, @"Type",
1573                                        [torrent dataLocation] , @"Location", nil];
1574        [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Download Complete", "Growl notification title")
1575                                    description: [torrent name] notificationName: GROWL_DOWNLOAD_COMPLETE
1576                                    iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
1577       
1578        if (![fWindow isKeyWindow])
1579            [fBadger incrementCompleted];
1580       
1581        if ([fDefaults boolForKey: @"QueueSeed"] && [self numToStartFromQueue: NO] <= 0)
1582        {
1583            [torrent stopTransfer];
1584            [torrent setWaitToStart: YES];
1585        }
1586    }
1587   
1588    [self updateTorrentsInQueue];
1589}
1590
1591- (void) torrentRestartedDownloading: (NSNotification *) notification
1592{
1593    Torrent * torrent = [notification object];
1594    if ([torrent isActive])
1595    {
1596        if ([fDefaults boolForKey: @"Queue"] && [self numToStartFromQueue: YES] <= 0)
1597        {
1598            [torrent stopTransfer];
1599            [torrent setWaitToStart: YES];
1600        }
1601    }
1602   
1603    [self updateTorrentsInQueue];
1604}
1605
1606- (void) updateTorrentHistory
1607{
1608    NSMutableArray * history = [NSMutableArray arrayWithCapacity: [fTorrents count]];
1609
1610    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1611    Torrent * torrent;
1612    while ((torrent = [enumerator nextObject]))
1613        [history addObject: [torrent history]];
1614
1615    [history writeToFile: [NSHomeDirectory() stringByAppendingPathComponent:
1616        @"/Library/Application Support/Transmission/Transfers.plist"] atomically: YES];
1617}
1618
1619- (void) sortTorrents
1620{
1621    //remember selected rows if needed
1622    NSArray * selectedTorrents = nil;
1623    int numSelected = [fTableView numberOfSelectedRows];
1624    if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
1625        selectedTorrents = [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]];
1626
1627    [self sortTorrentsIgnoreSelected]; //actually sort
1628   
1629    //set selected rows if needed
1630    if (selectedTorrents)
1631    {
1632        Torrent * torrent;
1633        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1634        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1635        while ((torrent = [enumerator nextObject]))
1636            [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
1637       
1638        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1639        [indexSet release];
1640    }
1641}
1642
1643//doesn't remember selected rows
1644- (void) sortTorrentsIgnoreSelected
1645{
1646    NSString * sortType = [fDefaults stringForKey: @"Sort"];
1647    BOOL asc = ![fDefaults boolForKey: @"SortReverse"];
1648   
1649    NSSortDescriptor * nameDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"name"
1650                                            ascending: asc selector: @selector(caseInsensitiveCompare:)] autorelease],
1651                    * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"orderValue"
1652                                            ascending: asc] autorelease];
1653   
1654    NSArray * descriptors;
1655    if ([sortType isEqualToString: SORT_NAME])
1656        descriptors = [[NSArray alloc] initWithObjects: nameDescriptor, orderDescriptor, nil];
1657    else if ([sortType isEqualToString: SORT_STATE])
1658    {
1659        NSSortDescriptor * stateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1660                                                @"stateSortKey" ascending: !asc] autorelease],
1661                        * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1662                                            @"progressSortKey" ascending: !asc] autorelease],
1663                        * ratioDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1664                                            @"ratioSortKey" ascending: !asc] autorelease];
1665       
1666        descriptors = [[NSArray alloc] initWithObjects: stateDescriptor, progressDescriptor, ratioDescriptor,
1667                                                            nameDescriptor, orderDescriptor, nil];
1668    }
1669    else if ([sortType isEqualToString: SORT_PROGRESS])
1670    {
1671        NSSortDescriptor * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1672                                            @"progressSortKey" ascending: asc] autorelease],
1673                        * ratioProgressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1674                                            @"ratioProgressSortKey" ascending: asc] autorelease],
1675                        * ratioDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1676                                            @"ratioSortKey" ascending: asc] autorelease];
1677       
1678        descriptors = [[NSArray alloc] initWithObjects: progressDescriptor, ratioProgressDescriptor, ratioDescriptor,
1679                                                            nameDescriptor, orderDescriptor, nil];
1680    }
1681    else if ([sortType isEqualToString: SORT_TRACKER])
1682    {
1683        NSSortDescriptor * trackerDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"trackerAddress"
1684                                                ascending: asc selector: @selector(caseInsensitiveCompare:)] autorelease];
1685       
1686        descriptors = [[NSArray alloc] initWithObjects: trackerDescriptor, nameDescriptor, orderDescriptor, nil];
1687    }
1688    else if ([sortType isEqualToString: SORT_ORDER])
1689        descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1690    else
1691    {
1692        NSSortDescriptor * dateDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"dateAdded" ascending: asc] autorelease];
1693   
1694        descriptors = [[NSArray alloc] initWithObjects: dateDescriptor, orderDescriptor, nil];
1695    }
1696
1697    [fDisplayedTorrents sortUsingDescriptors: descriptors];
1698    [descriptors release];
1699   
1700    [fTableView reloadData];
1701}
1702
1703- (void) setSort: (id) sender
1704{
1705    NSString * oldSortType = [fDefaults stringForKey: @"Sort"];
1706   
1707    //get checked items
1708    NSMenuItem * prevSortItem, * prevSortActionItem;
1709    if ([oldSortType isEqualToString: SORT_NAME])
1710    {
1711        prevSortItem = fNameSortItem;
1712        prevSortActionItem = fNameSortActionItem;
1713    }
1714    else if ([oldSortType isEqualToString: SORT_STATE])
1715    {
1716        prevSortItem = fStateSortItem;
1717        prevSortActionItem = fStateSortActionItem;
1718    }
1719    else if ([oldSortType isEqualToString: SORT_PROGRESS])
1720    {
1721        prevSortItem = fProgressSortItem;
1722        prevSortActionItem = fProgressSortActionItem;
1723    }
1724    else if ([oldSortType isEqualToString: SORT_TRACKER])
1725    {
1726        prevSortItem = fTrackerSortItem;
1727        prevSortActionItem = fTrackerSortActionItem;
1728    }
1729    else if ([oldSortType isEqualToString: SORT_ORDER])
1730    {
1731        prevSortItem = fOrderSortItem;
1732        prevSortActionItem = fOrderSortActionItem;
1733    }
1734    else
1735    {
1736        prevSortItem = fDateSortItem;
1737        prevSortActionItem = fDateSortActionItem;
1738    }
1739   
1740    if (sender != prevSortItem && sender != prevSortActionItem)
1741    {
1742        //get new items to check
1743        NSMenuItem * currentSortItem, * currentSortActionItem;
1744        NSString * sortType;
1745        if (sender == fNameSortItem || sender == fNameSortActionItem)
1746        {
1747            currentSortItem = fNameSortItem;
1748            currentSortActionItem = fNameSortActionItem;
1749            sortType = SORT_NAME;
1750        }
1751        else if (sender == fStateSortItem || sender == fStateSortActionItem)
1752        {
1753            currentSortItem = fStateSortItem;
1754            currentSortActionItem = fStateSortActionItem;
1755            sortType = SORT_STATE;
1756        }
1757        else if (sender == fProgressSortItem || sender == fProgressSortActionItem)
1758        {
1759            currentSortItem = fProgressSortItem;
1760            currentSortActionItem = fProgressSortActionItem;
1761            sortType = SORT_PROGRESS;
1762        }
1763        else if (sender == fTrackerSortItem || sender == fTrackerSortActionItem)
1764        {
1765            currentSortItem = fTrackerSortItem;
1766            currentSortActionItem = fTrackerSortActionItem;
1767            sortType = SORT_TRACKER;
1768        }
1769        else if (sender == fOrderSortItem || sender == fOrderSortActionItem)
1770        {
1771            currentSortItem = fOrderSortItem;
1772            currentSortActionItem = fOrderSortActionItem;
1773            sortType = SORT_ORDER;
1774           
1775            [fDefaults setBool: NO forKey: @"SortReverse"];
1776        }
1777        else
1778        {
1779            currentSortItem = fDateSortItem;
1780            currentSortActionItem = fDateSortActionItem;
1781            sortType = SORT_DATE;
1782        }
1783       
1784        [fDefaults setObject: sortType forKey: @"Sort"];
1785   
1786        [prevSortItem setState: NSOffState];
1787        [prevSortActionItem setState: NSOffState];
1788        [currentSortItem setState: NSOnState];
1789        [currentSortActionItem setState: NSOnState];
1790    }
1791
1792    [self sortTorrents];
1793}
1794
1795- (void) setSortReverse: (id) sender
1796{
1797    [self sortTorrents];
1798}
1799
1800- (void) applyFilter: (id) sender
1801{
1802    //remember selected rows if needed
1803    NSArray * selectedTorrents = [fTableView numberOfSelectedRows] > 0
1804                ? [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] : nil;
1805
1806    NSMutableArray * tempTorrents = [[NSMutableArray alloc] initWithCapacity: [fTorrents count]];
1807   
1808    NSString * filterType = [fDefaults stringForKey: @"Filter"];
1809   
1810    int downloading = 0, seeding = 0, paused = 0;
1811    BOOL isDownload = [filterType isEqualToString: FILTER_DOWNLOAD],
1812            isSeed = [filterType isEqualToString: FILTER_SEED],
1813            isPause = [filterType isEqualToString: FILTER_PAUSE];
1814    BOOL filtering = isDownload || isSeed || isPause;
1815   
1816    //get count of each type
1817    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1818    Torrent * torrent;
1819    while ((torrent = [enumerator nextObject]))
1820    {
1821        if ([torrent isActive])
1822        {
1823            if ([torrent isSeeding])
1824            {
1825                seeding++;
1826                if (isSeed)
1827                    [tempTorrents addObject: torrent];
1828            }
1829            else
1830            {
1831                downloading++;
1832                if (isDownload)
1833                    [tempTorrents addObject: torrent];
1834            }
1835        }
1836        else
1837        {
1838            paused++;
1839            if (isPause)
1840                [tempTorrents addObject: torrent];
1841        }
1842    }
1843   
1844    if (!filtering)
1845        [tempTorrents setArray: fTorrents];
1846   
1847    //set buttons with counts
1848    [fNoFilterButton setCount: [fTorrents count]];
1849    [fDownloadFilterButton setCount: downloading];
1850    [fSeedFilterButton setCount: seeding];
1851    [fPauseFilterButton setCount: paused];
1852   
1853    NSString * searchString = [fSearchFilterField stringValue];
1854    if ([searchString length] > 0)
1855    {
1856        filtering = YES;
1857       
1858        #warning check multiple trackers
1859        NSString * filterType = [fDefaults stringForKey: @"FilterSearchType"];
1860        NSString * fullString;
1861        Torrent * torrent;
1862       
1863        int i;
1864        for (i = [tempTorrents count]-1; i >= 0; i--)
1865        {
1866            torrent = [tempTorrents objectAtIndex: i];
1867            if ([filterType isEqualToString: FILTER_TYPE_TRACKER])
1868                fullString = [torrent trackerAddress];
1869            else
1870                fullString = [torrent name];
1871           
1872            if ([fullString rangeOfString: searchString options: NSCaseInsensitiveSearch].location == NSNotFound)
1873                [tempTorrents removeObjectAtIndex: i];
1874        }
1875    }
1876   
1877    [fDisplayedTorrents setArray: tempTorrents];
1878    [tempTorrents release];
1879   
1880    [self sortTorrentsIgnoreSelected];
1881   
1882    //set selected rows if needed
1883    if (selectedTorrents)
1884    {
1885        Torrent * torrent;
1886        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1887        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1888        unsigned index;
1889        while ((torrent = [enumerator nextObject]))
1890            if ((index = [fDisplayedTorrents indexOfObject: torrent]) != NSNotFound)
1891                [indexSet addIndex: index];
1892       
1893        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1894        [indexSet release];
1895    }
1896   
1897    //set status bar torrent count text
1898    NSMutableString * totalTorrentsString = [[NSMutableString alloc] initWithString: @""];
1899    if (filtering)
1900        [totalTorrentsString appendFormat: @"%d/", [fDisplayedTorrents count]];
1901   
1902    int totalCount = [fTorrents count];
1903    if (totalCount != 1)
1904        [totalTorrentsString appendFormat: NSLocalizedString(@"%d Transfers", "Status bar transfer count"), totalCount];
1905    else
1906        [totalTorrentsString appendString: NSLocalizedString(@"1 Transfer", "Status bar transfer count")];
1907   
1908    [fTotalTorrentsField setStringValue: totalTorrentsString];
1909    [totalTorrentsString release];
1910
1911    [self setWindowSizeToFit];
1912}
1913
1914//resets filter and sorts torrents
1915- (void) setFilter: (id) sender
1916{
1917    NSString * oldFilterType = [fDefaults stringForKey: @"Filter"];
1918   
1919    FilterBarButton * prevFilterButton;
1920    if ([oldFilterType isEqualToString: FILTER_PAUSE])
1921        prevFilterButton = fPauseFilterButton;
1922    else if ([oldFilterType isEqualToString: FILTER_SEED])
1923        prevFilterButton = fSeedFilterButton;
1924    else if ([oldFilterType isEqualToString: FILTER_DOWNLOAD])
1925        prevFilterButton = fDownloadFilterButton;
1926    else
1927        prevFilterButton = fNoFilterButton;
1928   
1929    if (sender != prevFilterButton)
1930    {
1931        [prevFilterButton setState: NSOffState];
1932        [sender setState: NSOnState];
1933
1934        NSString * filterType;
1935        if (sender == fDownloadFilterButton)
1936            filterType = FILTER_DOWNLOAD;
1937        else if (sender == fPauseFilterButton)
1938            filterType = FILTER_PAUSE;
1939        else if (sender == fSeedFilterButton)
1940            filterType = FILTER_SEED;
1941        else
1942            filterType = FILTER_NONE;
1943
1944        [fDefaults setObject: filterType forKey: @"Filter"];
1945    }
1946
1947    [self applyFilter: nil];
1948}
1949
1950- (void) setFilterSearchType: (id) sender
1951{
1952    NSString * oldFilterType = [fDefaults stringForKey: @"FilterSearchType"];
1953   
1954    int prevTag, currentTag = [sender tag];
1955    if ([oldFilterType isEqualToString: FILTER_TYPE_TRACKER])
1956        prevTag = FILTER_TYPE_TAG_TRACKER;
1957    else
1958        prevTag = FILTER_TYPE_TAG_NAME;
1959   
1960    if (currentTag != prevTag)
1961    {
1962        NSString * filterType;
1963        if (currentTag == FILTER_TYPE_TAG_TRACKER)
1964            filterType = FILTER_TYPE_TRACKER;
1965        else
1966            filterType = FILTER_TYPE_NAME;
1967       
1968        [fDefaults setObject: filterType forKey: @"FilterSearchType"];
1969       
1970        [[fSearchFilterField cell] setPlaceholderString: [sender title]];
1971    }
1972   
1973    [self applyFilter: nil];
1974}
1975
1976- (void) switchFilter: (id) sender
1977{
1978    NSString * filterType = [fDefaults stringForKey: @"Filter"];
1979   
1980    FilterBarButton * button;
1981    if ([filterType isEqualToString: FILTER_NONE])
1982        button = sender == fNextFilterItem ? fDownloadFilterButton : fPauseFilterButton;
1983    else if ([filterType isEqualToString: FILTER_DOWNLOAD])
1984        button = sender == fNextFilterItem ? fSeedFilterButton : fNoFilterButton;
1985    else if ([filterType isEqualToString: FILTER_SEED])
1986        button = sender == fNextFilterItem ? fPauseFilterButton : fDownloadFilterButton;
1987    else if ([filterType isEqualToString: FILTER_PAUSE])
1988        button = sender == fNextFilterItem ? fNoFilterButton : fSeedFilterButton;
1989    else
1990        button = fNoFilterButton;
1991   
1992    [self setFilter: button];
1993}
1994
1995- (void) updateControlTint: (NSNotification *) notification
1996{
1997    if ([fDefaults boolForKey: @"SpeedLimit"])
1998        [fSpeedLimitButton setImage: [NSColor currentControlTint] == NSBlueControlTint
1999            ? [NSImage imageNamed: @"SpeedLimitButtonBlue.png"] : [NSImage imageNamed: @"SpeedLimitButtonGraphite.png"]];
2000}
2001
2002- (void) applySpeedLimit: (id) sender
2003{
2004    [fPrefsController applySpeedSettings: nil];
2005}
2006
2007- (void) toggleSpeedLimit: (id) sender
2008{
2009    [fDefaults setBool: ![fDefaults boolForKey: @"SpeedLimit"] forKey: @"SpeedLimit"];
2010    [self applySpeedLimit: nil];
2011}
2012
2013- (void) autoSpeedLimitChange: (NSNotification *) notification
2014{
2015    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
2016        return;
2017 
2018    NSCalendarDate * onDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2019                        [[fDefaults objectForKey: @"SpeedLimitAutoOnDate"] timeIntervalSinceReferenceDate]],
2020        * offDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2021                        [[fDefaults objectForKey: @"SpeedLimitAutoOffDate"] timeIntervalSinceReferenceDate]],
2022        * nowDate = [NSCalendarDate calendarDate];
2023   
2024    //check if should be on if within range
2025    BOOL shouldBeOn;
2026   
2027    int onTime = [onDate hourOfDay] * 60 + [onDate minuteOfHour],
2028        offTime = [offDate hourOfDay] * 60 + [offDate minuteOfHour],
2029        nowTime = [nowDate hourOfDay] * 60 + [nowDate minuteOfHour];
2030   
2031    if (onTime == offTime)
2032        shouldBeOn = NO;
2033    else if (onTime < offTime)
2034        shouldBeOn = onTime <= nowTime && nowTime < offTime;
2035    else
2036        shouldBeOn = onTime <= nowTime || nowTime < offTime;
2037   
2038    if ([fDefaults boolForKey: @"SpeedLimit"] != shouldBeOn)
2039        [self toggleSpeedLimit: nil];
2040}
2041
2042- (void) autoSpeedLimit
2043{
2044    if (![fDefaults boolForKey: @"SpeedLimitAuto"])
2045        return;
2046   
2047    //only toggle if within first few seconds of minutes
2048    NSCalendarDate * nowDate = [NSCalendarDate calendarDate];
2049    if ([nowDate secondOfMinute] > AUTO_SPEED_LIMIT_SECONDS)
2050        return;
2051   
2052    NSCalendarDate * offDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2053                        [[fDefaults objectForKey: @"SpeedLimitAutoOffDate"] timeIntervalSinceReferenceDate]];
2054   
2055    BOOL toggle;
2056    if ([fDefaults boolForKey: @"SpeedLimit"])
2057        toggle = [nowDate hourOfDay] == [offDate hourOfDay] && [nowDate minuteOfHour] == [offDate minuteOfHour];
2058    else
2059    {
2060        NSCalendarDate * onDate = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:
2061                        [[fDefaults objectForKey: @"SpeedLimitAutoOnDate"] timeIntervalSinceReferenceDate]];
2062        toggle = ([nowDate hourOfDay] == [onDate hourOfDay] && [nowDate minuteOfHour] == [onDate minuteOfHour])
2063                    && !([onDate hourOfDay] == [offDate hourOfDay] && [onDate minuteOfHour] == [offDate minuteOfHour]);
2064    }
2065   
2066    if (toggle)
2067    {
2068        [self toggleSpeedLimit: nil];
2069       
2070        [GrowlApplicationBridge notifyWithTitle: [fDefaults boolForKey: @"SpeedLimit"]
2071                ? NSLocalizedString(@"Speed Limit Auto Enabled", "Growl notification title")
2072                : NSLocalizedString(@"Speed Limit Auto Disabled", "Growl notification title")
2073            description: NSLocalizedString(@"Bandwidth settings changed", "Growl notification description")
2074            notificationName: GROWL_AUTO_SPEED_LIMIT iconData: nil priority: 0 isSticky: NO clickContext: nil];
2075    }
2076}
2077
2078- (void) setLimitGlobalEnabled: (id) sender
2079{
2080    [fPrefsController applySpeedSettings: nil];
2081}
2082
2083- (void) setQuickLimitGlobal: (id) sender
2084{
2085    [fDefaults setInteger: [[sender title] intValue] forKey: [sender menu] == fUploadMenu ? @"UploadLimit" : @"DownloadLimit"];
2086    [fDefaults setBool: YES forKey: [sender menu] == fUploadMenu ? @"CheckUpload" : @"CheckDownload"];
2087   
2088    [fPrefsController updateLimitFields];
2089    [fPrefsController applySpeedSettings: nil];
2090}
2091
2092- (void) setQuickRatioGlobal: (id) sender
2093{
2094    [fDefaults setBool: YES forKey: @"RatioCheck"];
2095    [fDefaults setFloat: [[sender title] floatValue] forKey: @"RatioLimit"];
2096   
2097    [fPrefsController updateRatioStopField];
2098}
2099
2100- (void) torrentStoppedForRatio: (NSNotification *) notification
2101{
2102    Torrent * torrent = [notification object];
2103   
2104    [self updateTorrentsInQueue];
2105    [fInfoController updateInfoStats];
2106   
2107    if ([fDefaults boolForKey: @"PlaySeedingSound"])
2108    {
2109        NSSound * sound;
2110        if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]]))
2111            [sound play];
2112    }
2113   
2114    NSDictionary * clickContext = [NSDictionary dictionaryWithObjectsAndKeys: GROWL_SEEDING_COMPLETE, @"Type",
2115                                    [torrent dataLocation], @"Location", nil];
2116    [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Seeding Complete", "Growl notification title")
2117                        description: [torrent name] notificationName: GROWL_SEEDING_COMPLETE
2118                        iconData: nil priority: 0 isSticky: NO clickContext: clickContext];
2119}
2120
2121-(void) watcher: (id<UKFileWatcher>) watcher receivedNotification: (NSString *) notification forPath: (NSString *) path
2122{
2123    if ([notification isEqualToString: UKFileWatcherWriteNotification])
2124    {
2125        if (![fDefaults boolForKey: @"AutoImport"] || ![fDefaults stringForKey: @"AutoImportDirectory"])
2126            return;
2127       
2128        if (fAutoImportTimer)
2129        {
2130            if ([fAutoImportTimer isValid])
2131                [fAutoImportTimer invalidate];
2132            [fAutoImportTimer release];
2133            fAutoImportTimer = nil;
2134        }
2135       
2136        //check again in 10 seconds in case torrent file wasn't complete
2137        fAutoImportTimer = [[NSTimer scheduledTimerWithTimeInterval: 10.0 target: self
2138            selector: @selector(checkAutoImportDirectory) userInfo: nil repeats: NO] retain];
2139       
2140        [self checkAutoImportDirectory];
2141    }
2142}
2143
2144- (void) changeAutoImport
2145{
2146    if (fAutoImportTimer)
2147    {
2148        if ([fAutoImportTimer isValid])
2149            [fAutoImportTimer invalidate];
2150        [fAutoImportTimer release];
2151        fAutoImportTimer = nil;
2152    }
2153   
2154    if (fAutoImportedNames)
2155        [fAutoImportedNames removeAllObjects];
2156    [self checkAutoImportDirectory];
2157}
2158
2159- (void) checkAutoImportDirectory
2160{
2161    NSString * path;
2162    if (![fDefaults boolForKey: @"AutoImport"] || !(path = [fDefaults stringForKey: @"AutoImportDirectory"]))
2163        return;
2164   
2165    path = [path stringByExpandingTildeInPath];
2166   
2167    NSArray * importedNames;
2168    if (!(importedNames = [[NSFileManager defaultManager] directoryContentsAtPath: path]))
2169        return;
2170   
2171    //only check files that have not been checked yet
2172    NSMutableArray * newNames = [importedNames mutableCopy];
2173   
2174    if (fAutoImportedNames)
2175        [newNames removeObjectsInArray: fAutoImportedNames];
2176    else
2177        fAutoImportedNames = [[NSMutableArray alloc] init];
2178    [fAutoImportedNames setArray: importedNames];
2179   
2180    NSString * file;
2181    int i;
2182    for (i = [newNames count] - 1; i >= 0; i--)
2183    {
2184        file = [newNames objectAtIndex: i];
2185        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame)
2186            [newNames removeObjectAtIndex: i];
2187        else
2188            [newNames replaceObjectAtIndex: i withObject: [path stringByAppendingPathComponent: file]];
2189    }
2190   
2191    BOOL ask = [[fDefaults stringForKey: @"DownloadChoice"] isEqualToString: @"Ask"];
2192   
2193    NSEnumerator * enumerator = [newNames objectEnumerator];
2194    int canAdd, count;
2195    while ((file = [enumerator nextObject]))
2196    {
2197        canAdd = tr_torrentParse(fLib, [file UTF8String], NULL, NULL);
2198        if (canAdd == TR_OK)
2199        {
2200            if (!ask)
2201                count = [fTorrents count];
2202            [self openFiles: [NSArray arrayWithObject: file]];
2203           
2204            //check if torrent was opened
2205            if (!ask && [fTorrents count] > count)
2206                [GrowlApplicationBridge notifyWithTitle: NSLocalizedString(@"Torrent File Auto Added",
2207                    "Growl notification title") description: [file lastPathComponent]
2208                    notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO clickContext: nil];
2209        }
2210        else if (canAdd == TR_EINVALID)
2211            [fAutoImportedNames removeObject: [file lastPathComponent]];
2212        else;
2213    }
2214   
2215    [newNames release];
2216}
2217
2218- (void) beginCreateFile: (NSNotification *) notification
2219{
2220    if (![fDefaults boolForKey: @"AutoImport"])
2221        return;
2222   
2223    NSString * location = [notification object],
2224            * path = [fDefaults stringForKey: @"AutoImportDirectory"];
2225   
2226    if (location && path && [[[location stringByDeletingLastPathComponent] stringByExpandingTildeInPath]
2227                                    isEqualToString: [path stringByExpandingTildeInPath]])
2228        [fAutoImportedNames addObject: [location lastPathComponent]];
2229}
2230
2231- (int) numberOfRowsInTableView: (NSTableView *) tableview
2232{
2233    return [fDisplayedTorrents count];
2234}
2235
2236- (BOOL) tableView: (NSTableView *) tableView writeRowsWithIndexes: (NSIndexSet *) indexes
2237    toPasteboard: (NSPasteboard *) pasteboard
2238{
2239    //only allow reordering of rows if sorting by order with no filter
2240    if ([[fDefaults stringForKey: @"Sort"] isEqualToString: @"Order"]
2241        && [[fDefaults stringForKey: @"Filter"] isEqualToString: @"None"]
2242        && [[fSearchFilterField stringValue] length] == 0)
2243    {
2244        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
2245        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexes]
2246                                forType: TORRENT_TABLE_VIEW_DATA_TYPE];
2247        return YES;
2248    }
2249    return NO;
2250}
2251
2252- (NSDragOperation) tableView: (NSTableView *) t validateDrop: (id <NSDraggingInfo>) info
2253    proposedRow: (int) row proposedDropOperation: (NSTableViewDropOperation) operation
2254{
2255    NSPasteboard * pasteboard = [info draggingPasteboard];
2256    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
2257    {
2258        [fTableView setDropRow: row dropOperation: NSTableViewDropAbove];
2259        return NSDragOperationGeneric;
2260    }
2261    else;
2262   
2263    return NSDragOperationNone;
2264}
2265
2266- (BOOL) tableView: (NSTableView *) t acceptDrop: (id <NSDraggingInfo>) info
2267    row: (int) newRow dropOperation: (NSTableViewDropOperation) operation
2268{
2269    NSPasteboard * pasteboard = [info draggingPasteboard];
2270    if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
2271    {
2272        //remember selected rows if needed
2273        NSArray * selectedTorrents = nil;
2274        int numSelected = [fTableView numberOfSelectedRows];
2275        if (numSelected > 0 && numSelected < [fDisplayedTorrents count])
2276            selectedTorrents = [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]];
2277   
2278        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData:
2279                                [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
2280       
2281        //move torrent in array
2282        NSArray * movingTorrents = [[fDisplayedTorrents objectsAtIndexes: indexes] retain];
2283        [fDisplayedTorrents removeObjectsInArray: movingTorrents];
2284       
2285        //determine the insertion index now that transfers to move have been removed
2286        int i, decrease = 0;
2287        for (i = [indexes firstIndex]; i < newRow && i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
2288            decrease++;
2289       
2290        //insert objects at new location
2291        for (i = 0; i < [movingTorrents count]; i++)
2292            [fDisplayedTorrents insertObject: [movingTorrents objectAtIndex: i] atIndex: newRow - decrease + i];
2293       
2294        [movingTorrents release];
2295       
2296        //redo order values
2297        int low = [indexes firstIndex], high = [indexes lastIndex];
2298        if (newRow < low)
2299            low = newRow;
2300        else if (newRow > high + 1)
2301            high = newRow - 1;
2302        else;
2303       
2304        for (i = low; i <= high; i++)
2305            [[fDisplayedTorrents objectAtIndex: i] setOrderValue: i];
2306       
2307        [fTableView reloadData];
2308       
2309        //set selected rows if needed
2310        if (selectedTorrents)
2311        {
2312            Torrent * torrent;
2313            NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
2314            NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
2315            while ((torrent = [enumerator nextObject]))
2316                [indexSet addIndex: [fDisplayedTorrents indexOfObject: torrent]];
2317           
2318            [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
2319            [indexSet release];
2320        }
2321    }
2322    else;
2323   
2324    return YES;
2325}
2326
2327- (void) torrentTableViewSelectionDidChange: (NSNotification *) notification
2328{
2329    [fInfoController updateInfoForTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]];
2330}
2331
2332- (NSDragOperation) draggingEntered: (id <NSDraggingInfo>) info
2333{
2334    NSPasteboard * pasteboard = [info draggingPasteboard];
2335    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
2336    {
2337        //check if any torrent files can be added
2338        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
2339        NSEnumerator * enumerator = [files objectEnumerator];
2340        NSString * file;
2341        BOOL torrent = NO;
2342        int canAdd;
2343        while ((file = [enumerator nextObject]))
2344        {
2345            canAdd = tr_torrentParse(fLib, [file UTF8String], NULL, NULL);
2346            if (canAdd == TR_OK)
2347            {
2348                if (!fOverlayWindow)
2349                    fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2350                [fOverlayWindow setTorrents: files];
2351               
2352                return NSDragOperationCopy;
2353            }
2354            else if (canAdd == TR_EDUPLICATE)
2355                torrent = YES;
2356            else;
2357        }
2358       
2359        //create a torrent file if a single file
2360        if (!torrent && [files count] == 1)
2361        {
2362            if (!fOverlayWindow)
2363                fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2364            [fOverlayWindow setFile: [[files objectAtIndex: 0] lastPathComponent]];
2365           
2366            return NSDragOperationCopy;
2367        }
2368    }
2369    else if ([[pasteboard types] containsObject: NSURLPboardType])
2370    {
2371        if (!fOverlayWindow)
2372            fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow];
2373        [fOverlayWindow setURL: [[NSURL URLFromPasteboard: pasteboard] relativeString]];
2374       
2375        return NSDragOperationCopy;
2376    }
2377    else;
2378   
2379    return NSDragOperationNone;
2380}
2381
2382- (void) draggingExited: (id <NSDraggingInfo>) info
2383{
2384    if (fOverlayWindow)
2385        [fOverlayWindow fadeOut];
2386}
2387
2388- (BOOL) performDragOperation: (id <NSDraggingInfo>) info
2389{
2390    if (fOverlayWindow)
2391        [fOverlayWindow fadeOut];
2392   
2393    NSPasteboard * pasteboard = [info draggingPasteboard];
2394    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
2395    {
2396        BOOL torrent = NO, accept = YES;
2397       
2398        //create an array of files that can be opened
2399        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
2400        NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType];
2401        NSEnumerator * enumerator = [files objectEnumerator];
2402        NSString * file;
2403        int canAdd;
2404        while ((file = [enumerator nextObject]))
2405        {
2406            canAdd = tr_torrentParse(fLib, [file UTF8String], NULL, NULL);
2407            if (canAdd == TR_OK)
2408            {
2409                [filesToOpen addObject: file];
2410                torrent = YES;
2411            }
2412            else if (canAdd == TR_EDUPLICATE)
2413                torrent = YES;
2414            else;
2415        }
2416       
2417        if ([filesToOpen count] > 0)
2418            [self application: NSApp openFiles: filesToOpen];
2419        else
2420        {
2421            if (!torrent && [files count] == 1)
2422                [CreatorWindowController createTorrentFile: fLib forFile: [files objectAtIndex: 0]];
2423            else
2424                accept = NO;
2425        }
2426        [filesToOpen release];
2427       
2428        return accept;
2429    }
2430    else if ([[pasteboard types] containsObject: NSURLPboardType])
2431    {
2432        NSURL * url;
2433        if ((url = [NSURL URLFromPasteboard: pasteboard]))
2434        {
2435            [self openURL: url];
2436            return YES;
2437        }
2438    }
2439    else;
2440   
2441    return NO;
2442}
2443
2444- (void) toggleSmallView: (id) sender
2445{
2446    BOOL makeSmall = [fDefaults boolForKey: @"SmallView"];
2447   
2448    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
2449   
2450    //window min height
2451    NSSize contentMinSize = [fWindow contentMinSize],
2452            contentSize = [[fWindow contentView] frame].size;
2453    contentMinSize.height = contentSize.height - [fScrollView frame].size.height
2454                            + [fTableView rowHeight] + [fTableView intercellSpacing].height;
2455    [fWindow setContentMinSize: contentMinSize];
2456   
2457    //resize for larger min height if not set to auto size
2458    if (![fDefaults boolForKey: @"AutoSize"])
2459    {
2460        if (!makeSmall && contentSize.height < contentMinSize.height)
2461        {
2462            NSRect frame = [fWindow frame];
2463            float heightChange = contentMinSize.height - contentSize.height;
2464            frame.size.height += heightChange;
2465            frame.origin.y -= heightChange;
2466           
2467            [fWindow setFrame: frame display: YES];
2468            [fTableView reloadData];
2469        }
2470    }
2471    else
2472        [self setWindowSizeToFit];
2473}
2474
2475- (void) toggleStatusBar: (id) sender
2476{
2477    [self showStatusBar: [fStatusBar isHidden] animate: YES];
2478    [fDefaults setBool: ![fStatusBar isHidden] forKey: @"StatusBar"];
2479}
2480
2481- (NSRect) windowFrameByAddingHeight: (float) height checkLimits: (BOOL) check
2482{
2483    //convert pixels to points
2484    NSRect windowFrame = [fWindow frame];
2485    NSSize windowSize = [fScrollView convertSize: windowFrame.size fromView: nil];
2486    windowSize.height += height;
2487   
2488    if (check)
2489    {
2490        NSSize minSize = [fScrollView convertSize: [fWindow minSize] fromView: nil];
2491       
2492        if (windowSize.height < minSize.height)
2493            windowSize.height = minSize.height;
2494        else
2495        {
2496            NSSize maxSize = [fScrollView convertSize: [[fWindow screen] visibleFrame].size fromView: nil];
2497            if ([fStatusBar isHidden])
2498                maxSize.height -= [fStatusBar frame].size.height;
2499            if ([fFilterBar isHidden])
2500                maxSize.height -= [fFilterBar frame].size.height;
2501            if (windowSize.height > maxSize.height)
2502                windowSize.height = maxSize.height;
2503        }
2504    }
2505
2506    //convert points to pixels
2507    windowSize = [fScrollView convertSize: windowSize toView: nil];
2508
2509    windowFrame.origin.y -= (windowSize.height - windowFrame.size.height);
2510    windowFrame.size.height = windowSize.height;
2511    return windowFrame;
2512}
2513
2514- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
2515{
2516    if (show != [fStatusBar isHidden])
2517        return;
2518
2519    if (show)
2520        [fStatusBar setHidden: NO];
2521
2522    NSRect frame;
2523    float heightChange = [fStatusBar frame].size.height;
2524    if (!show)
2525        heightChange *= -1;
2526   
2527    //allow bar to show even if not enough room
2528    if (show && ![fDefaults boolForKey: @"AutoSize"])
2529    {
2530        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2531        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2532        if (change < 0.0)
2533        {
2534            frame = [fWindow frame];
2535            frame.size.height += change;
2536            frame.origin.y -= change;
2537            [fWindow setFrame: frame display: NO animate: NO];
2538        }
2539    }
2540
2541    [self updateUI];
2542   
2543    //set views to not autoresize
2544    unsigned int statsMask = [fStatusBar autoresizingMask];
2545    unsigned int filterMask = [fFilterBar autoresizingMask];
2546    unsigned int scrollMask = [fScrollView autoresizingMask];
2547    [fStatusBar setAutoresizingMask: NSViewNotSizable];
2548    [fFilterBar setAutoresizingMask: NSViewNotSizable];
2549    [fScrollView setAutoresizingMask: NSViewNotSizable];
2550   
2551    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2552    [fWindow setFrame: frame display: YES animate: animate];
2553   
2554    //re-enable autoresize
2555    [fStatusBar setAutoresizingMask: statsMask];
2556    [fFilterBar setAutoresizingMask: filterMask];
2557    [fScrollView setAutoresizingMask: scrollMask];
2558   
2559    //change min size
2560    NSSize minSize = [fWindow contentMinSize];
2561    minSize.height += heightChange;
2562    [fWindow setContentMinSize: minSize];
2563   
2564    if (!show)
2565        [fStatusBar setHidden: YES];
2566}
2567
2568- (void) toggleFilterBar: (id) sender
2569{
2570    //disable filtering when hiding
2571    if (![fFilterBar isHidden])
2572    {
2573        [fSearchFilterField setStringValue: @""];
2574        [self setFilter: fNoFilterButton];
2575    }
2576
2577    [self showFilterBar: [fFilterBar isHidden] animate: YES];
2578    [fDefaults setBool: ![fFilterBar isHidden] forKey: @"FilterBar"];
2579}
2580
2581- (void) showFilterBar: (BOOL) show animate: (BOOL) animate
2582{
2583    if (show != [fFilterBar isHidden])
2584        return;
2585
2586    if (show)
2587        [fFilterBar setHidden: NO];
2588
2589    NSRect frame;
2590    float heightChange = [fFilterBar frame].size.height;
2591    if (!show)
2592        heightChange *= -1;
2593   
2594    //allow bar to show even if not enough room
2595    if (show && ![fDefaults boolForKey: @"AutoSize"])
2596    {
2597        frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2598        float change = [[fWindow screen] visibleFrame].size.height - frame.size.height;
2599        if (change < 0.0)
2600        {
2601            frame = [fWindow frame];
2602            frame.size.height += change;
2603            frame.origin.y -= change;
2604            [fWindow setFrame: frame display: NO animate: NO];
2605        }
2606    }
2607
2608    //set views to not autoresize
2609    unsigned int filterMask = [fFilterBar autoresizingMask];
2610    unsigned int scrollMask = [fScrollView autoresizingMask];
2611    [fFilterBar setAutoresizingMask: NSViewNotSizable];
2612    [fScrollView setAutoresizingMask: NSViewNotSizable];
2613   
2614    frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO];
2615    [fWindow setFrame: frame display: YES animate: animate];
2616   
2617    //re-enable autoresize
2618    [fFilterBar setAutoresizingMask: filterMask];
2619    [fScrollView setAutoresizingMask: scrollMask];
2620   
2621    //change min size
2622    NSSize minSize = [fWindow contentMinSize];
2623    minSize.height += heightChange;
2624    [fWindow setContentMinSize: minSize];
2625   
2626    if (!show)
2627    {
2628        [fFilterBar setHidden: YES];
2629        [fWindow makeFirstResponder: fTableView];
2630    }
2631}
2632
2633/*- (void) toggleAdvancedBar: (id) sender
2634{
2635    [fTableView display];
2636}*/
2637
2638- (void) doNothing: (id) sender {}
2639
2640- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier: (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
2641{
2642    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
2643
2644    if ([ident isEqualToString: TOOLBAR_CREATE])
2645    {
2646        [item setLabel: NSLocalizedString(@"Create", "Create toolbar item -> label")];
2647        [item setPaletteLabel: NSLocalizedString(@"Create Torrent File", "Create toolbar item -> palette label")];
2648        [item setToolTip: NSLocalizedString(@"Create torrent file", "Create toolbar item -> tooltip")];
2649        [item setImage: [NSImage imageNamed: @"Create.png"]];
2650        [item setTarget: self];
2651        [item setAction: @selector(createFile:)];
2652        [item setAutovalidates: NO];
2653    }
2654    else if ([ident isEqualToString: TOOLBAR_OPEN])
2655    {
2656        [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")];
2657        [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")];
2658        [item setToolTip: NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip")];
2659        [item setImage: [NSImage imageNamed: @"Open.png"]];
2660        [item setTarget: self];
2661        [item setAction: @selector(openShowSheet:)];
2662        [item setAutovalidates: NO];
2663    }
2664    else if ([ident isEqualToString: TOOLBAR_REMOVE])
2665    {
2666        [item setLabel: NSLocalizedString(@"Remove", "Remove toolbar item -> label")];
2667        [item setPaletteLabel: NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label")];
2668        [item setToolTip: NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip")];
2669        [item setImage: [NSImage imageNamed: @"Remove.png"]];
2670        [item setTarget: self];
2671        [item setAction: @selector(removeNoDelete:)];
2672    }
2673    else if ([ident isEqualToString: TOOLBAR_INFO])
2674    {
2675        [item setLabel: NSLocalizedString(@"Inspector", "Inspector toolbar item -> label")];
2676        [item setPaletteLabel: NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label")];
2677        [item setToolTip: NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip")];
2678        [item setImage: [NSImage imageNamed: @"Info.png"]];
2679        [item setTarget: self];
2680        [item setAction: @selector(showInfo:)];
2681        [item setAutovalidates: NO];
2682    }
2683    else if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
2684    {
2685        [item setLabel: NSLocalizedString(@"Pause All", "Pause All toolbar item -> label")];
2686        [item setPaletteLabel: [item label]];
2687        [item setToolTip: NSLocalizedString(@"Pause all transfers", "Pause All toolbar item -> tooltip")];
2688        [item setImage: [NSImage imageNamed: @"PauseAll.png"]];
2689        [item setTarget: self];
2690        [item setAction: @selector(stopAllTorrents:)];
2691    }
2692    else if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
2693    {
2694        [item setLabel: NSLocalizedString(@"Resume All", "Resume All toolbar item -> label")];
2695        [item setPaletteLabel: [item label]];
2696        [item setToolTip: NSLocalizedString(@"Resume all transfers", "Resume All toolbar item -> tooltip")];
2697        [item setImage: [NSImage imageNamed: @"ResumeAll.png"]];
2698        [item setTarget: self];
2699        [item setAction: @selector(resumeAllTorrents:)];
2700    }
2701    else if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
2702    {
2703        [item setLabel: NSLocalizedString(@"Pause", "Pause toolbar item -> label")];
2704        [item setPaletteLabel: NSLocalizedString(@"Pause Selected", "Pause toolbar item -> palette label")];
2705        [item setToolTip: NSLocalizedString(@"Pause selected transfers", "Pause toolbar item -> tooltip")];
2706        [item setImage: [NSImage imageNamed: @"PauseSelected.png"]];
2707        [item setTarget: self];
2708        [item setAction: @selector(stopSelectedTorrents:)];
2709    }
2710    else if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2711    {
2712        [item setLabel: NSLocalizedString(@"Resume", "Resume toolbar item -> label")];
2713        [item setPaletteLabel: NSLocalizedString(@"Resume Selected", "Resume toolbar item -> palette label")];
2714        [item setToolTip: NSLocalizedString(@"Resume selected transfers", "Resume toolbar item -> tooltip")];
2715        [item setImage: [NSImage imageNamed: @"ResumeSelected.png"]];
2716        [item setTarget: self];
2717        [item setAction: @selector(resumeSelectedTorrents:)];
2718    }
2719    else if ([ident isEqualToString: TOOLBAR_FILTER])
2720    {
2721        [item setLabel: NSLocalizedString(@"Filter", "Filter toolbar item -> label")];
2722        [item setPaletteLabel: NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label")];
2723        [item setToolTip: NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip")];
2724        [item setImage: [NSImage imageNamed: @"Filter.png"]];
2725        [item setTarget: self];
2726        [item setAction: @selector(toggleFilterBar:)];
2727        [item setAutovalidates: NO];
2728    }
2729    else
2730    {
2731        [item release];
2732        return nil;
2733    }
2734
2735    return [item autorelease];
2736}
2737
2738- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t
2739{
2740    return [NSArray arrayWithObjects:
2741            TOOLBAR_CREATE, TOOLBAR_OPEN, TOOLBAR_REMOVE,
2742            TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED,
2743            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, TOOLBAR_FILTER, TOOLBAR_INFO,
2744            NSToolbarSeparatorItemIdentifier,
2745            NSToolbarSpaceItemIdentifier,
2746            NSToolbarFlexibleSpaceItemIdentifier,
2747            NSToolbarCustomizeToolbarItemIdentifier, nil];
2748}
2749
2750- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t
2751{
2752    return [NSArray arrayWithObjects:
2753            TOOLBAR_CREATE, TOOLBAR_OPEN, TOOLBAR_REMOVE,
2754            NSToolbarSeparatorItemIdentifier,
2755            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
2756            NSToolbarFlexibleSpaceItemIdentifier,
2757            TOOLBAR_FILTER, TOOLBAR_INFO, nil];
2758}
2759
2760#warning validateUserInterfaceItem: ???
2761- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
2762{
2763    NSString * ident = [toolbarItem itemIdentifier];
2764
2765    //enable remove item
2766    if ([ident isEqualToString: TOOLBAR_REMOVE])
2767        return [fTableView numberOfSelectedRows] > 0;
2768
2769    //enable pause all item
2770    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
2771    {
2772        Torrent * torrent;
2773        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2774        while ((torrent = [enumerator nextObject]))
2775            if ([torrent isActive] || [torrent waitingToStart])
2776                return YES;
2777        return NO;
2778    }
2779
2780    //enable resume all item
2781    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
2782    {
2783        Torrent * torrent;
2784        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2785        while ((torrent = [enumerator nextObject]))
2786            if ([torrent isPaused] && ![torrent waitingToStart])
2787                return YES;
2788        return NO;
2789    }
2790
2791    //enable pause item
2792    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
2793    {
2794        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2795        Torrent * torrent;
2796        while ((torrent = [enumerator nextObject]))
2797            if ([torrent isActive] || [torrent waitingToStart])
2798                return YES;
2799        return NO;
2800    }
2801   
2802    //enable resume item
2803    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
2804    {
2805        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2806        Torrent * torrent;
2807        while ((torrent = [enumerator nextObject]))
2808            if ([torrent isPaused] && ![torrent waitingToStart])
2809                return YES;
2810        return NO;
2811    }
2812
2813    return YES;
2814}
2815
2816- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
2817{
2818    SEL action = [menuItem action];
2819
2820    //only enable some items if it is in a context menu or the window is useable
2821    BOOL canUseTable = [fWindow isKeyWindow] || [[menuItem menu] supermenu] != [NSApp mainMenu];
2822
2823    //enable open items
2824    if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:))
2825        return [fWindow attachedSheet] == nil;
2826   
2827    //enable sort and advanced bar items
2828    if (action == @selector(setSort:) || action == @selector(toggleAdvancedBar:) || action == @selector(toggleSmallView:))
2829        return [fWindow isVisible];
2830
2831    //enable show info
2832    if (action == @selector(showInfo:))
2833    {
2834        NSString * title = [[fInfoController window] isVisible] ? NSLocalizedString(@"Hide Inspector",
2835                            "View menu -> Inspector") : NSLocalizedString(@"Show Inspector", "View menu -> Inspector");
2836        if (![[menuItem title] isEqualToString: title])
2837                [menuItem setTitle: title];
2838
2839        return YES;
2840    }
2841   
2842    //enable prev/next inspector tab
2843    if (action == @selector(setInfoTab:))
2844        return [[fInfoController window] isVisible];
2845   
2846    //enable toggle status bar
2847    if (action == @selector(toggleStatusBar:))
2848    {
2849        NSString * title = [fStatusBar isHidden] ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar")
2850                            : NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar");
2851        if (![[menuItem title] isEqualToString: title])
2852            [menuItem setTitle: title];
2853
2854        return [fWindow isVisible];
2855    }
2856   
2857    //enable toggle filter bar
2858    if (action == @selector(toggleFilterBar:))
2859    {
2860        NSString * title = [fFilterBar isHidden] ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar")
2861                            : NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar");
2862        if (![[menuItem title] isEqualToString: title])
2863            [menuItem setTitle: title];
2864
2865        return [fWindow isVisible];
2866    }
2867   
2868    //enable prev/next filter button
2869    if (action == @selector(switchFilter:))
2870        return [fWindow isVisible] && ![fFilterBar isHidden];
2871
2872    //enable reveal in finder
2873    if (action == @selector(revealFile:))
2874        return canUseTable && [fTableView numberOfSelectedRows] > 0;
2875
2876    //enable remove items
2877    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
2878        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteDataAndTorrent:))
2879    {
2880        BOOL warning = NO,
2881            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
2882            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteDataAndTorrent:);
2883       
2884        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2885        Torrent * torrent;
2886        while ((torrent = [enumerator nextObject]))
2887        {
2888            if (!warning && [torrent isActive])
2889            {
2890                warning = onlyDownloading ? ![torrent isSeeding] : YES;
2891                if (warning && canDelete)
2892                    break;
2893            }
2894            if (!canDelete && [torrent publicTorrent])
2895            {
2896                canDelete = YES;
2897                if (warning)
2898                    break;
2899            }
2900        }
2901   
2902        //append or remove ellipsis when needed
2903        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
2904        if (warning && [fDefaults boolForKey: @"CheckRemove"])
2905        {
2906            if (![title hasSuffix: ellipsis])
2907                [menuItem setTitle: [title stringByAppendingEllipsis]];
2908        }
2909        else
2910        {
2911            if ([title hasSuffix: ellipsis])
2912                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
2913        }
2914       
2915        return canUseTable && canDelete && [fTableView numberOfSelectedRows] > 0;
2916    }
2917
2918    //enable pause all item
2919    if (action == @selector(stopAllTorrents:))
2920    {
2921        Torrent * torrent;
2922        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2923        while ((torrent = [enumerator nextObject]))
2924            if ([torrent isActive] || [torrent waitingToStart])
2925                return YES;
2926        return NO;
2927    }
2928   
2929    //enable resume all item
2930    if (action == @selector(resumeAllTorrents:))
2931    {
2932        Torrent * torrent;
2933        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2934        while ((torrent = [enumerator nextObject]))
2935            if ([torrent isPaused] && ![torrent waitingToStart])
2936                return YES;
2937        return NO;
2938    }
2939   
2940    //enable resume all waiting item
2941    if (action == @selector(resumeWaitingTorrents:))
2942    {
2943        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
2944            return NO;
2945   
2946        Torrent * torrent;
2947        NSEnumerator * enumerator = [fTorrents objectEnumerator];
2948        while ((torrent = [enumerator nextObject]))
2949            if ([torrent isPaused] && [torrent waitingToStart])
2950                return YES;
2951        return NO;
2952    }
2953   
2954    //enable resume selected waiting item
2955    if (action == @selector(resumeSelectedTorrentsNoWait:))
2956    {
2957        if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"])
2958            return NO;
2959   
2960        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2961        Torrent * torrent;
2962        while ((torrent = [enumerator nextObject]))
2963            if ([torrent isPaused])
2964                return YES;
2965        return NO;
2966    }
2967
2968    //enable pause item
2969    if (action == @selector(stopSelectedTorrents:))
2970    {
2971        if (!canUseTable)
2972            return NO;
2973   
2974        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2975        Torrent * torrent;
2976        while ((torrent = [enumerator nextObject]))
2977            if ([torrent isActive] || [torrent waitingToStart])
2978                return YES;
2979        return NO;
2980    }
2981   
2982    //enable resume item
2983    if (action == @selector(resumeSelectedTorrents:))
2984    {
2985        if (!canUseTable)
2986            return NO;
2987   
2988        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
2989        Torrent * torrent;
2990        while ((torrent = [enumerator nextObject]))
2991            if ([torrent isPaused] && ![torrent waitingToStart])
2992                return YES;
2993        return NO;
2994    }
2995   
2996    //enable manual announce item
2997    if (action == @selector(announceSelectedTorrents:))
2998    {
2999        if (!canUseTable)
3000            return NO;
3001       
3002        NSEnumerator * enumerator = [[fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]] objectEnumerator];
3003        Torrent * torrent;
3004        while ((torrent = [enumerator nextObject]))
3005            if ([torrent canManualAnnounce])
3006                return YES;
3007        return NO;
3008    }
3009   
3010    //enable reset cache item
3011    if (action == @selector(resetCacheForSelectedTorrents:))
3012        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3013   
3014    //enable move torrent file item
3015    if (action == @selector(moveDataFiles:))
3016        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3017   
3018    //enable copy torrent file item
3019    if (action == @selector(copyTorrentFiles:))
3020        return canUseTable && [fTableView numberOfSelectedRows] > 0;
3021   
3022    //enable reverse sort item
3023    if (action == @selector(setSortReverse:))
3024        return ![[fDefaults stringForKey: @"Sort"] isEqualToString: @"Order"];
3025   
3026    //check proper filter search item
3027    if (action == @selector(setFilterSearchType:))
3028    {
3029        NSString * filterType = [fDefaults stringForKey: @"FilterSearchType"];
3030       
3031        int tag = [menuItem tag];
3032        BOOL state;
3033        if (tag == FILTER_TYPE_TAG_TRACKER)
3034            state = [filterType isEqualToString: FILTER_TYPE_TRACKER];
3035        else
3036            state = [filterType isEqualToString: FILTER_TYPE_NAME];
3037       
3038        [menuItem setState: state ? NSOnState : NSOffState];
3039        return YES;
3040    }
3041   
3042    return YES;
3043}
3044
3045- (void) sleepCallBack: (natural_t) messageType argument: (void *) messageArgument
3046{
3047    NSEnumerator * enumerator;
3048    Torrent * torrent;
3049    BOOL active, allowSleep;
3050
3051    switch (messageType)
3052    {
3053        case kIOMessageSystemWillSleep:
3054            //close all connections before going to sleep and remember we should resume when we wake up
3055            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
3056
3057            //wait for running transfers to stop (5 second timeout)
3058            NSDate * start = [NSDate date];
3059            BOOL timeUp = NO;
3060           
3061            enumerator = [fTorrents objectEnumerator];
3062            while (!timeUp && (torrent = [enumerator nextObject]))
3063                while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
3064                {
3065                    usleep(100000);
3066                    [torrent update];
3067                }
3068
3069            IOAllowPowerChange(fRootPort, (long) messageArgument);
3070            break;
3071
3072        case kIOMessageCanSystemSleep:
3073            allowSleep = YES;
3074            if ([fDefaults boolForKey: @"SleepPrevent"])
3075            {
3076                //prevent idle sleep unless no torrents are active
3077                enumerator = [fTorrents objectEnumerator];
3078                while ((torrent = [enumerator nextObject]))
3079                    if ([torrent isActive] && ![torrent isStalled] && ![torrent isError])
3080                    {
3081                        allowSleep = NO;
3082                        break;
3083                    }
3084            }
3085
3086            if (allowSleep)
3087                IOAllowPowerChange(fRootPort, (long) messageArgument);
3088            else
3089                IOCancelPowerChange(fRootPort, (long) messageArgument);
3090            break;
3091
3092        case kIOMessageSystemHasPoweredOn:
3093            //resume sleeping transfers after we wake up
3094            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
3095            [self autoSpeedLimitChange: nil];
3096            break;
3097    }
3098}
3099
3100- (NSMenu *) applicationDockMenu: (NSApplication *) sender
3101{
3102    int seeding = 0, downloading = 0;
3103    NSEnumerator * enumerator = [fTorrents objectEnumerator];
3104    Torrent * torrent;
3105    while ((torrent = [enumerator nextObject]))
3106    {
3107        if ([torrent isSeeding])
3108            seeding++;
3109        else if ([torrent isActive])
3110            downloading++;
3111        else;
3112    }
3113   
3114    NSMenuItem * seedingItem = [fDockMenu itemWithTag: DOCK_SEEDING_TAG],
3115            * downloadingItem = [fDockMenu itemWithTag: DOCK_DOWNLOADING_TAG];
3116   
3117    BOOL hasSeparator = seedingItem || downloadingItem;
3118   
3119    if (seeding > 0)
3120    {
3121        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Seeding",
3122                                                        "Dock item - Seeding"), seeding];
3123        if (!seedingItem)
3124        {
3125            seedingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
3126            [seedingItem setTag: DOCK_SEEDING_TAG];
3127            [fDockMenu insertItem: seedingItem atIndex: 0];
3128        }
3129        else
3130            [seedingItem setTitle: title];
3131    }
3132    else
3133    {
3134        if (seedingItem)
3135            [fDockMenu removeItem: seedingItem];
3136    }
3137   
3138    if (downloading > 0)
3139    {
3140        NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Downloading",
3141                                                        "Dock item - Downloading"), downloading];
3142        if (!downloadingItem)
3143        {
3144            downloadingItem = [[[NSMenuItem alloc] initWithTitle: title action: nil keyEquivalent: @""] autorelease];
3145            [downloadingItem setTag: DOCK_DOWNLOADING_TAG];
3146            [fDockMenu insertItem: downloadingItem atIndex: seeding > 0 ? 1 : 0];
3147        }
3148        else
3149            [downloadingItem setTitle: title];
3150    }
3151    else
3152    {
3153        if (downloadingItem)
3154            [fDockMenu removeItem: downloadingItem];
3155    }
3156   
3157    if (seeding > 0 || downloading > 0)
3158    {
3159        if (!hasSeparator)
3160            [fDockMenu insertItem: [NSMenuItem separatorItem] atIndex: seeding > 0 && downloading > 0 ? 2 : 1];
3161    }
3162    else
3163    {
3164        if (hasSeparator)
3165            [fDockMenu removeItemAtIndex: 0];
3166    }
3167   
3168    return fDockMenu;
3169}
3170
3171- (void) updateDockBadge: (NSNotification *) notification
3172{
3173    [fBadger updateBadge];
3174}
3175
3176- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
3177{
3178    //if auto size is enabled, the current frame shouldn't need to change
3179    NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame];
3180   
3181    frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH;
3182    return frame;
3183}
3184
3185- (void) setWindowSizeToFit
3186{
3187    if ([fDefaults boolForKey: @"AutoSize"])
3188    {
3189        [fScrollView setHasVerticalScroller: NO];
3190        [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES];
3191        [fScrollView setHasVerticalScroller: YES];
3192    }
3193}
3194
3195- (NSRect) sizedWindowFrame
3196{
3197    float heightChange = [fDisplayedTorrents count] * ([fTableView rowHeight] +
3198            [fTableView intercellSpacing].height) - [fScrollView frame].size.height;
3199    return [self windowFrameByAddingHeight: heightChange checkLimits: YES];
3200}
3201
3202- (void) showMainWindow: (id) sender
3203{
3204    [fWindow makeKeyAndOrderFront: nil];
3205}
3206
3207- (void) windowDidBecomeKey: (NSNotification *) notification
3208{
3209    [fStatusBar setNeedsDisplay: YES];
3210   
3211    [fBadger clearCompleted];
3212    [self updateUI];
3213}
3214
3215- (void) windowDidResignKey: (NSNotification *) notification
3216{
3217    [fStatusBar setNeedsDisplay: YES];
3218}
3219
3220- (NSSize) windowWillResize: (NSWindow *) sender toSize: (NSSize) proposedFrameSize
3221{
3222    //only resize horizontally if autosize is enabled
3223    if ([fDefaults boolForKey: @"AutoSize"])
3224        proposedFrameSize.height = [fWindow frame].size.height;
3225    return proposedFrameSize;
3226}
3227
3228- (void) windowDidResize: (NSNotification *) notification
3229{
3230    //size search filter to not overlap buttons
3231    float pointX = NSMaxX([fPauseFilterButton frame]) + 5.0;
3232    NSRect oldFrame = [fSearchFilterField frame],
3233            frame = NSMakeRect(pointX, oldFrame.origin.y, NSMaxX(oldFrame) - pointX, oldFrame.size.height);
3234   
3235    BOOL show;
3236    if (show = (frame.size.width >= SEARCH_FILTER_MIN_WIDTH))
3237    {
3238        //make sure it is not too long
3239        if (frame.size.width > SEARCH_FILTER_MAX_WIDTH)
3240        {
3241            float different = frame.size.width - SEARCH_FILTER_MAX_WIDTH;
3242            frame.origin.x += different;
3243            frame.size.width -= different;
3244        }
3245        [fSearchFilterField setFrame: frame];
3246    }
3247   
3248    //hide search filter if it overlaps filter buttons
3249    [fSearchFilterField setHidden: !show];
3250}
3251
3252- (void) applicationWillUnhide: (NSNotification *) notification
3253{
3254    [self updateUI];
3255}
3256
3257- (void) linkHomepage: (id) sender
3258{
3259    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
3260}
3261
3262- (void) linkForums: (id) sender
3263{
3264    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
3265}
3266
3267- (void) linkDonate: (id) sender
3268{
3269    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: DONATE_URL]];
3270}
3271
3272- (void) prepareForUpdate: (NSNotification *) notification
3273{
3274    fUpdateInProgress = YES;
3275}
3276
3277- (NSDictionary *) registrationDictionaryForGrowl
3278{
3279    NSArray * notifications = [NSArray arrayWithObjects: GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE,
3280                                                            GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT, nil];
3281    return [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL,
3282                                notifications, GROWL_NOTIFICATIONS_DEFAULT, nil];
3283}
3284
3285- (void) growlNotificationWasClicked: (id) clickContext
3286{
3287    if (!clickContext || ![clickContext isKindOfClass: [NSDictionary class]])
3288        return;
3289   
3290    NSString * type = [clickContext objectForKey: @"Type"], * location;
3291    if (([type isEqualToString: GROWL_DOWNLOAD_COMPLETE] || [type isEqualToString: GROWL_SEEDING_COMPLETE])
3292            && (location = [clickContext objectForKey: @"Location"]))
3293        [[NSWorkspace sharedWorkspace] selectFile: location inFileViewerRootedAtPath: nil];
3294}
3295
3296- (void) ipcQuit
3297{
3298    fRemoteQuit = YES;
3299    [NSApp terminate: self];
3300}
3301
3302- (NSArray *) ipcGetTorrentsByID: (NSArray *) idlist
3303{
3304    if (!idlist)
3305        return fTorrents;
3306   
3307    NSMutableArray * torrents = [NSMutableArray array];
3308   
3309    NSEnumerator * torrentEnum = [fTorrents objectEnumerator], * idEnum;
3310    int torId;
3311    Torrent * torrent;
3312    NSNumber * tempId;
3313    while ((torrent = [torrentEnum nextObject]))
3314    {
3315        torId = [torrent torrentID];
3316       
3317        idEnum = [idlist objectEnumerator];
3318        while ((tempId = [idEnum nextObject]))
3319        {
3320            if ([tempId intValue] == torId)
3321            {
3322                [torrents addObject: torrent];
3323                break;
3324            }
3325        }
3326    }
3327
3328    return torrents;
3329}
3330
3331- (NSArray *) ipcGetTorrentsByHash: (NSArray *) hashlist
3332{
3333    if (!hashlist)
3334        return fTorrents;
3335   
3336    NSMutableArray * torrents = [NSMutableArray array];
3337   
3338    NSEnumerator * torrentEnum = [fTorrents objectEnumerator], * hashEnum;
3339    NSString * torHash, * tempHash;
3340    Torrent * torrent;
3341    while ((torrent = [torrentEnum nextObject]))
3342    {
3343        torHash = [torrent hashString];
3344       
3345        hashEnum = [hashlist objectEnumerator];
3346        while ((tempHash = [hashEnum nextObject]))
3347        {
3348            if ([torHash caseInsensitiveCompare: tempHash] == NSOrderedSame)
3349            {
3350                [torrents addObject: torrent];
3351                break;
3352            }
3353        }
3354    }
3355   
3356    return torrents;
3357}
3358
3359- (BOOL) ipcAddTorrents: (NSArray *) torrents
3360{
3361    int oldCount = [fTorrents count];
3362   
3363    [self openFiles: torrents];
3364   
3365    return [fTorrents count] > oldCount;
3366}
3367
3368- (BOOL) ipcAddTorrentFile: (NSString *) path directory: (NSString *) directory
3369{
3370    int oldCount = [fTorrents count];
3371   
3372    [self openFiles: [NSArray arrayWithObject: path] forcePath: directory ignoreDownloadFolder: NO
3373            deleteTorrentFile: TORRENT_FILE_DEFAULT];
3374   
3375    return [fTorrents count] > oldCount;
3376}
3377
3378- (BOOL) ipcAddTorrentFileAutostart: (NSString *) path directory: (NSString *) directory autostart: (BOOL) autostart
3379{
3380    /* 'path' is path to torrent file, 'dir' is the directory it
3381       should download it's files to and may be nil, 'autostart' is a
3382       boolean indicating if the torrent should be automatically
3383       started (or queued to start, I guess), should return NO if
3384       torrent fails to load */
3385    return NO;
3386}
3387
3388- (BOOL) ipcAddTorrentData: (NSData *) data directory: (NSString *) directory
3389{
3390    /* 'data' is the contents of a torrent file, 'directory' is the
3391       directory it should download it's files to and may be nil,
3392       should return NO if torrent fails to load */
3393    return NO;
3394}
3395
3396- (BOOL) ipcAddTorrentDataAutostart: (NSData *) path directory: (NSString *) directory autostart: (BOOL) autostart
3397{
3398    /* 'data' is the contents of a torrent file, 'directory' is the
3399       directory it should download it's files to and may be nil,
3400       'autostart' is a boolean indicating if the torrent should be
3401       automatically started (or queued to start, I guess), should
3402       return NO if torrent fails to load */
3403    return NO;
3404}
3405
3406- (BOOL) ipcStartTorrents: (NSArray *) torrents
3407{
3408    if (!torrents)
3409        [self resumeAllTorrents: self];
3410    else
3411        [self resumeTorrents: torrents];
3412
3413    return YES;
3414}
3415
3416- (BOOL) ipcStopTorrents: (NSArray *) torrents
3417{
3418    if (!torrents)
3419        [self stopAllTorrents: self];
3420    else
3421        [self stopTorrents: torrents];
3422
3423    return YES;
3424}
3425
3426- (BOOL) ipcRemoveTorrents: (NSArray *) torrents
3427{
3428    if (!torrents)
3429        torrents = [NSArray arrayWithArray: fTorrents];
3430
3431    [self confirmRemoveTorrents: torrents deleteData: NO deleteTorrent: NO];
3432
3433    return YES;
3434}
3435
3436@end
Note: See TracBrowser for help on using the repository browser.