source: trunk/macosx/Controller.m @ 552

Last change on this file since 552 was 552, checked in by livings124, 16 years ago

Rescaled bandwidth icon from Thomas Bohn.

  • Property svn:keywords set to Date Rev Author Id
File size: 62.6 KB
Line 
1/******************************************************************************
2 * $Id: Controller.m 552 2006-07-08 19:51:49Z livings124 $
3 *
4 * Copyright (c) 2005-2006 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#import <IOKit/IOMessage.h>
26
27#import "Controller.h"
28#import "Torrent.h"
29#import "TorrentCell.h"
30#import "TorrentTableView.h"
31#import "StringAdditions.h"
32
33#import <Sparkle/Sparkle.h>
34
35#define TOOLBAR_OPEN            @"Toolbar Open"
36#define TOOLBAR_REMOVE          @"Toolbar Remove"
37#define TOOLBAR_INFO            @"Toolbar Info"
38#define TOOLBAR_PAUSE_ALL       @"Toolbar Pause All"
39#define TOOLBAR_RESUME_ALL      @"Toolbar Resume All"
40#define TOOLBAR_PAUSE_SELECTED  @"Toolbar Pause Selected"
41#define TOOLBAR_RESUME_SELECTED @"Toolbar Resume Selected"
42
43#define TORRENT_TABLE_VIEW_DATA_TYPE    @"TorrentTableViewDataType"
44
45#define ROW_HEIGHT_REGULAR 65.0
46#define ROW_HEIGHT_SMALL 40.0
47
48#define WEBSITE_URL @"http://transmission.m0k.org/"
49#define FORUM_URL   @"http://transmission.m0k.org/forum/"
50
51#define GROWL_PATH  @"/Library/PreferencePanes/Growl.prefPane/Contents/Resources/GrowlHelperApp.app"
52
53static void sleepCallBack(void * controller, io_service_t y,
54        natural_t messageType, void * messageArgument)
55{
56    Controller * c = controller;
57    [c sleepCallBack: messageType argument: messageArgument];
58}
59
60
61@implementation Controller
62
63- (id) init
64{
65    if ((self = [super init]))
66    {
67        fLib = tr_init();
68        fTorrents = [[NSMutableArray alloc] initWithCapacity: 10];
69        fDefaults = [NSUserDefaults standardUserDefaults];
70        fInfoController = [[InfoWindowController alloc] initWithWindowNibName: @"InfoWindow"];
71        fPrefsController = [[PrefsController alloc] initWithWindowNibName: @"PrefsWindow"];
72        fBadger = [[Badger alloc] init];
73       
74        //check and register Growl if it is installed for this user or all users
75        NSFileManager * manager = [NSFileManager defaultManager];
76        fHasGrowl = [manager fileExistsAtPath: GROWL_PATH]
77            || [manager fileExistsAtPath: [NSHomeDirectory() stringByAppendingPathComponent: GROWL_PATH]];
78        [self growlRegister];
79    }
80    return self;
81}
82
83- (void) dealloc
84{
85    [[NSNotificationCenter defaultCenter] removeObserver: self];
86
87    [fTorrents release];
88    [fToolbar release];
89    [fInfoController release];
90    [fBadger release];
91    [fSortType release];
92    [fAutoImportedNames release];
93   
94    tr_close( fLib );
95    [super dealloc];
96}
97
98- (void) awakeFromNib
99{
100    [fPrefsController setPrefs: fLib];
101   
102    [fAdvancedBarItem setState: [fDefaults boolForKey: @"UseAdvancedBar"]];
103
104    fToolbar = [[NSToolbar alloc] initWithIdentifier: @"Transmission Toolbar"];
105    [fToolbar setDelegate: self];
106    [fToolbar setAllowsUserCustomization: YES];
107    [fToolbar setAutosavesConfiguration: YES];
108    [fWindow setToolbar: fToolbar];
109    [fWindow setDelegate: self];
110   
111    //window min height
112    NSSize contentMinSize = [fWindow contentMinSize];
113    contentMinSize.height = [[fWindow contentView] frame].size.height - [fScrollView frame].size.height
114                                + [fTableView rowHeight] + [fTableView intercellSpacing].height;
115    [fWindow setContentMinSize: contentMinSize];
116   
117    //set table size
118    if ([fDefaults boolForKey: @"SmallView"])
119    {
120        [fTableView setRowHeight: ROW_HEIGHT_SMALL];
121        [fSmallViewItem setState: NSOnState];
122    }
123   
124    //set info keyboard shortcuts
125    unichar ch = NSRightArrowFunctionKey;
126    [fNextInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & ch length: 1]];
127    ch = NSLeftArrowFunctionKey;
128    [fPrevInfoTabItem setKeyEquivalent: [NSString stringWithCharacters: & ch length: 1]];
129   
130    //set up status bar
131    NSRect statusBarFrame = [fStatusBar frame];
132    statusBarFrame.size.width = [fWindow frame].size.width;
133    [fStatusBar setFrame: statusBarFrame];
134   
135    NSView * contentView = [fWindow contentView];
136    [contentView addSubview: fStatusBar];
137    [fStatusBar setFrameOrigin: NSMakePoint(0, [fScrollView frame].origin.y + [fScrollView frame].size.height)];
138    [self showStatusBar: [fDefaults boolForKey: @"StatusBar"] animate: NO];
139   
140    //set speed limit
141    if ([fDefaults boolForKey: @"SpeedLimit"])
142    {
143        [fSpeedLimitItem setState: NSOnState];
144        [fSpeedLimitDockItem setState: NSOnState];
145        [fSpeedLimitButton setState: NSOnState];
146    }
147
148    [fActionButton setToolTip: @"Shortcuts for changing global settings."];
149    [fSpeedLimitButton setToolTip: @"Speed Limit overrides the total bandwidth limits with its own limits."];
150
151    [fTableView setTorrents: fTorrents];
152    [[fTableView tableColumnWithIdentifier: @"Torrent"] setDataCell: [[TorrentCell alloc] init]];
153
154    [fTableView registerForDraggedTypes: [NSArray arrayWithObjects: NSFilenamesPboardType,
155                                                        TORRENT_TABLE_VIEW_DATA_TYPE, nil]];
156
157    //register for sleep notifications
158    IONotificationPortRef notify;
159    io_object_t iterator;
160    if (fRootPort = IORegisterForSystemPower(self, & notify, sleepCallBack, & iterator))
161        CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify),
162                            kCFRunLoopCommonModes);
163    else
164        NSLog(@"Could not IORegisterForSystemPower");
165
166    //load torrents from history
167    Torrent * torrent;
168    NSDictionary * historyItem;
169    NSEnumerator * enumerator = [[fDefaults arrayForKey: @"History"] objectEnumerator];
170    while ((historyItem = [enumerator nextObject]))
171        if ((torrent = [[Torrent alloc] initWithHistory: historyItem lib: fLib]))
172        {
173            [fTorrents addObject: torrent];
174            [torrent release];
175        }
176   
177    [self torrentNumberChanged];
178   
179    //set sort
180    fSortType = [[fDefaults stringForKey: @"Sort"] retain];
181   
182    NSMenuItem * currentSortItem;
183    if ([fSortType isEqualToString: @"Name"])
184        currentSortItem = fNameSortItem;
185    else if ([fSortType isEqualToString: @"State"])
186        currentSortItem = fStateSortItem;
187    else if ([fSortType isEqualToString: @"Progress"])
188        currentSortItem = fProgressSortItem;
189    else if ([fSortType isEqualToString: @"Date"])
190        currentSortItem = fDateSortItem;
191    else
192        currentSortItem = fOrderSortItem;
193    [currentSortItem setState: NSOnState];
194   
195    //set upload limit action button
196    [fUploadLimitItem setTitle: [NSString stringWithFormat: @"Limit (%d KB/s)",
197                    [fDefaults integerForKey: @"UploadLimit"]]];
198    if ([fDefaults boolForKey: @"CheckUpload"])
199        [fUploadLimitItem setState: NSOnState];
200    else
201        [fUploadNoLimitItem setState: NSOnState];
202
203        //set download limit action menu
204    [fDownloadLimitItem setTitle: [NSString stringWithFormat: @"Limit (%d KB/s)",
205                    [fDefaults integerForKey: @"DownloadLimit"]]];
206    if ([fDefaults boolForKey: @"CheckDownload"])
207        [fDownloadLimitItem setState: NSOnState];
208    else
209        [fDownloadNoLimitItem setState: NSOnState];
210   
211    //set ratio action menu
212    [fRatioSetItem setTitle: [NSString stringWithFormat: @"Stop at Ratio (%.2f)",
213                                [fDefaults floatForKey: @"RatioLimit"]]];
214    if ([fDefaults boolForKey: @"RatioCheck"])
215        [fRatioSetItem setState: NSOnState];
216    else
217        [fRatioNotSetItem setState: NSOnState];
218   
219    //observe notifications
220    NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
221   
222    [nc addObserver: self selector: @selector(prepareForUpdate:)
223                    name: SUUpdaterWillRestartNotification object: nil];
224    fUpdateInProgress = NO;
225   
226    [nc addObserver: self selector: @selector(limitGlobalChange:)
227                    name: @"LimitGlobalChange" object: nil];
228   
229    [nc addObserver: self selector: @selector(ratioGlobalChange:)
230                    name: @"RatioGlobalChange" object: nil];
231   
232    //check to start another because of stopped torrent
233    [nc addObserver: self selector: @selector(checkWaitingForStopped:)
234                    name: @"StoppedDownloading" object: nil];
235   
236    //check all torrents for starting
237    [nc addObserver: self selector: @selector(globalStartSettingChange:)
238                    name: @"GlobalStartSettingChange" object: nil];
239
240    //check if torrent should now start
241    [nc addObserver: self selector: @selector(torrentStartSettingChange:)
242                    name: @"TorrentStartSettingChange" object: nil];
243   
244    //check if torrent should now start
245    [nc addObserver: self selector: @selector(torrentStoppedForRatio:)
246                    name: @"TorrentStoppedForRatio" object: nil];
247   
248    //change that just impacts the inspector
249    [nc addObserver: self selector: @selector(reloadInspectorSettings:)
250                    name: @"TorrentSettingChange" object: nil];
251   
252    //reset auto import
253    [nc addObserver: self selector: @selector(autoImportChange:)
254                    name: @"AutoImportSettingChange" object: nil];
255
256    //timer to update the interface every second
257    fCompleted = 0;
258    [self updateUI: nil];
259    fTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self
260        selector: @selector(updateUI:) userInfo: nil repeats: YES];
261    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSModalPanelRunLoopMode];
262    [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSEventTrackingRunLoopMode];
263   
264    //timer for auto import, will check every 15 seconds
265    fAutoImportedNames = [[NSArray alloc] init];
266   
267    [self checkAutoImportDirectory: nil];
268    fAutoImportTimer = [NSTimer scheduledTimerWithTimeInterval: 15.0 target: self
269        selector: @selector(checkAutoImportDirectory:) userInfo: nil repeats: YES];
270    [[NSRunLoop currentRunLoop] addTimer: fAutoImportTimer forMode: NSDefaultRunLoopMode];
271   
272    [self sortTorrents];
273   
274    [fWindow makeKeyAndOrderFront: nil];
275
276    //load info for no torrents
277    [fInfoController updateInfoForTorrents: [NSArray array]];
278    if ([fDefaults boolForKey: @"InfoVisible"])
279        [self showInfo: nil];
280}
281
282- (BOOL) applicationShouldHandleReopen: (NSApplication *) app hasVisibleWindows: (BOOL) visibleWindows
283{
284    if (![fWindow isVisible] && ![[fPrefsController window] isVisible])
285        [fWindow makeKeyAndOrderFront: nil];
286    return NO;
287}
288
289- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) sender
290{
291    if (!fUpdateInProgress && [fDefaults boolForKey: @"CheckQuit"])
292    {
293        int active = 0, downloading = 0;
294        Torrent * torrent;
295        NSEnumerator * enumerator = [fTorrents objectEnumerator];
296        while ((torrent = [enumerator nextObject]))
297            if ([torrent isActive])
298            {
299                active++;
300                if (![torrent isSeeding])
301                    downloading++;
302            }
303
304        if ([fDefaults boolForKey: @"CheckQuitDownloading"] ? downloading > 0 : active > 0)
305        {
306            NSString * message = active == 1
307                ? @"There is an active transfer. Do you really want to quit?"
308                : [NSString stringWithFormat:
309                    @"There are %d active transfers. Do you really want to quit?", active];
310
311            NSBeginAlertSheet(@"Confirm Quit", @"Quit", @"Cancel", nil, fWindow, self,
312                                @selector(quitSheetDidEnd:returnCode:contextInfo:),
313                                nil, nil, message);
314            return NSTerminateLater;
315        }
316    }
317
318    return NSTerminateNow;
319}
320
321- (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode
322    contextInfo: (void *) contextInfo
323{
324    [NSApp stopModal];
325    [NSApp replyToApplicationShouldTerminate: returnCode == NSAlertDefaultReturn];
326}
327
328- (void) applicationWillTerminate: (NSNotification *) notification
329{
330    //stop timers
331    [fAutoImportTimer invalidate];
332    [fTimer invalidate];
333   
334    //save history and stop running torrents
335    [self updateTorrentHistory];
336    [fTorrents makeObjectsPerformSelector: @selector(stopTransferForQuit)];
337   
338    //remember window states and close all windows
339    [fDefaults setBool: [[fInfoController window] isVisible] forKey: @"InfoVisible"];
340    [[NSApp windows] makeObjectsPerformSelector: @selector(close)];
341    [self showStatusBar: NO animate: NO];
342   
343    //clear badge
344    [fBadger clearBadge];
345
346    //end quickly if the app is updating
347    if (fUpdateInProgress)
348        return;
349
350    //wait for running transfers to stop (5 second timeout)
351    NSDate * start = [NSDate date];
352    BOOL timeUp = NO;
353   
354    NSEnumerator * enumerator = [fTorrents objectEnumerator];
355    Torrent * torrent;
356    while (!timeUp && (torrent = [enumerator nextObject]))
357        while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
358        {
359            usleep(100000);
360            [torrent update];
361        }
362   
363    [fTorrents release];
364}
365
366- (void) folderChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code
367    contextInfo: (Torrent *) torrent
368{
369    if (code == NSOKButton)
370    {
371        [torrent setDownloadFolder: [[openPanel filenames] objectAtIndex: 0]];
372        [torrent update];
373        [self attemptToStartAuto: torrent];
374        [fTorrents addObject: torrent];
375       
376        [self torrentNumberChanged];
377    }
378   
379    [NSApp stopModal];
380}
381
382- (void) application: (NSApplication *) sender openFiles: (NSArray *) filenames
383{
384    NSString * downloadChoice = [fDefaults stringForKey: @"DownloadChoice"], * torrentPath;
385    Torrent * torrent;
386    NSEnumerator * enumerator = [filenames objectEnumerator];
387    while ((torrentPath = [enumerator nextObject]))
388    {
389        if (!(torrent = [[Torrent alloc] initWithPath: torrentPath lib: fLib]))
390            continue;
391
392        //add it to the "File > Open Recent" menu
393        [[NSDocumentController sharedDocumentController]
394            noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]];
395
396        if ([downloadChoice isEqualToString: @"Ask"])
397        {
398            NSOpenPanel * panel = [NSOpenPanel openPanel];
399
400            [panel setPrompt: @"Select Download Folder"];
401            [panel setAllowsMultipleSelection: NO];
402            [panel setCanChooseFiles: NO];
403            [panel setCanChooseDirectories: YES];
404
405            [panel setMessage: [NSString stringWithFormat: @"Select the download folder for \"%@\"", [torrent name]]];
406
407            [panel beginSheetForDirectory: nil file: nil types: nil
408                modalForWindow: fWindow modalDelegate: self didEndSelector:
409                @selector( folderChoiceClosed:returnCode:contextInfo: )
410                contextInfo: torrent];
411            [NSApp runModalForWindow: panel];
412        }
413        else
414        {
415            NSString * folder = [downloadChoice isEqualToString: @"Constant"]
416                ? [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath]
417                : [torrentPath stringByDeletingLastPathComponent];
418
419            [torrent setDownloadFolder: folder];
420            [torrent update];
421            [self attemptToStartAuto: torrent];
422            [fTorrents addObject: torrent];
423        }
424       
425        [torrent release];
426    }
427
428    [self torrentNumberChanged];
429
430    [self updateUI: nil];
431    [self sortTorrents];
432    [self updateTorrentHistory];
433}
434
435- (NSArray *) torrentsAtIndexes: (NSIndexSet *) indexSet
436{
437    if ([fTorrents respondsToSelector: @selector(objectsAtIndexes:)])
438        return [fTorrents objectsAtIndexes: indexSet];
439    else
440    {
441        NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [indexSet count]];
442        unsigned int i;
443        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
444            [torrents addObject: [fTorrents objectAtIndex: i]];
445
446        return torrents;
447    }
448}
449
450- (void) torrentNumberChanged
451{
452    int count = [fTorrents count];
453    [fTotalTorrentsField setStringValue: [NSString stringWithFormat:
454        @"%d Transfer%s", count, count == 1 ? "" : "s"]];
455}
456
457//called on by applescript
458- (void) open: (NSArray *) files
459{
460    [self performSelectorOnMainThread: @selector(openFromSheet:) withObject: files waitUntilDone: NO];
461}
462
463- (void) openShowSheet: (id) sender
464{
465    NSOpenPanel * panel = [NSOpenPanel openPanel];
466
467    [panel setAllowsMultipleSelection: YES];
468    [panel setCanChooseFiles: YES];
469    [panel setCanChooseDirectories: NO];
470
471    [panel beginSheetForDirectory: nil file: nil types: [NSArray arrayWithObject: @"torrent"]
472        modalForWindow: fWindow modalDelegate: self didEndSelector:
473        @selector(openSheetClosed:returnCode:contextInfo:) contextInfo: nil];
474}
475
476- (void) openSheetClosed: (NSOpenPanel *) panel returnCode: (int) code
477    contextInfo: (void *) info
478{
479    if (code == NSOKButton)
480        [self performSelectorOnMainThread: @selector(openFromSheet:)
481                    withObject: [panel filenames] waitUntilDone: NO];
482}
483
484- (void) openFromSheet: (NSArray *) filenames
485{
486    [self application: NSApp openFiles: filenames];
487}
488
489- (void) resumeTorrent: (id) sender
490{
491    [self resumeTorrentWithIndex: [fTableView selectedRowIndexes]];
492}
493
494- (void) resumeAllTorrents: (id) sender
495{
496    [self resumeTorrentWithIndex: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTorrents count])]];
497}
498
499- (void) resumeTorrentWithIndex: (NSIndexSet *) indexSet
500{
501    unsigned int i;
502    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
503        [[fTorrents objectAtIndex: i] startTransfer];
504   
505    [self updateUI: nil];
506    [fInfoController updateInfoStatsAndSettings];
507    [self updateTorrentHistory];
508}
509
510- (void) stopTorrent: (id) sender
511{
512    [self stopTorrentWithIndex: [fTableView selectedRowIndexes]];
513}
514
515- (void) stopAllTorrents: (id) sender
516{
517    [self stopTorrentWithIndex: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTorrents count])]];
518}
519
520- (void) stopTorrentWithIndex: (NSIndexSet *) indexSet
521{
522    //don't want any of these starting then stopping
523    unsigned int i;
524    Torrent * torrent;
525    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
526    {
527        torrent = [fTorrents objectAtIndex: i];
528        [torrent setWaitToStart: NO];
529    }
530
531    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
532        [[fTorrents objectAtIndex: i] stopTransfer];
533   
534    [self updateUI: nil];
535    [fInfoController updateInfoStatsAndSettings];
536    [self updateTorrentHistory];
537}
538
539- (void) removeWithIndex: (NSIndexSet *) indexSet
540        deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
541{
542    NSArray * torrents = [[self torrentsAtIndexes: indexSet] retain];
543    int active = 0, downloading = 0;
544
545    if ([fDefaults boolForKey: @"CheckRemove"])
546    {
547        Torrent * torrent;
548        NSEnumerator * enumerator = [torrents objectEnumerator];
549        while ((torrent = [enumerator nextObject]))
550            if ([torrent isActive])
551            {
552                active++;
553                if (![torrent isSeeding])
554                    downloading++;
555            }
556
557        if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? downloading > 0 : active > 0)
558        {
559            NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys:
560                torrents, @"Torrents",
561                [NSNumber numberWithBool: deleteData], @"DeleteData",
562                [NSNumber numberWithBool: deleteTorrent], @"DeleteTorrent", nil];
563
564            NSString * title, * message;
565           
566            int selected = [fTableView numberOfSelectedRows];
567            if (selected == 1)
568            {
569                title = [NSString stringWithFormat: @"Comfirm Removal of \"%@\"",
570                            [[fTorrents objectAtIndex: [fTableView selectedRow]] name]];
571                message = @"This transfer is active."
572                            " Once removed, continuing the transfer will require the torrent file."
573                            " Do you really want to remove it?";
574            }
575            else
576            {
577                title = [NSString stringWithFormat: @"Comfirm Removal of %d Transfers", selected];
578                if (selected == active)
579                    message = [NSString stringWithFormat:
580                        @"There are %d active transfers.", active];
581                else
582                    message = [NSString stringWithFormat:
583                        @"There are %d transfers (%d active).", selected, active];
584                message = [message stringByAppendingString:
585                    @" Once removed, continuing the transfers will require the torrent files."
586                    " Do you really want to remove them?"];
587            }
588
589            NSBeginAlertSheet(title, @"Remove", @"Cancel", nil, fWindow, self,
590                @selector(removeSheetDidEnd:returnCode:contextInfo:), nil, dict, message);
591            return;
592        }
593    }
594   
595    [self confirmRemove: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
596}
597
598- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (NSDictionary *) dict
599{
600    [NSApp stopModal];
601
602    NSArray * torrents = [dict objectForKey: @"Torrents"];
603    BOOL deleteData = [[dict objectForKey: @"DeleteData"] boolValue],
604        deleteTorrent = [[dict objectForKey: @"DeleteTorrent"] boolValue];
605    [dict release];
606   
607    if (returnCode == NSAlertDefaultReturn)
608        [self confirmRemove: torrents deleteData: deleteData deleteTorrent: deleteTorrent];
609    else
610        [torrents release];
611}
612
613- (void) confirmRemove: (NSArray *) torrents deleteData: (BOOL) deleteData deleteTorrent: (BOOL) deleteTorrent
614{
615    //don't want any of these starting then stopping
616    NSEnumerator * enumerator = [torrents objectEnumerator];
617    Torrent * torrent;
618    while ((torrent = [enumerator nextObject]))
619        [torrent setWaitToStart: NO];
620
621    NSNumber * lowestOrderValue = [NSNumber numberWithInt: [torrents count]], * currentOrederValue;
622
623    enumerator = [torrents objectEnumerator];
624    while ((torrent = [enumerator nextObject]))
625    {
626        [torrent stopTransfer];
627
628        if (deleteData)
629            [torrent trashData];
630        if (deleteTorrent)
631            [torrent trashTorrent];
632       
633        //determine lowest order value
634        currentOrederValue = [torrent orderValue];
635        if ([lowestOrderValue compare: currentOrederValue] == NSOrderedDescending)
636            lowestOrderValue = currentOrederValue;
637
638        [torrent removeForever];
639        [fTorrents removeObject: torrent];
640    }
641    [torrents release];
642
643    //reset the order values if necessary
644    if ([lowestOrderValue intValue] < [fTorrents count])
645    {
646        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
647                                                @"orderValue" ascending: YES] autorelease];
648        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
649
650        NSArray * tempTorrents = [fTorrents sortedArrayUsingDescriptors: descriptors];
651        [descriptors release];
652
653        int i;
654        for (i = [lowestOrderValue intValue]; i < [tempTorrents count]; i++)
655            [[tempTorrents objectAtIndex: i] setOrderValue: i];
656    }
657   
658    [self torrentNumberChanged];
659    [fTableView deselectAll: nil];
660    [self updateUI: nil];
661    [self updateTorrentHistory];
662}
663
664- (void) removeNoDelete: (id) sender
665{
666    [self removeWithIndex: [fTableView selectedRowIndexes] deleteData: NO deleteTorrent: NO];
667}
668
669- (void) removeDeleteData: (id) sender
670{
671    [self removeWithIndex: [fTableView selectedRowIndexes] deleteData: YES deleteTorrent: NO];
672}
673
674- (void) removeDeleteTorrent: (id) sender
675{
676    [self removeWithIndex: [fTableView selectedRowIndexes] deleteData: NO deleteTorrent: YES];
677}
678
679- (void) removeDeleteBoth: (id) sender
680{
681    [self removeWithIndex: [fTableView selectedRowIndexes] deleteData: YES deleteTorrent: YES];
682}
683
684- (void) copyTorrentFile: (id) sender
685{
686    [self copyTorrentFileForTorrents: [[NSMutableArray alloc] initWithArray:
687            [self torrentsAtIndexes: [fTableView selectedRowIndexes]]]];
688}
689
690- (void) copyTorrentFileForTorrents: (NSMutableArray *) torrents
691{
692    if ([torrents count] == 0)
693    {
694        [torrents release];
695        return;
696    }
697
698    Torrent * torrent = [torrents objectAtIndex: 0];
699
700    //warn user if torrent file can't be found
701    if (![[NSFileManager defaultManager] fileExistsAtPath: [torrent torrentLocation]])
702    {
703        NSAlert * alert = [[NSAlert alloc] init];
704        [alert addButtonWithTitle: @"OK"];
705        [alert setMessageText: [NSString stringWithFormat:
706                @"Copy of \"%@\" Cannot Be Created", [torrent name]]];
707        [alert setInformativeText: [NSString stringWithFormat:
708                @"The torrent file (%@) cannot be found.", [torrent torrentLocation]]];
709        [alert setAlertStyle: NSWarningAlertStyle];
710       
711        [alert runModal];
712       
713        [torrents removeObjectAtIndex: 0];
714        [self copyTorrentFileForTorrents: torrents];
715    }
716    else
717    {
718        NSSavePanel * panel = [NSSavePanel savePanel];
719        [panel setRequiredFileType: @"torrent"];
720        [panel setCanSelectHiddenExtension: YES];
721       
722        [panel beginSheetForDirectory: nil file: [torrent name]
723            modalForWindow: fWindow modalDelegate: self didEndSelector:
724            @selector(saveTorrentCopySheetClosed:returnCode:contextInfo:) contextInfo: torrents];
725    }
726}
727
728- (void) saveTorrentCopySheetClosed: (NSSavePanel *) panel returnCode: (int) code
729    contextInfo: (NSMutableArray *) torrents
730{
731    //if save successful, copy torrent to new location with name of data file
732    if (code == NSOKButton)
733        [[NSFileManager defaultManager] copyPath: [[torrents objectAtIndex: 0] torrentLocation]
734                toPath: [panel filename] handler: nil];
735   
736    [torrents removeObjectAtIndex: 0];
737    [self performSelectorOnMainThread: @selector(copyTorrentFileForTorrents:)
738                withObject: torrents waitUntilDone: NO];
739}
740
741- (void) revealFile: (id) sender
742{
743    NSIndexSet * indexSet = [fTableView selectedRowIndexes];
744    unsigned int i;
745   
746    for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
747        [[fTorrents objectAtIndex: i] revealData];
748}
749
750- (void) showPreferenceWindow: (id) sender
751{
752    NSWindow * window = [fPrefsController window];
753    if (![window isVisible])
754        [window center];
755
756    [window makeKeyAndOrderFront: nil];
757}
758
759- (void) showInfo: (id) sender
760{
761    if ([[fInfoController window] isVisible])
762        [fInfoController close];
763    else
764    {
765        [fInfoController updateInfoStats];
766        [[fInfoController window] orderFront: nil];
767    }
768}
769
770- (void) setInfoTab: (id) sender
771{
772    if (sender == fNextInfoTabItem)
773        [fInfoController setNextTab];
774    else
775        [fInfoController setPreviousTab];
776}
777
778- (void) updateUI: (NSTimer *) t
779{
780    NSEnumerator * enumerator = [fTorrents objectEnumerator];
781    Torrent * torrent;
782    while ((torrent = [enumerator nextObject]))
783    {
784        [torrent update];
785
786        if ([torrent justFinished])
787        {
788            [self checkWaitingForFinished: torrent];
789       
790            //notifications
791            [self notifyGrowl: @"Download Complete" message: [[torrent name] stringByAppendingString:
792                                                    @" finished downloading"] identifier: @"Download Complete"];
793            if (![fWindow isKeyWindow])
794                fCompleted++;
795        }
796    }
797
798    if ([fSortType isEqualToString: @"Progress"] || [fSortType isEqualToString: @"State"])
799        [self sortTorrents];
800    else
801        [fTableView reloadData];
802   
803    //update the global DL/UL rates
804    float downloadRate, uploadRate;
805    tr_torrentRates(fLib, & downloadRate, & uploadRate);
806    if (fStatusBarVisible)
807    {
808        [fTotalDLField setStringValue: [NSString stringForSpeed: downloadRate]];
809        [fTotalULField setStringValue: [NSString stringForSpeed: uploadRate]];
810    }
811
812    if ([[fInfoController window] isVisible])
813        [fInfoController updateInfoStats];
814
815    //badge dock
816    [fBadger updateBadgeWithCompleted: fCompleted
817        uploadRate: uploadRate downloadRate: downloadRate];
818}
819
820- (void) updateTorrentHistory
821{
822    NSMutableArray * history = [NSMutableArray
823        arrayWithCapacity: [fTorrents count]];
824
825    NSEnumerator * enumerator = [fTorrents objectEnumerator];
826    Torrent * torrent;
827    while( ( torrent = [enumerator nextObject] ) )
828        [history addObject: [torrent history]];
829
830    [fDefaults setObject: history forKey: @"History"];
831    [fDefaults synchronize];
832}
833
834- (void) sortTorrents
835{
836    //remember selected rows if needed
837    NSArray * selectedTorrents = nil;
838    int numSelected = [fTableView numberOfSelectedRows];
839    if (numSelected > 0 && numSelected < [fTorrents count])
840        selectedTorrents = [self torrentsAtIndexes: [fTableView selectedRowIndexes]];
841
842    NSSortDescriptor * nameDescriptor = [[[NSSortDescriptor alloc] initWithKey:
843                                            @"name" ascending: YES] autorelease],
844                    * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
845                                            @"orderValue" ascending: YES] autorelease];
846
847    NSArray * descriptors;
848    if ([fSortType isEqualToString: @"Name"])
849        descriptors = [[NSArray alloc] initWithObjects: nameDescriptor, orderDescriptor, nil];
850    else if ([fSortType isEqualToString: @"State"])
851    {
852        NSSortDescriptor * stateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
853                                                @"stateSortKey" ascending: NO] autorelease],
854                        * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
855                                            @"progressSortKey" ascending: NO] autorelease];
856       
857        descriptors = [[NSArray alloc] initWithObjects: stateDescriptor, progressDescriptor,
858                                                            nameDescriptor, orderDescriptor, nil];
859    }
860    else if ([fSortType isEqualToString: @"Progress"])
861    {
862        NSSortDescriptor * progressDescriptor = [[[NSSortDescriptor alloc] initWithKey:
863                                            @"progressSortKey" ascending: YES] autorelease];
864       
865        descriptors = [[NSArray alloc] initWithObjects: progressDescriptor, nameDescriptor, orderDescriptor, nil];
866    }
867    else if ([fSortType isEqualToString: @"Date"])
868    {
869        NSSortDescriptor * dateDescriptor = [[[NSSortDescriptor alloc] initWithKey:
870                                            @"date" ascending: YES] autorelease];
871   
872        descriptors = [[NSArray alloc] initWithObjects: dateDescriptor, orderDescriptor, nil];
873    }
874    else
875        descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
876
877    [fTorrents sortUsingDescriptors: descriptors];
878   
879    [descriptors release];
880   
881    [fTableView reloadData];
882   
883    //set selected rows if needed
884    if (selectedTorrents)
885    {
886        Torrent * torrent;
887        NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
888        NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
889        while ((torrent = [enumerator nextObject]))
890            [indexSet addIndex: [fTorrents indexOfObject: torrent]];
891       
892        [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
893        [indexSet release];
894    }
895}
896
897- (void) setSort: (id) sender
898{
899    NSMenuItem * prevSortItem;
900    if ([fSortType isEqualToString: @"Name"])
901        prevSortItem = fNameSortItem;
902    else if ([fSortType isEqualToString: @"State"])
903        prevSortItem = fStateSortItem;
904    else if ([fSortType isEqualToString: @"Progress"])
905        prevSortItem = fProgressSortItem;
906    else if ([fSortType isEqualToString: @"Date"])
907        prevSortItem = fDateSortItem;
908    else
909        prevSortItem = fOrderSortItem;
910   
911    if (sender != prevSortItem)
912    {
913        [prevSortItem setState: NSOffState];
914        [sender setState: NSOnState];
915
916        [fSortType release];
917        if (sender == fNameSortItem)
918            fSortType = [[NSString alloc] initWithString: @"Name"];
919        else if (sender == fStateSortItem)
920            fSortType = [[NSString alloc] initWithString: @"State"];
921        else if (sender == fProgressSortItem)
922            fSortType = [[NSString alloc] initWithString: @"Progress"];
923        else if (sender == fDateSortItem)
924            fSortType = [[NSString alloc] initWithString: @"Date"];
925        else
926            fSortType = [[NSString alloc] initWithString: @"Order"];
927           
928        [fDefaults setObject: fSortType forKey: @"Sort"];
929    }
930
931    [self sortTorrents];
932}
933
934- (void) toggleSpeedLimit: (id) sender
935{
936    int state = [fSpeedLimitItem state] ? NSOffState : NSOnState;
937
938    [fSpeedLimitItem setState: state];
939    [fSpeedLimitDockItem setState: state];
940    [fSpeedLimitButton setState: state];
941   
942    [fPrefsController enableSpeedLimit: state];
943}
944
945- (void) setLimitGlobalEnabled: (id) sender
946{
947    [fPrefsController setLimitEnabled: (sender == fUploadLimitItem || sender == fDownloadLimitItem)
948        type: (sender == fUploadLimitItem || sender == fUploadNoLimitItem) ? @"Upload" : @"Download"];
949}
950
951- (void) setQuickLimitGlobal: (id) sender
952{
953    [fPrefsController setQuickLimit: [[sender title] intValue]
954        type: [sender menu] == fUploadMenu ? @"Upload" : @"Download"];
955}
956
957- (void) limitGlobalChange: (NSNotification *) notification
958{
959    NSDictionary * dict = [notification object];
960   
961    NSMenuItem * limitItem, * noLimitItem;
962    if ([[dict objectForKey: @"Type"] isEqualToString: @"Upload"])
963    {
964        limitItem = fUploadLimitItem;
965        noLimitItem = fUploadNoLimitItem;
966    }
967    else
968    {
969        limitItem = fDownloadLimitItem;
970        noLimitItem = fDownloadNoLimitItem;
971    }
972   
973    BOOL enable = [[dict objectForKey: @"Enable"] boolValue];
974    [limitItem setState: enable ? NSOnState : NSOffState];
975    [noLimitItem setState: !enable ? NSOnState : NSOffState];
976   
977    [limitItem setTitle: [NSString stringWithFormat: @"Limit (%d KB/s)",
978                            [[dict objectForKey: @"Limit"] intValue]]];
979
980    [dict release];
981}
982
983- (void) setRatioGlobalEnabled: (id) sender
984{
985    [fPrefsController setRatioEnabled: sender == fRatioSetItem];
986}
987
988- (void) setQuickRatioGlobal: (id) sender
989{
990    [fPrefsController setQuickRatio: [[sender title] floatValue]];
991}
992
993- (void) ratioGlobalChange: (NSNotification *) notification
994{
995    NSDictionary * dict = [notification object];
996   
997    BOOL enable = [[dict objectForKey: @"Enable"] boolValue];
998    [fRatioSetItem setState: enable ? NSOnState : NSOffState];
999    [fRatioNotSetItem setState: !enable ? NSOnState : NSOffState];
1000   
1001    [fRatioSetItem setTitle: [NSString stringWithFormat: @"Stop at Ratio (%.2f)",
1002                            [[dict objectForKey: @"Ratio"] floatValue]]];
1003
1004    [dict release];
1005}
1006
1007- (void) checkWaitingForStopped: (NSNotification *) notification
1008{
1009    [self checkWaitingForFinished: [notification object]];
1010}
1011
1012- (void) checkWaitingForFinished: (Torrent *) finishedTorrent
1013{
1014    //don't try to start a transfer if there should be none waiting
1015    if (![[fDefaults stringForKey: @"StartSetting"] isEqualToString: @"Wait"])
1016        return;
1017
1018    int desiredActive = [fDefaults integerForKey: @"WaitToStartNumber"];
1019   
1020    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1021    Torrent * torrent, * torrentToStart = nil;
1022    while ((torrent = [enumerator nextObject]))
1023    {
1024        //ignore the torrent just stopped
1025        if (torrent == finishedTorrent)
1026            continue;
1027   
1028        if ([torrent isActive])
1029        {
1030            if (![torrent isSeeding])
1031            {
1032                desiredActive--;
1033                if (desiredActive <= 0)
1034                    return;
1035            }
1036        }
1037        else
1038        {
1039            //use as next if it is waiting to start and either no previous or order value is lower
1040            if ([torrent waitingToStart] && (!torrentToStart
1041                || [[torrentToStart orderValue] compare: [torrent orderValue]] == NSOrderedDescending))
1042                torrentToStart = torrent;
1043        }
1044    }
1045   
1046    //since it hasn't returned, the queue amount has not been met
1047    if (torrentToStart)
1048    {
1049        [torrentToStart startTransfer];
1050       
1051        [self updateUI: nil];
1052        [fInfoController updateInfoStatsAndSettings];
1053        [self updateTorrentHistory];
1054    }
1055}
1056
1057- (void) torrentStartSettingChange: (NSNotification *) notification
1058{
1059    [self attemptToStartMultipleAuto: [notification object]];
1060
1061    [self updateUI: nil];
1062    [fInfoController updateInfoStatsAndSettings];
1063    [self updateTorrentHistory];
1064}
1065
1066- (void) globalStartSettingChange: (NSNotification *) notification
1067{
1068    [self attemptToStartMultipleAuto: fTorrents];
1069   
1070    [self updateUI: nil];
1071    [fInfoController updateInfoStatsAndSettings];
1072    [self updateTorrentHistory];
1073}
1074
1075- (void) torrentStoppedForRatio: (NSNotification *) notification
1076{
1077    [fInfoController updateInfoStatsAndSettings];
1078   
1079    [self notifyGrowl: @"Seeding Complete" message: [[[notification object] name] stringByAppendingString:
1080                                                        @" finished seeding"] identifier: @"Seeding Complete"];
1081}
1082
1083- (void) attemptToStartAuto: (Torrent *) torrent
1084{
1085    [self attemptToStartMultipleAuto: [NSArray arrayWithObject: torrent]];
1086}
1087
1088//will try to start, taking into consideration the start preference
1089- (void) attemptToStartMultipleAuto: (NSArray *) torrents
1090{
1091    NSString * startSetting = [fDefaults stringForKey: @"StartSetting"];
1092    if ([startSetting isEqualToString: @"Start"])
1093    {
1094        NSEnumerator * enumerator = [torrents objectEnumerator];
1095        Torrent * torrent;
1096        while ((torrent = [enumerator nextObject]))
1097            if ([torrent waitingToStart])
1098                [torrent startTransfer];
1099       
1100        return;
1101    }
1102    else if (![startSetting isEqualToString: @"Wait"])
1103        return;
1104    else;
1105   
1106    //determine the number of downloads needed to start
1107    int desiredActive = [fDefaults integerForKey: @"WaitToStartNumber"];
1108           
1109    NSEnumerator * enumerator = [fTorrents objectEnumerator];
1110    Torrent * torrent;
1111    while ((torrent = [enumerator nextObject]))
1112        if ([torrent isActive] && ![torrent isSeeding])
1113        {
1114            desiredActive--;
1115            if (desiredActive <= 0)
1116                break;
1117        }
1118   
1119    //sort torrents by order value
1120    NSArray * sortedTorrents;
1121    if ([torrents count] > 1 && desiredActive > 0)
1122    {
1123        NSSortDescriptor * orderDescriptor = [[[NSSortDescriptor alloc] initWithKey:
1124                                                    @"orderValue" ascending: YES] autorelease];
1125        NSArray * descriptors = [[NSArray alloc] initWithObjects: orderDescriptor, nil];
1126       
1127        sortedTorrents = [torrents sortedArrayUsingDescriptors: descriptors];
1128        [descriptors release];
1129    }
1130    else
1131        sortedTorrents = torrents;
1132
1133    enumerator = [sortedTorrents objectEnumerator];
1134    while ((torrent = [enumerator nextObject]))
1135    {
1136        if ([torrent progress] >= 1.0)
1137            [torrent startTransfer];
1138        else
1139        {
1140            if ([torrent waitingToStart] && desiredActive > 0)
1141            {
1142                [torrent startTransfer];
1143                desiredActive--;
1144            }
1145        }
1146       
1147        [torrent update];
1148    }
1149}
1150
1151- (void) reloadInspectorSettings: (NSNotification *) notification
1152{
1153    [fInfoController updateInfoStatsAndSettings];
1154}
1155
1156- (void) checkAutoImportDirectory: (NSTimer *) timer
1157{
1158    if (![fDefaults boolForKey: @"AutoImport"])
1159        return;
1160       
1161    NSString * path = [[fDefaults stringForKey: @"AutoImportDirectory"] stringByExpandingTildeInPath];
1162   
1163    //if folder cannot be found or the contents hasn't changed simply give up
1164    NSArray * allFileNames;
1165    if (!(allFileNames = [[NSFileManager defaultManager] directoryContentsAtPath: path])
1166            || [allFileNames isEqualToArray: fAutoImportedNames])
1167        return;
1168
1169    //try to add files that haven't already been added
1170    NSMutableArray * newFileNames = [NSMutableArray arrayWithArray: allFileNames];
1171    [newFileNames removeObjectsInArray: fAutoImportedNames];
1172   
1173    //save the current list of files
1174    [fAutoImportedNames release];
1175    fAutoImportedNames = [allFileNames retain];
1176   
1177    NSEnumerator * enumerator = [newFileNames objectEnumerator];
1178    NSString * file;
1179    unsigned oldCount;
1180    while ((file = [enumerator nextObject]))
1181        if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1182        {
1183            oldCount = [fTorrents count];
1184            [self openFromSheet: [NSArray arrayWithObject: [path stringByAppendingPathComponent: file]]];
1185           
1186            //import only actually happened if the torrent array is larger
1187            if (oldCount < [fTorrents count])
1188                [self notifyGrowl: @"Torrent File Auto Added" message: [file stringByAppendingString:
1189                                                    @" added to Transmission"] identifier: @"Torrent Auto Added"];
1190        }
1191}
1192
1193- (void) autoImportChange: (NSNotification *) notification
1194{
1195    [fAutoImportedNames release];
1196    fAutoImportedNames = [[NSArray alloc] init];
1197   
1198    [self checkAutoImportDirectory: nil];
1199}
1200
1201- (int) numberOfRowsInTableView: (NSTableView *) t
1202{
1203    return [fTorrents count];
1204}
1205
1206- (void) tableView: (NSTableView *) t willDisplayCell: (id) cell
1207    forTableColumn: (NSTableColumn *) tableColumn row: (int) row
1208{
1209    [cell setTorrent: [fTorrents objectAtIndex: row]];
1210}
1211
1212- (BOOL) tableView: (NSTableView *) tableView writeRowsWithIndexes: (NSIndexSet *) indexes
1213    toPasteboard: (NSPasteboard *) pasteboard
1214{
1215    if ([fSortType isEqualToString: @"Order"])
1216    {
1217        [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self];
1218        [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexes]
1219                                forType: TORRENT_TABLE_VIEW_DATA_TYPE];
1220        return YES;
1221    }
1222    return NO;
1223}
1224
1225- (NSDragOperation) tableView: (NSTableView *) t validateDrop: (id <NSDraggingInfo>) info
1226    proposedRow: (int) row proposedDropOperation: (NSTableViewDropOperation) operation
1227{
1228    NSPasteboard * pasteboard = [info draggingPasteboard];
1229    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1230    {
1231        //check if any files to add have "torrent" as an extension
1232        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1233        NSString * file;
1234        while ((file = [enumerator nextObject]))
1235            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1236            {
1237                [fTableView setDropRow: -1 dropOperation: NSTableViewDropOn];
1238                return NSDragOperationGeneric;
1239            }
1240    }
1241    else if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE])
1242    {
1243        [fTableView setDropRow: row dropOperation: NSTableViewDropAbove];
1244        return NSDragOperationGeneric;
1245    }
1246    else;
1247   
1248    return NSDragOperationNone;
1249}
1250
1251- (BOOL) tableView: (NSTableView *) t acceptDrop: (id <NSDraggingInfo>) info
1252    row: (int) newRow dropOperation: (NSTableViewDropOperation) operation
1253{
1254    NSPasteboard * pasteboard = [info draggingPasteboard];
1255    if ([[pasteboard types] containsObject: NSFilenamesPboardType])
1256    {
1257        //create an array of files with the "torrent" extension
1258        NSMutableArray * filesToOpen = [[NSMutableArray alloc] init];
1259        NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator];
1260        NSString * file;
1261        while ((file = [enumerator nextObject]))
1262            if ([[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)
1263                [filesToOpen addObject: file];
1264   
1265        [self application: NSApp openFiles: filesToOpen];
1266        [filesToOpen release];
1267    }
1268    else
1269    {
1270        //remember selected rows if needed
1271        NSArray * selectedTorrents = nil;
1272        int numSelected = [fTableView numberOfSelectedRows];
1273        if (numSelected > 0 && numSelected < [fTorrents count])
1274            selectedTorrents = [self torrentsAtIndexes: [fTableView selectedRowIndexes]];
1275   
1276        NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData:
1277                                [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]];
1278       
1279        //move torrent in array
1280        NSArray * movingTorrents = [[self torrentsAtIndexes: indexes] retain];
1281        [fTorrents removeObjectsInArray: movingTorrents];
1282       
1283        //determine the insertion index now that transfers to move have been removed
1284        int i, decrease = 0;
1285        for (i = [indexes firstIndex]; i < newRow && i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
1286            decrease++;
1287       
1288        //insert objects at new location
1289        for (i = 0; i < [movingTorrents count]; i++)
1290            [fTorrents insertObject: [movingTorrents objectAtIndex: i] atIndex: newRow - decrease + i];
1291       
1292        [movingTorrents release];
1293       
1294        //redo order values
1295        int low = [indexes firstIndex], high = [indexes lastIndex];
1296        if (newRow < low)
1297            low = newRow;
1298        else if (newRow > high + 1)
1299            high = newRow - 1;
1300        else;
1301       
1302        for (i = low; i <= high; i++)
1303            [[fTorrents objectAtIndex: i] setOrderValue: i];
1304       
1305        [fTableView reloadData];
1306       
1307        //set selected rows if needed
1308        if (selectedTorrents)
1309        {
1310            Torrent * torrent;
1311            NSEnumerator * enumerator = [selectedTorrents objectEnumerator];
1312            NSMutableIndexSet * indexSet = [[NSMutableIndexSet alloc] init];
1313            while ((torrent = [enumerator nextObject]))
1314                [indexSet addIndex: [fTorrents indexOfObject: torrent]];
1315           
1316            [fTableView selectRowIndexes: indexSet byExtendingSelection: NO];
1317            [indexSet release];
1318        }
1319    }
1320   
1321    return YES;
1322}
1323
1324- (void) tableViewSelectionDidChange: (NSNotification *) notification
1325{
1326    [fInfoController updateInfoForTorrents: [self torrentsAtIndexes: [fTableView selectedRowIndexes]]];
1327}
1328
1329- (void) toggleSmallView: (id) sender
1330{
1331    BOOL makeSmall = ![fDefaults boolForKey: @"SmallView"];
1332   
1333    [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR];
1334    [fSmallViewItem setState: makeSmall];
1335   
1336    [fDefaults setBool: makeSmall forKey: @"SmallView"];
1337}
1338
1339- (void) toggleStatusBar: (id) sender
1340{
1341    [self showStatusBar: !fStatusBarVisible animate: YES];
1342    [fDefaults setBool: fStatusBarVisible forKey: @"StatusBar"];
1343}
1344
1345- (void) showStatusBar: (BOOL) show animate: (BOOL) animate
1346{
1347    if (show == fStatusBarVisible)
1348        return;
1349
1350    NSRect frame = [fWindow frame];
1351    float heightChange = [fStatusBar frame].size.height;
1352    if (!show)
1353        heightChange *= -1;
1354
1355    frame.size.height += heightChange;
1356    frame.origin.y -= heightChange;
1357       
1358    fStatusBarVisible = !fStatusBarVisible;
1359   
1360    [self updateUI: nil];
1361   
1362    //set views to not autoresize
1363    unsigned int statsMask = [fStatusBar autoresizingMask];
1364    unsigned int scrollMask = [fScrollView autoresizingMask];
1365    [fStatusBar setAutoresizingMask: 0];
1366    [fScrollView setAutoresizingMask: 0];
1367   
1368    [fWindow setFrame: frame display: YES animate: animate];
1369   
1370    //re-enable autoresize
1371    [fStatusBar setAutoresizingMask: statsMask];
1372    [fScrollView setAutoresizingMask: scrollMask];
1373   
1374    //change min size
1375    NSSize minSize = [fWindow contentMinSize];
1376    minSize.height += heightChange;
1377    [fWindow setContentMinSize: minSize];
1378}
1379
1380- (void) toggleAdvancedBar: (id) sender
1381{
1382    int state = ![fAdvancedBarItem state];
1383    [fAdvancedBarItem setState: state];
1384    [fDefaults setBool: state forKey: @"UseAdvancedBar"];
1385
1386    [fTableView display];
1387}
1388
1389- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier:
1390    (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag
1391{
1392    NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident];
1393
1394    if ([ident isEqualToString: TOOLBAR_OPEN])
1395    {
1396        [item setLabel: @"Open"];
1397        [item setPaletteLabel: @"Open Torrent Files"];
1398        [item setToolTip: @"Open torrent files"];
1399        [item setImage: [NSImage imageNamed: @"Open.png"]];
1400        [item setTarget: self];
1401        [item setAction: @selector(openShowSheet:)];
1402    }
1403    else if ([ident isEqualToString: TOOLBAR_REMOVE])
1404    {
1405        [item setLabel: @"Remove"];
1406        [item setPaletteLabel: @"Remove Selected"];
1407        [item setToolTip: @"Remove selected transfers"];
1408        [item setImage: [NSImage imageNamed: @"Remove.png"]];
1409        [item setTarget: self];
1410        [item setAction: @selector(removeNoDelete:)];
1411    }
1412    else if ([ident isEqualToString: TOOLBAR_INFO])
1413    {
1414        [item setLabel: @"Inspector"];
1415        [item setPaletteLabel: @"Show/Hide Inspector"];
1416        [item setToolTip: @"Display torrent inspector"];
1417        [item setImage: [NSImage imageNamed: @"Info.png"]];
1418        [item setTarget: self];
1419        [item setAction: @selector(showInfo:)];
1420    }
1421    else if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
1422    {
1423        [item setLabel: @"Pause All"];
1424        [item setPaletteLabel: [item label]];
1425        [item setToolTip: @"Pause all transfers"];
1426        [item setImage: [NSImage imageNamed: @"PauseAll.png"]];
1427        [item setTarget: self];
1428        [item setAction: @selector(stopAllTorrents:)];
1429    }
1430    else if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
1431    {
1432        [item setLabel: @"Resume All"];
1433        [item setPaletteLabel: [item label]];
1434        [item setToolTip: @"Resume all transfers"];
1435        [item setImage: [NSImage imageNamed: @"ResumeAll.png"]];
1436        [item setTarget: self];
1437        [item setAction: @selector(resumeAllTorrents:)];
1438    }
1439    else if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
1440    {
1441        [item setLabel: @"Pause"];
1442        [item setPaletteLabel: @"Pause Selected"];
1443        [item setToolTip: @"Pause selected transfers"];
1444        [item setImage: [NSImage imageNamed: @"PauseSelected.png"]];
1445        [item setTarget: self];
1446        [item setAction: @selector(stopTorrent:)];
1447    }
1448    else if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
1449    {
1450        [item setLabel: @"Resume"];
1451        [item setPaletteLabel: @"Resume Selected"];
1452        [item setToolTip: @"Resume selected transfers"];
1453        [item setImage: [NSImage imageNamed: @"ResumeSelected.png"]];
1454        [item setTarget: self];
1455        [item setAction: @selector(resumeTorrent:)];
1456    }
1457    else
1458    {
1459        [item release];
1460        return nil;
1461    }
1462
1463    return item;
1464}
1465
1466- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t
1467{
1468    return [NSArray arrayWithObjects:
1469            TOOLBAR_OPEN, TOOLBAR_REMOVE,
1470            TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED,
1471            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, TOOLBAR_INFO,
1472            NSToolbarSeparatorItemIdentifier,
1473            NSToolbarSpaceItemIdentifier,
1474            NSToolbarFlexibleSpaceItemIdentifier,
1475            NSToolbarCustomizeToolbarItemIdentifier, nil];
1476}
1477
1478- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t
1479{
1480    return [NSArray arrayWithObjects:
1481            TOOLBAR_OPEN, TOOLBAR_REMOVE,
1482            NSToolbarSeparatorItemIdentifier,
1483            TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL,
1484            NSToolbarFlexibleSpaceItemIdentifier,
1485            TOOLBAR_INFO, nil];
1486}
1487
1488- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
1489{
1490    NSString * ident = [toolbarItem itemIdentifier];
1491
1492    //enable remove item
1493    if ([ident isEqualToString: TOOLBAR_REMOVE])
1494        return [fTableView numberOfSelectedRows] > 0;
1495
1496    //enable pause all item
1497    if ([ident isEqualToString: TOOLBAR_PAUSE_ALL])
1498    {
1499        Torrent * torrent;
1500        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1501        while ((torrent = [enumerator nextObject]))
1502            if ([torrent isActive])
1503                return YES;
1504        return NO;
1505    }
1506
1507    //enable resume all item
1508    if ([ident isEqualToString: TOOLBAR_RESUME_ALL])
1509    {
1510        Torrent * torrent;
1511        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1512        while ((torrent = [enumerator nextObject]))
1513            if ([torrent isPaused])
1514                return YES;
1515        return NO;
1516    }
1517
1518    //enable pause item
1519    if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED])
1520    {
1521        Torrent * torrent;
1522        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1523        unsigned int i;
1524       
1525        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1526            if ([[fTorrents objectAtIndex: i] isActive])
1527                return YES;
1528        return NO;
1529    }
1530   
1531    //enable resume item
1532    if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED])
1533    {
1534        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1535        unsigned int i;
1536       
1537        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1538            if ([[fTorrents objectAtIndex: i] isPaused])
1539                return YES;
1540        return NO;
1541    }
1542
1543    return YES;
1544}
1545
1546- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
1547{
1548    SEL action = [menuItem action];
1549
1550    //only enable some items if it is in a context menu or the window is useable
1551    BOOL canUseMenu = [fWindow isKeyWindow] || [[[menuItem menu] title] isEqualToString: @"Context"];
1552
1553    //enable show info
1554    if (action == @selector(showInfo:))
1555    {
1556        NSString * title = [[fInfoController window] isVisible] ? @"Hide Inspector" : @"Show Inspector";
1557        if (![[menuItem title] isEqualToString: title])
1558                [menuItem setTitle: title];
1559
1560        return YES;
1561    }
1562   
1563    //enable prev/next inspector tab
1564    if (action == @selector(setInfoTab:))
1565        return [[fInfoController window] isVisible];
1566   
1567    //enable toggle status bar
1568    if (action == @selector(toggleStatusBar:))
1569    {
1570        NSString * title = fStatusBarVisible ? @"Hide Status Bar" : @"Show Status Bar";
1571        if (![[menuItem title] isEqualToString: title])
1572                [menuItem setTitle: title];
1573
1574        return canUseMenu;
1575    }
1576
1577    //enable resume all item
1578    if (action == @selector(resumeAllTorrents:))
1579    {
1580        Torrent * torrent;
1581        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1582        while ((torrent = [enumerator nextObject]))
1583            if ([torrent isPaused])
1584                return YES;
1585        return NO;
1586    }
1587
1588    //enable pause all item
1589    if (action == @selector(stopAllTorrents:))
1590    {
1591        Torrent * torrent;
1592        NSEnumerator * enumerator = [fTorrents objectEnumerator];
1593        while ((torrent = [enumerator nextObject]))
1594            if ([torrent isActive])
1595                return YES;
1596        return NO;
1597    }
1598
1599    //enable reveal in finder
1600    if (action == @selector(revealFile:))
1601        return canUseMenu && [fTableView numberOfSelectedRows] > 0;
1602
1603    //enable remove items
1604    if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)
1605        || action == @selector(removeDeleteTorrent:) || action == @selector(removeDeleteBoth:))
1606    {
1607        BOOL warning = NO,
1608            onlyDownloading = [fDefaults boolForKey: @"CheckRemoveDownloading"],
1609            canDelete = action != @selector(removeDeleteTorrent:) && action != @selector(removeDeleteBoth:);
1610        Torrent * torrent;
1611        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1612        unsigned int i;
1613       
1614        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1615        {
1616            torrent = [fTorrents objectAtIndex: i];
1617            if (!warning && [torrent isActive])
1618            {
1619                warning = onlyDownloading ? ![torrent isSeeding] : YES;
1620                if (warning && canDelete)
1621                    break;
1622            }
1623            if (!canDelete && [torrent publicTorrent])
1624            {
1625                canDelete = YES;
1626                if (warning)
1627                    break;
1628            }
1629        }
1630   
1631        //append or remove ellipsis when needed
1632        NSString * title = [menuItem title], * ellipsis = [NSString ellipsis];
1633        if (warning && [fDefaults boolForKey: @"CheckRemove"])
1634        {
1635            if (![title hasSuffix: ellipsis])
1636                [menuItem setTitle: [title stringByAppendingEllipsis]];
1637        }
1638        else
1639        {
1640            if ([title hasSuffix: ellipsis])
1641                [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]];
1642        }
1643       
1644        return canUseMenu && canDelete && [fTableView numberOfSelectedRows] > 0;
1645    }
1646
1647    //enable pause item
1648    if (action == @selector(stopTorrent:))
1649    {
1650        if (!canUseMenu)
1651            return NO;
1652   
1653        Torrent * torrent;
1654        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1655        unsigned int i;
1656       
1657        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1658        {
1659            torrent = [fTorrents objectAtIndex: i];
1660            if ([torrent isActive])
1661                return YES;
1662        }
1663        return NO;
1664    }
1665   
1666    //enable resume item
1667    if (action == @selector(resumeTorrent:))
1668    {
1669        if (!canUseMenu)
1670            return NO;
1671   
1672        Torrent * torrent;
1673        NSIndexSet * indexSet = [fTableView selectedRowIndexes];
1674        unsigned int i;
1675       
1676        for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
1677        {
1678            torrent = [fTorrents objectAtIndex: i];
1679            if ([torrent isPaused])
1680                return YES;
1681        }
1682        return NO;
1683    }
1684   
1685    //enable sort and advanced bar items
1686    if (action == @selector(setSort:) || action == @selector(toggleAdvancedBar:)
1687            || action == @selector(toggleSmallView:))
1688        return canUseMenu;
1689   
1690    //enable copy torrent file item
1691    if (action == @selector(copyTorrentFile:))
1692    {
1693        return canUseMenu && [fTableView numberOfSelectedRows] > 0;
1694    }
1695
1696    return YES;
1697}
1698
1699- (void) sleepCallBack: (natural_t) messageType argument: (void *) messageArgument
1700{
1701    NSEnumerator * enumerator;
1702    Torrent * torrent;
1703    BOOL active;
1704
1705    switch (messageType)
1706    {
1707        case kIOMessageSystemWillSleep:
1708            //close all connections before going to sleep and remember we should resume when we wake up
1709            [fTorrents makeObjectsPerformSelector: @selector(sleep)];
1710
1711            //wait for running transfers to stop (5 second timeout)
1712            NSDate * start = [NSDate date];
1713            BOOL timeUp = NO;
1714           
1715            NSEnumerator * enumerator = [fTorrents objectEnumerator];
1716            Torrent * torrent;
1717            while (!timeUp && (torrent = [enumerator nextObject]))
1718                while (![torrent isPaused] && !(timeUp = [start timeIntervalSinceNow] < -5.0))
1719                {
1720                    usleep(100000);
1721                    [torrent update];
1722                }
1723
1724            IOAllowPowerChange(fRootPort, (long) messageArgument);
1725            break;
1726
1727        case kIOMessageCanSystemSleep:
1728            /* Prevent idle sleep unless all paused */
1729            active = NO;
1730            enumerator = [fTorrents objectEnumerator];
1731            while ((torrent = [enumerator nextObject]))
1732                if ([torrent isActive])
1733                {
1734                    active = YES;
1735                    break;
1736                }
1737
1738            if (active)
1739                IOCancelPowerChange(fRootPort, (long) messageArgument);
1740            else
1741                IOAllowPowerChange(fRootPort, (long) messageArgument);
1742            break;
1743
1744        case kIOMessageSystemHasPoweredOn:
1745            //resume sleeping transfers after we wake up
1746            [fTorrents makeObjectsPerformSelector: @selector(wakeUp)];
1747            break;
1748    }
1749}
1750
1751- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
1752{
1753    NSRect windowRect = [fWindow frame];
1754    float newHeight = windowRect.size.height - [fScrollView frame].size.height
1755        + [fTorrents count] * ([fTableView rowHeight] + [fTableView intercellSpacing].height) + 30.0;
1756
1757    float minHeight = [fWindow minSize].height;
1758    if (newHeight < minHeight)
1759        newHeight = minHeight;
1760
1761    windowRect.origin.y -= (newHeight - windowRect.size.height);
1762    windowRect.size.height = newHeight;
1763
1764    return windowRect;
1765}
1766
1767- (void) showMainWindow: (id) sender
1768{
1769    [fWindow makeKeyAndOrderFront: nil];
1770}
1771
1772- (void) windowDidBecomeKey: (NSNotification *) notification
1773{
1774    fCompleted = 0;
1775}
1776
1777- (void) linkHomepage: (id) sender
1778{
1779    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]];
1780}
1781
1782- (void) linkForums: (id) sender
1783{
1784    [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]];
1785}
1786
1787- (void) notifyGrowl: (NSString *) title message: (NSString *) message identifier: (NSString *) ident
1788{
1789    if (!fHasGrowl)
1790        return;
1791
1792    NSString * growlScript = [NSString stringWithFormat:
1793        @"tell application \"System Events\"\n"
1794         "  if exists application process \"GrowlHelperApp\" then\n"
1795         "    tell application \"GrowlHelperApp\"\n "
1796         "      notify with name \"%@\""
1797         "        title \"%@\""
1798         "        description \"%@\""
1799         "        application name \"Transmission\"\n"
1800         "    end tell\n"
1801         "  end if\n"
1802         "end tell", ident, title, message];
1803   
1804    NSAppleScript * appleScript = [[NSAppleScript alloc] initWithSource: growlScript];
1805    NSDictionary * error;
1806    if (![appleScript executeAndReturnError: & error])
1807        NSLog(@"Growl notify failed");
1808    [appleScript release];
1809}
1810
1811- (void) growlRegister
1812{
1813    if (!fHasGrowl)
1814        return;
1815
1816    NSString * growlScript = @"tell application \"System Events\"\n"
1817         "  if exists application process \"GrowlHelperApp\" then\n"
1818         "    tell application \"GrowlHelperApp\"\n"
1819         "      register as application \"Transmission\" "
1820         "        all notifications {\"Download Complete\", \"Seeding Complete\", \"Torrent Auto Added\"}"
1821         "        default notifications {\"Download Complete\", \"Seeding Complete\", \"Torrent Auto Added\"}"
1822         "        icon of application \"Transmission\"\n"
1823         "    end tell\n"
1824         "  end if\n"
1825         "end tell";
1826
1827    NSAppleScript * appleScript = [[NSAppleScript alloc] initWithSource: growlScript];
1828    NSDictionary * error;
1829    if (![appleScript executeAndReturnError: & error])
1830        NSLog(@"Growl registration failed");
1831    [appleScript release];
1832}
1833
1834- (void) checkUpdate: (id) sender
1835{
1836    [fPrefsController checkUpdate];
1837}
1838
1839- (void) prepareForUpdate: (NSNotification *) notification
1840{
1841    fUpdateInProgress = YES;
1842}
1843
1844@end
Note: See TracBrowser for help on using the repository browser.