source: trunk/macosx/InfoWindowController.m @ 8023

Last change on this file since 8023 was 8023, checked in by livings124, 13 years ago

#8021 per-torrent vs. global speed limit confusion for mac ui

  • Property svn:keywords set to Date Rev Author Id
File size: 63.9 KB
Line 
1/******************************************************************************
2 * $Id: InfoWindowController.m 8023 2009-03-05 01:10:09Z livings124 $
3 *
4 * Copyright (c) 2006-2009 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 "InfoWindowController.h"
26#import "Torrent.h"
27#import "InfoTabButtonCell.h"
28#import "FileOutlineView.h"
29#import "FileOutlineController.h"
30#import "FileListNode.h"
31#import "PeerProgressIndicatorCell.h"
32#import "TrackerTableView.h"
33#import "PiecesView.h"
34#import "QuickLookController.h"
35#import "NSStringAdditions.h"
36#include "utils.h" //tr_getRatio()
37
38#define TAB_INFO_IDENT @"Info"
39#define TAB_ACTIVITY_IDENT @"Activity"
40#define TAB_TRACKER_IDENT @"Tracker"
41#define TAB_PEERS_IDENT @"Peers"
42#define TAB_FILES_IDENT @"Files"
43#define TAB_OPTIONS_IDENT @"Options"
44
45#define TAB_MIN_HEIGHT 250
46
47#define PIECES_CONTROL_PROGRESS 0
48#define PIECES_CONTROL_AVAILABLE 1
49
50#define OPTION_POPUP_GLOBAL 0
51#define OPTION_POPUP_NO_LIMIT 1
52#define OPTION_POPUP_LIMIT 2
53
54#define INVALID -99
55
56#define TRACKER_ADD_TAG 0
57#define TRACKER_REMOVE_TAG 1
58
59typedef enum
60{
61    TAB_INFO_TAG = 0,
62    TAB_ACTIVITY_TAG = 1,
63    TAB_TRACKER_TAG = 2,
64    TAB_PEERS_TAG = 3,
65    TAB_FILES_TAG = 4,
66    TAB_OPTIONS_TAG = 5
67} tabTag;
68
69@interface InfoWindowController (Private)
70
71- (void) updateInfoGeneral;
72- (void) updateInfoActivity;
73- (void) updateInfoTracker;
74- (void) updateInfoPeers;
75- (void) updateInfoFiles;
76
77- (NSView *) tabViewForTag: (NSInteger) tag;
78- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate;
79- (NSArray *) peerSortDescriptors;
80
81- (BOOL) canQuickLookFile: (FileListNode *) item;
82
83- (void) addTrackers;
84- (void) removeTrackers;
85
86@end
87
88@implementation InfoWindowController
89
90- (id) init
91{
92    return [super initWithWindowNibName: @"InfoWindow"];
93}
94
95- (void) awakeFromNib
96{
97    //window location and size
98    NSPanel * window = (NSPanel *)[self window];
99   
100    CGFloat windowHeight = [window frame].size.height;
101   
102    [window setFrameAutosaveName: @"InspectorWindow"];
103    [window setFrameUsingName: @"InspectorWindow"];
104   
105    NSRect windowRect = [window frame];
106    windowRect.origin.y -= windowHeight - windowRect.size.height;
107    windowRect.size.height = windowHeight;
108    [window setFrame: windowRect display: NO];
109   
110    [window setBecomesKeyOnlyIfNeeded: YES];
111   
112    //set tab images and tooltips
113    [[fTabMatrix cellWithTag: TAB_INFO_TAG] setIcon: [NSImage imageNamed: @"InfoGeneral.png"]];
114    [[fTabMatrix cellWithTag: TAB_ACTIVITY_TAG] setIcon: [NSImage imageNamed: @"InfoActivity.png"]];
115    [[fTabMatrix cellWithTag: TAB_TRACKER_TAG] setIcon: [NSImage imageNamed: @"InfoTracker.png"]];
116    [[fTabMatrix cellWithTag: TAB_PEERS_TAG] setIcon: [NSImage imageNamed: @"InfoPeers.png"]];
117    [[fTabMatrix cellWithTag: TAB_FILES_TAG] setIcon: [NSImage imageNamed: @"InfoFiles.png"]];
118    [[fTabMatrix cellWithTag: TAB_OPTIONS_TAG] setIcon: [NSImage imageNamed: @"InfoOptions.png"]];
119   
120    //set selected tab
121    fCurrentTabTag = INVALID;
122    NSString * identifier = [[NSUserDefaults standardUserDefaults] stringForKey: @"InspectorSelected"];
123    NSInteger tag;
124    if ([identifier isEqualToString: TAB_INFO_IDENT])
125        tag = TAB_INFO_TAG;
126    else if ([identifier isEqualToString: TAB_ACTIVITY_IDENT])
127        tag = TAB_ACTIVITY_TAG;
128    else if ([identifier isEqualToString: TAB_TRACKER_IDENT])
129        tag = TAB_TRACKER_TAG;
130    else if ([identifier isEqualToString: TAB_PEERS_IDENT])
131        tag = TAB_PEERS_TAG;
132    else if ([identifier isEqualToString: TAB_FILES_IDENT])
133        tag = TAB_FILES_TAG;
134    else if ([identifier isEqualToString: TAB_OPTIONS_IDENT])
135        tag = TAB_OPTIONS_TAG;
136    else //safety
137    {
138        [[NSUserDefaults standardUserDefaults] setObject: TAB_INFO_IDENT forKey: @"InspectorSelected"];
139        tag = TAB_INFO_TAG;
140    }
141    [fTabMatrix selectCellWithTag: tag];
142    [self setTab: nil];
143   
144    //reset images for reveal buttons, since the images are also used in the main table
145    NSImage * revealOn = [[NSImage imageNamed: @"RevealOn.png"] copy],
146            * revealOff = [[NSImage imageNamed: @"RevealOff.png"] copy];
147    [revealOn setFlipped: NO];
148    [revealOff setFlipped: NO];
149   
150    [fRevealDataButton setImage: revealOff];
151    [fRevealDataButton setAlternateImage: revealOn];
152    [fRevealTorrentButton setImage: revealOff];
153    [fRevealTorrentButton setAlternateImage: revealOn];
154   
155    [revealOn release];
156    [revealOff release];
157   
158    //initially sort peer table by IP
159    if ([[fPeerTable sortDescriptors] count] == 0)
160        [fPeerTable setSortDescriptors: [NSArray arrayWithObject: [[fPeerTable tableColumnWithIdentifier: @"IP"]
161                                            sortDescriptorPrototype]]];
162   
163    //initially sort webseed table by address
164    if ([[fWebSeedTable sortDescriptors] count] == 0)
165        [fWebSeedTable setSortDescriptors: [NSArray arrayWithObject: [[fWebSeedTable tableColumnWithIdentifier: @"Address"]
166                                            sortDescriptorPrototype]]];
167   
168    //set table header tool tips
169    [[fPeerTable tableColumnWithIdentifier: @"Encryption"] setHeaderToolTip: NSLocalizedString(@"Encrypted Connection",
170                                                                        "inspector -> peer table -> header tool tip")];
171    [[fPeerTable tableColumnWithIdentifier: @"Progress"] setHeaderToolTip: NSLocalizedString(@"Available",
172                                                                        "inspector -> peer table -> header tool tip")];
173    [[fPeerTable tableColumnWithIdentifier: @"UL To"] setHeaderToolTip: NSLocalizedString(@"Uploading To Peer",
174                                                                        "inspector -> peer table -> header tool tip")];
175    [[fPeerTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Peer",
176                                                                        "inspector -> peer table -> header tool tip")];
177   
178    [[fWebSeedTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Web Seed",
179                                                                        "inspector -> web seed table -> header tool tip")];
180   
181    //prepare for animating peer table and web seed table
182    NSRect webSeedTableFrame = [[fWebSeedTable enclosingScrollView] frame];
183    fWebSeedTableHeight = webSeedTableFrame.size.height;
184    fSpaceBetweenWebSeedAndPeer = webSeedTableFrame.origin.y - NSMaxY([[fPeerTable enclosingScrollView] frame]);
185   
186    [self setWebSeedTableHidden: YES animate: NO];
187   
188    //set blank inspector
189    [self setInfoForTorrents: [NSArray array]];
190   
191    //allow for update notifications
192    NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
193    [nc addObserver: self selector: @selector(updateInfoStats) name: @"UpdateStats" object: nil];
194    [nc addObserver: self selector: @selector(updateOptions) name: @"UpdateOptions" object: nil];
195}
196
197- (void) dealloc
198{
199    //save resizeable view height
200    NSString * resizeSaveKey = nil;
201    switch (fCurrentTabTag)
202    {
203        case TAB_TRACKER_TAG:
204            resizeSaveKey = @"InspectorContentHeightTracker";
205            break;
206        case TAB_PEERS_TAG:
207            resizeSaveKey = @"InspectorContentHeightPeers";
208            break;
209        case TAB_FILES_TAG:
210            resizeSaveKey = @"InspectorContentHeightFiles";
211            break;
212    }
213    if (resizeSaveKey)
214        [[NSUserDefaults standardUserDefaults] setFloat: [[self tabViewForTag: fCurrentTabTag] frame].size.height forKey: resizeSaveKey];
215   
216    [[NSNotificationCenter defaultCenter] removeObserver: self];
217   
218    [fTorrents release];
219    [fPeers release];
220    [fWebSeeds release];
221    [fTrackers release];
222   
223    [fWebSeedTableAnimation release];
224   
225    [super dealloc];
226}
227
228- (void) setInfoForTorrents: (NSArray *) torrents
229{
230    if (fTorrents && [fTorrents isEqualToArray: torrents])
231        return;
232   
233    [fTorrents release];
234    fTorrents = [torrents retain];
235
236    NSUInteger numberSelected = [fTorrents count];
237    if (numberSelected != 1)
238    {
239        if (numberSelected > 0)
240        {
241            [fImageView setImage: [NSImage imageNamed: NSImageNameMultipleDocuments]];
242           
243            [fNameField setStringValue: [NSString stringWithFormat: NSLocalizedString(@"%d Torrents Selected",
244                                            "Inspector -> selected torrents"), numberSelected]];
245       
246            uint64_t size = 0;
247            NSInteger fileCount = 0;
248            for (Torrent * torrent in torrents)
249            {
250                size += [torrent size];
251                fileCount += [torrent fileCount];
252            }
253           
254            [fBasicInfoField setStringValue: [NSString stringWithFormat: @"%@, %@",
255                [NSString stringWithFormat: NSLocalizedString(@"%d files", "Inspector -> selected torrents"), fileCount],
256                [NSString stringWithFormat: NSLocalizedString(@"%@ total", "Inspector -> selected torrents"),
257                [NSString stringForFileSize: size]]]];
258            [fBasicInfoField setToolTip: [NSString stringWithFormat: NSLocalizedString(@"%llu bytes", "Inspector -> selected torrents"),
259                                            size]];
260        }
261        else
262        {
263            [fImageView setImage: [NSImage imageNamed: @"NSApplicationIcon"]];
264           
265            [fNameField setStringValue: NSLocalizedString(@"No Torrents Selected", "Inspector -> selected torrents")];
266            [fBasicInfoField setStringValue: @""];
267            [fBasicInfoField setToolTip: @""];
268   
269            [fHaveField setStringValue: @""];
270            [fDownloadedTotalField setStringValue: @""];
271            [fUploadedTotalField setStringValue: @""];
272            [fFailedHashField setStringValue: @""];
273            [fDateActivityField setStringValue: @""];
274            [fRatioField setStringValue: @""];
275           
276            //options fields
277            [fUploadLimitCheck setEnabled: NO];
278            [fUploadLimitCheck setState: NSOffState];
279            [fUploadLimitField setEnabled: NO];
280            [fUploadLimitLabel setEnabled: NO];
281            [fUploadLimitField setStringValue: @""];
282           
283            [fDownloadLimitCheck setEnabled: NO];
284            [fDownloadLimitCheck setState: NSOffState];
285            [fDownloadLimitField setEnabled: NO];
286            [fDownloadLimitLabel setEnabled: NO];
287            [fDownloadLimitField setStringValue: @""];
288           
289            [fGlobalLimitCheck setEnabled: NO];
290            [fGlobalLimitCheck setState: NSOffState];
291           
292            [fRatioPopUp setEnabled: NO];
293            [fRatioPopUp selectItemAtIndex: -1];
294            [fRatioLimitField setHidden: YES];
295            [fRatioLimitField setStringValue: @""];
296           
297            [fPeersConnectField setEnabled: NO];
298            [fPeersConnectField setStringValue: @""];
299            [fPeersConnectLabel setEnabled: NO];
300        }
301       
302        [fFileController setTorrent: nil];
303       
304        [fNameField setToolTip: nil];
305
306        [fTrackerField setStringValue: @""];
307        [fPiecesField setStringValue: @""];
308        [fHashField setStringValue: @""];
309        [fHashField setToolTip: nil];
310        [fSecureField setStringValue: @""];
311        [fCommentView setString: @""];
312       
313        [fCreatorField setStringValue: @""];
314        [fDateCreatedField setStringValue: @""];
315        [fCommentView setSelectable: NO];
316       
317        [fTorrentLocationField setStringValue: @""];
318        [fTorrentLocationField setToolTip: nil];
319        [fDataLocationField setStringValue: @""];
320        [fDataLocationField setToolTip: nil];
321       
322        [fRevealDataButton setHidden: YES];
323        [fRevealTorrentButton setHidden: YES];
324       
325        //don't allow empty fields to be selected
326        [fTrackerField setSelectable: NO];
327        [fHashField setSelectable: NO];
328        [fCreatorField setSelectable: NO];
329        [fTorrentLocationField setSelectable: NO];
330        [fDataLocationField setSelectable: NO];
331       
332        [fStateField setStringValue: @""];
333        [fProgressField setStringValue: @""];
334       
335        [fSwarmSpeedField setStringValue: @""];
336        [fErrorMessageView setString: @""];
337        [fErrorMessageView setSelectable: NO];
338       
339        [fAnnounceAddressField setStringValue: @""];
340        [fAnnounceAddressField setToolTip: nil];
341        [fAnnounceAddressField setSelectable: NO];
342        [fAnnounceLastField setStringValue: @""];
343        [fAnnounceResponseField setStringValue: @""];
344        [fAnnounceResponseField setToolTip: nil];
345        [fAnnounceResponseField setSelectable: NO];
346        [fAnnounceNextField setStringValue: @""];
347       
348        [fScrapeAddressField setStringValue: @""];
349        [fScrapeAddressField setToolTip: nil];
350        [fScrapeAddressField setSelectable: NO];
351        [fScrapeLastField setStringValue: @""];
352        [fScrapeResponseField setStringValue: @""];
353        [fScrapeResponseField setToolTip: nil];
354        [fScrapeResponseField setSelectable: NO];
355        [fScrapeNextField setStringValue: @""];
356       
357        [fConnectedPeersField setStringValue: @""];
358        [fDownloadingFromField setStringValue: @""];
359        [fUploadingToField setStringValue: @""];
360        [fKnownField setStringValue: @""];
361        [fSeedersField setStringValue: @""];
362        [fLeechersField setStringValue: @""];
363        [fCompletedFromTrackerField setStringValue: @""];
364       
365        [fDateAddedField setStringValue: @""];
366        [fDateCompletedField setStringValue: @""];
367       
368        [fPiecesControl setSelected: NO forSegment: PIECES_CONTROL_AVAILABLE];
369        [fPiecesControl setSelected: NO forSegment: PIECES_CONTROL_PROGRESS];
370        [fPiecesControl setEnabled: NO];
371        [fPiecesView setTorrent: nil];
372       
373        [fPeers release];
374        fPeers = nil;
375        [fPeerTable reloadData];
376       
377        [fWebSeeds release];
378        fWebSeeds = nil;
379        [fWebSeedTable reloadData];
380        [self setWebSeedTableHidden: YES animate: YES];
381       
382        [fTrackers release];
383        fTrackers = nil;
384       
385        [fTrackerAddRemoveControl setEnabled: NO forSegment: TRACKER_ADD_TAG];
386        [fTrackerAddRemoveControl setEnabled: NO forSegment: TRACKER_REMOVE_TAG];
387       
388        [fFileFilterField setEnabled: NO];
389    }
390    else
391    {
392        Torrent * torrent = [fTorrents objectAtIndex: 0];
393       
394        [fFileController setTorrent: torrent];
395       
396        NSImage * icon = [[torrent icon] copy];
397        [icon setFlipped: NO];
398        [fImageView setImage: icon];
399        [icon release];
400       
401        NSString * name = [torrent name];
402        [fNameField setStringValue: name];
403        [fNameField setToolTip: name];
404       
405        NSString * basicString = [NSString stringForFileSize: [torrent size]];
406        if ([torrent isFolder])
407        {
408            NSString * fileString;
409            NSInteger fileCount = [torrent fileCount];
410            if (fileCount == 1)
411                fileString = NSLocalizedString(@"1 file", "Inspector -> selected torrents");
412            else
413                fileString= [NSString stringWithFormat: NSLocalizedString(@"%d files", "Inspector -> selected torrents"), fileCount];
414            basicString = [NSString stringWithFormat: @"%@, %@", fileString, basicString];
415        }
416        [fBasicInfoField setStringValue: basicString];
417        [fBasicInfoField setToolTip: [NSString stringWithFormat: NSLocalizedString(@"%llu bytes", "Inspector -> selected torrents"),
418                                        [torrent size]]];
419       
420        NSString * hashString = [torrent hashString];
421        [fPiecesField setStringValue: [NSString stringWithFormat: @"%d, %@", [torrent pieceCount],
422                                        [NSString stringForFileSize: [torrent pieceSize]]]];
423        [fHashField setStringValue: hashString];
424        [fHashField setToolTip: hashString];
425        [fSecureField setStringValue: [torrent privateTorrent]
426                        ? NSLocalizedString(@"Private Torrent, PEX automatically disabled", "Inspector -> private torrent")
427                        : NSLocalizedString(@"Public Torrent", "Inspector -> private torrent")];
428       
429        NSString * commentString = [torrent comment];
430        [fCommentView setString: commentString];
431       
432        NSString * creatorString = [torrent creator];
433        [fCreatorField setStringValue: creatorString];
434        [fDateCreatedField setObjectValue: [torrent dateCreated]];
435       
436        if ([torrent publicTorrent])
437        {
438            NSString * location = [torrent publicTorrentLocation];
439            [fTorrentLocationField setStringValue: [location stringByAbbreviatingWithTildeInPath]];
440            [fTorrentLocationField setToolTip: location];
441        }
442        else
443        {
444            [fTorrentLocationField setStringValue: @""];
445            [fTorrentLocationField setToolTip: nil];
446        }
447       
448        [fDateAddedField setObjectValue: [torrent dateAdded]];
449       
450        [fRevealDataButton setHidden: NO];
451        [fRevealTorrentButton setHidden: ![torrent publicTorrent]];
452       
453        //allow these fields to be selected
454        [fTrackerField setSelectable: YES];
455        [fHashField setSelectable: YES];
456        [fCommentView setSelectable: ![commentString isEqualToString: @""]];
457        [fCreatorField setSelectable: ![creatorString isEqualToString: @""]];
458        [fTorrentLocationField setSelectable: YES];
459        [fDataLocationField setSelectable: YES];
460        [fAnnounceAddressField setSelectable: YES];
461        [fScrapeAddressField setSelectable: YES];
462       
463        //set pieces view
464        BOOL piecesAvailableSegment = [[NSUserDefaults standardUserDefaults] boolForKey: @"PiecesViewShowAvailability"];
465        [fPiecesControl setSelected: piecesAvailableSegment forSegment: PIECES_CONTROL_AVAILABLE];
466        [fPiecesControl setSelected: !piecesAvailableSegment forSegment: PIECES_CONTROL_PROGRESS];
467        [fPiecesControl setEnabled: YES];
468        [fPiecesView setTorrent: torrent];
469       
470        //get webseeds for table - if no webseeds for this torrent, clear the table
471        BOOL hasWebSeeds = [torrent webSeedCount] > 0;
472        [self setWebSeedTableHidden: !hasWebSeeds animate: YES];
473        if (!hasWebSeeds)
474        {
475            [fWebSeeds release];
476            fWebSeeds = nil;
477            [fWebSeedTable reloadData];
478        }
479       
480        //get trackers for table
481        [fTrackers release];
482        fTrackers = [[torrent allTrackers: YES] retain];
483        [fTrackerTable deselectAll: self];
484       
485        [fTrackerAddRemoveControl setEnabled: YES forSegment: TRACKER_ADD_TAG];
486        [fTrackerAddRemoveControl setEnabled: NO forSegment: TRACKER_REMOVE_TAG];
487       
488        [fFileFilterField setEnabled: [torrent isFolder]];
489    }
490   
491    [fFileFilterField setStringValue: @""];
492   
493    //update stats and settings
494    [self updateInfoStats];
495    [self updateOptions];
496   
497    //reload tables that won't change every update
498    [fTrackerTable setTrackers: fTrackers];
499    [fTrackerTable reloadData];
500}
501
502- (void) updateInfoStats
503{
504    switch ([fTabMatrix selectedTag])
505    {
506        case TAB_INFO_TAG:
507            [self updateInfoGeneral];
508            break;
509        case TAB_ACTIVITY_TAG:
510            [self updateInfoActivity];
511            break;
512        case TAB_TRACKER_TAG:
513            [self updateInfoTracker];
514            break;
515        case TAB_PEERS_TAG:
516            [self updateInfoPeers];
517            break;
518        case TAB_FILES_TAG:
519            [self updateInfoFiles];
520            break;
521    }
522}
523
524- (void) updateOptions
525{
526    if ([fTorrents count] == 0)
527        return;
528   
529    //get bandwidth info
530    NSEnumerator * enumerator = [fTorrents objectEnumerator];
531    Torrent * torrent = [enumerator nextObject]; //first torrent
532   
533    NSInteger uploadUseSpeedLimit = [torrent usesSpeedLimit: YES] ? NSOnState : NSOffState,
534                uploadSpeedLimit = [torrent speedLimit: YES],
535                downloadUseSpeedLimit = [torrent usesSpeedLimit: NO] ? NSOnState : NSOffState,
536                downloadSpeedLimit = [torrent speedLimit: NO],
537                globalUseSpeedLimit = [torrent usesGlobalSpeedLimit] ? NSOnState : NSOffState;
538   
539    while ((torrent = [enumerator nextObject])
540            && (uploadUseSpeedLimit != NSMixedState || uploadSpeedLimit != INVALID
541                || downloadUseSpeedLimit != NSMixedState || downloadSpeedLimit != INVALID
542                || globalUseSpeedLimit != NSMixedState))
543    {
544        if (uploadUseSpeedLimit != INVALID && uploadUseSpeedLimit != ([torrent usesSpeedLimit: YES] ? NSOnState : NSOffState))
545            uploadUseSpeedLimit = NSMixedState;
546       
547        if (uploadSpeedLimit != INVALID && uploadSpeedLimit != [torrent speedLimit: YES])
548            uploadSpeedLimit = INVALID;
549       
550        if (downloadUseSpeedLimit != INVALID && downloadUseSpeedLimit != ([torrent usesSpeedLimit: NO] ? NSOnState : NSOffState))
551            downloadUseSpeedLimit = NSMixedState;
552       
553        if (downloadSpeedLimit != INVALID && downloadSpeedLimit != [torrent speedLimit: NO])
554            downloadSpeedLimit = INVALID;
555       
556        if (globalUseSpeedLimit != INVALID && globalUseSpeedLimit != ([torrent usesGlobalSpeedLimit] ? NSOnState : NSOffState))
557            globalUseSpeedLimit = NSMixedState;
558    }
559   
560    //set upload view
561    [fUploadLimitCheck setState: uploadUseSpeedLimit];
562    [fUploadLimitCheck setEnabled: YES];
563   
564    [fUploadLimitLabel setEnabled: uploadUseSpeedLimit == NSOnState];
565    [fUploadLimitField setEnabled: uploadUseSpeedLimit == NSOnState];
566    if (uploadSpeedLimit != INVALID)
567        [fUploadLimitField setIntValue: uploadSpeedLimit];
568    else
569        [fUploadLimitField setStringValue: @""];
570   
571    //set download view
572    [fDownloadLimitCheck setState: downloadUseSpeedLimit];
573    [fDownloadLimitCheck setEnabled: YES];
574   
575    [fDownloadLimitLabel setEnabled: downloadUseSpeedLimit == NSOnState];
576    [fDownloadLimitField setEnabled: downloadUseSpeedLimit == NSOnState];
577    if (downloadSpeedLimit != INVALID)
578        [fDownloadLimitField setIntValue: downloadSpeedLimit];
579    else
580        [fDownloadLimitField setStringValue: @""];
581   
582    //set global check
583    [fGlobalLimitCheck setState: globalUseSpeedLimit];
584    [fGlobalLimitCheck setEnabled: YES];
585   
586    //get ratio info
587    enumerator = [fTorrents objectEnumerator];
588    torrent = [enumerator nextObject]; //first torrent
589   
590    NSInteger checkRatio = [torrent ratioSetting];
591    CGFloat ratioLimit = [torrent ratioLimit];
592   
593    while ((torrent = [enumerator nextObject]) && (checkRatio != INVALID || checkRatio != INVALID))
594    {
595        if (checkRatio != INVALID && checkRatio != [torrent ratioSetting])
596            checkRatio = INVALID;
597       
598        if (ratioLimit != INVALID && ratioLimit != [torrent ratioLimit])
599            ratioLimit = INVALID;
600    }
601   
602    //set ratio view
603    NSInteger index;
604    if (checkRatio == TR_RATIOLIMIT_SINGLE)
605        index = OPTION_POPUP_LIMIT;
606    else if (checkRatio == TR_RATIOLIMIT_UNLIMITED)
607        index = OPTION_POPUP_NO_LIMIT;
608    else if (checkRatio == TR_RATIOLIMIT_GLOBAL)
609        index = OPTION_POPUP_GLOBAL;
610    else
611        index = -1;
612    [fRatioPopUp selectItemAtIndex: index];
613    [fRatioPopUp setEnabled: YES];
614   
615    [fRatioLimitField setHidden: checkRatio != TR_RATIOLIMIT_SINGLE];
616    if (ratioLimit != INVALID)
617        [fRatioLimitField setFloatValue: ratioLimit];
618    else
619        [fRatioLimitField setStringValue: @""];
620   
621    //get peer info
622    enumerator = [fTorrents objectEnumerator];
623    torrent = [enumerator nextObject]; //first torrent
624   
625    NSInteger maxPeers = [torrent maxPeerConnect];
626   
627    while ((torrent = [enumerator nextObject]))
628    {
629        if (maxPeers != [torrent maxPeerConnect])
630        {
631            maxPeers = INVALID;
632            break;
633        }
634    }
635   
636    //set peer view
637    [fPeersConnectField setEnabled: YES];
638    [fPeersConnectLabel setEnabled: YES];
639    if (maxPeers != INVALID)
640        [fPeersConnectField setIntValue: maxPeers];
641    else
642        [fPeersConnectField setStringValue: @""];
643}
644
645- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame
646{
647    NSRect windowRect = [window frame];
648    windowRect.size.width = [window minSize].width;
649    return windowRect;
650}
651
652- (void) animationDidEnd: (NSAnimation *) animation
653{
654    if (animation == fWebSeedTableAnimation)
655    {
656        [fWebSeedTableAnimation release];
657        fWebSeedTableAnimation = nil;
658    }
659}
660
661- (NSSize) windowWillResize: (NSWindow *) window toSize: (NSSize) proposedFrameSize
662{
663    //this is an edge-case - just stop the animation (stopAnimation jumps to end frame)
664    if (fWebSeedTableAnimation)
665    {
666        [fWebSeedTableAnimation stopAnimation];
667        [fWebSeedTableAnimation release];
668        fWebSeedTableAnimation = nil;
669    }
670   
671    return proposedFrameSize;
672}
673
674- (void) setTab: (id) sender
675{
676    NSInteger oldTabTag = fCurrentTabTag;
677    fCurrentTabTag = [fTabMatrix selectedTag];
678    if (fCurrentTabTag == oldTabTag)
679        return;
680   
681    [self updateInfoStats];
682   
683    //take care of old view
684    CGFloat oldHeight = 0;
685    NSString * oldResizeSaveKey = nil;
686    if (oldTabTag != INVALID)
687    {
688        //deselect old tab item
689        [(InfoTabButtonCell *)[fTabMatrix cellWithTag: oldTabTag] setSelectedTab: NO];
690       
691        switch (oldTabTag)
692        {
693            case TAB_ACTIVITY_TAG:
694                [fPiecesView clearView];
695                break;
696           
697            case TAB_TRACKER_TAG:
698                oldResizeSaveKey = @"InspectorContentHeightTracker";
699                break;
700           
701            case TAB_PEERS_TAG:
702                //if in the middle of animating, just stop and resize immediately
703                if (fWebSeedTableAnimation)
704                    [self setWebSeedTableHidden: !fWebSeeds animate: NO];
705               
706                [fPeers release];
707                fPeers = nil;
708                [fWebSeeds release];
709                fWebSeeds = nil;
710               
711                oldResizeSaveKey = @"InspectorContentHeightPeers";
712                break;
713           
714            case TAB_FILES_TAG:
715                [[QuickLookController quickLook] updateQuickLook];
716               
717                oldResizeSaveKey = @"InspectorContentHeightFiles";
718                break;
719        }
720       
721        NSView * oldView = [self tabViewForTag: oldTabTag];
722        oldHeight = [oldView frame].size.height;
723        if (oldResizeSaveKey)
724            [[NSUserDefaults standardUserDefaults] setFloat: oldHeight forKey: oldResizeSaveKey];
725       
726        //remove old view
727        [oldView setHidden: YES];
728        [oldView removeFromSuperview];
729    }
730   
731    //set new tab item
732    NSView * view = [self tabViewForTag: fCurrentTabTag];
733   
734    NSString * resizeSaveKey = nil;
735    NSString * identifier, * title;
736    switch (fCurrentTabTag)
737    {
738        case TAB_INFO_TAG:
739            identifier = TAB_INFO_IDENT;
740            title = NSLocalizedString(@"General Info", "Inspector -> title");
741            break;
742        case TAB_ACTIVITY_TAG:
743            identifier = TAB_ACTIVITY_IDENT;
744            title = NSLocalizedString(@"Activity", "Inspector -> title");
745            break;
746        case TAB_TRACKER_TAG:
747            identifier = TAB_TRACKER_IDENT;
748            title = NSLocalizedString(@"Tracker", "Inspector -> title");
749            resizeSaveKey = @"InspectorContentHeightTracker";
750            break;
751        case TAB_PEERS_TAG:
752            identifier = TAB_PEERS_IDENT;
753            title = NSLocalizedString(@"Peers", "Inspector -> title");
754            resizeSaveKey = @"InspectorContentHeightPeers";
755            break;
756        case TAB_FILES_TAG:
757            identifier = TAB_FILES_IDENT;
758            title = NSLocalizedString(@"Files", "Inspector -> title");
759            resizeSaveKey = @"InspectorContentHeightFiles";
760            break;
761        case TAB_OPTIONS_TAG:
762            identifier = TAB_OPTIONS_IDENT;
763            title = NSLocalizedString(@"Options", "Inspector -> title");
764            break;
765        default:
766            return;
767    }
768   
769    [[NSUserDefaults standardUserDefaults] setObject: identifier forKey: @"InspectorSelected"];
770   
771    NSWindow * window = [self window];
772   
773    [window setTitle: [NSString stringWithFormat: @"%@ - %@", title, NSLocalizedString(@"Torrent Inspector", "Inspector -> title")]];
774   
775    //selected tab item
776    [(InfoTabButtonCell *)[fTabMatrix selectedCell] setSelectedTab: YES];
777   
778    NSRect windowRect = [window frame], viewRect = [view frame];
779   
780    if (resizeSaveKey)
781    {
782        CGFloat height = [[NSUserDefaults standardUserDefaults] floatForKey: resizeSaveKey];
783        if (height != 0.0)
784            viewRect.size.height = MAX(height, TAB_MIN_HEIGHT);
785    }
786   
787    CGFloat difference = (viewRect.size.height - oldHeight) * [window userSpaceScaleFactor];
788    windowRect.origin.y -= difference;
789    windowRect.size.height += difference;
790   
791    if (resizeSaveKey)
792    {
793        if (!oldResizeSaveKey)
794        {
795            [window setMinSize: NSMakeSize([window minSize].width, windowRect.size.height - viewRect.size.height + TAB_MIN_HEIGHT)];
796            [window setMaxSize: NSMakeSize(FLT_MAX, FLT_MAX)];
797        }
798    }
799    else
800    {
801        [window setMinSize: NSMakeSize([window minSize].width, windowRect.size.height)];
802        [window setMaxSize: NSMakeSize(FLT_MAX, windowRect.size.height)];
803    }
804   
805    viewRect.size.width = windowRect.size.width;
806    [view setFrame: viewRect];
807   
808    [window setFrame: windowRect display: YES animate: oldTabTag != INVALID];
809    [[window contentView] addSubview: view];
810    [view setHidden: NO];
811   
812    if (fCurrentTabTag == TAB_FILES_TAG)
813        [[QuickLookController quickLook] updateQuickLook];
814}
815
816- (void) setNextTab
817{
818    NSInteger tag = [fTabMatrix selectedTag]+1;
819    if (tag >= [fTabMatrix numberOfColumns])
820        tag = 0;
821   
822    [fTabMatrix selectCellWithTag: tag];
823    [self setTab: nil];
824}
825
826- (void) setPreviousTab
827{
828    NSInteger tag = [fTabMatrix selectedTag]-1;
829    if (tag < 0)
830        tag = [fTabMatrix numberOfColumns]-1;
831   
832    [fTabMatrix selectCellWithTag: tag];
833    [self setTab: nil];
834}
835
836- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
837{
838    if (tableView == fPeerTable)
839        return fPeers ? [fPeers count] : 0;
840    else if (tableView == fWebSeedTable)
841        return fWebSeeds ? [fWebSeeds count] : 0;
842    else if (tableView == fTrackerTable)
843        return fTrackers ? [fTrackers count] : 0;
844    return 0;
845}
846
847- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) column row: (NSInteger) row
848{
849    if (tableView == fPeerTable)
850    {
851        NSString * ident = [column identifier];
852        NSDictionary * peer = [fPeers objectAtIndex: row];
853       
854        if ([ident isEqualToString: @"Encryption"])
855            return [[peer objectForKey: @"Encryption"] boolValue] ? [NSImage imageNamed: @"Lock.png"] : nil;
856        else if ([ident isEqualToString: @"Client"])
857            return [peer objectForKey: @"Client"];
858        else if  ([ident isEqualToString: @"Progress"])
859            return [peer objectForKey: @"Progress"];
860        else if ([ident isEqualToString: @"UL To"])
861        {
862            NSNumber * rate;
863            return (rate = [peer objectForKey: @"UL To Rate"]) ? [NSString stringForSpeedAbbrev: [rate floatValue]] : @"";
864        }
865        else if ([ident isEqualToString: @"DL From"])
866        {
867            NSNumber * rate;
868            return (rate = [peer objectForKey: @"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate floatValue]] : @"";
869        }
870        else
871            return [peer objectForKey: @"IP"];
872    }
873    else if (tableView == fWebSeedTable)
874    {
875        NSString * ident = [column identifier];
876        NSDictionary * webSeed = [fWebSeeds objectAtIndex: row];
877       
878        if ([ident isEqualToString: @"DL From"])
879        {
880            NSNumber * rate;
881            return (rate = [webSeed objectForKey: @"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate floatValue]] : @"";
882        }
883        else
884            return [webSeed objectForKey: @"Address"];
885    }
886    else if (tableView == fTrackerTable)
887    {
888        id item = [fTrackers objectAtIndex: row];
889        if ([item isKindOfClass: [NSNumber class]])
890        {
891            NSInteger tier = [item intValue];
892            if (tier == 0)
893                return NSLocalizedString(@"User-Added", "Inspector -> tracker table");
894            else
895                return [NSString stringWithFormat: NSLocalizedString(@"Tier %d", "Inspector -> tracker table"), tier];
896        }
897        else
898            return item;
899    }
900    return nil;
901}
902
903- (void) tableView: (NSTableView *) tableView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn
904    row: (NSInteger) row
905{
906    if (tableView == fPeerTable)
907    {
908        NSString * ident = [tableColumn identifier];
909       
910        if  ([ident isEqualToString: @"Progress"])
911        {
912            NSDictionary * peer = [fPeers objectAtIndex: row];
913            [(PeerProgressIndicatorCell *)cell setSeed: [[peer objectForKey: @"Seed"] boolValue]];
914        }
915    }
916}
917
918- (void) tableView: (NSTableView *) tableView didClickTableColumn: (NSTableColumn *) tableColumn
919{
920    if (tableView == fPeerTable)
921    {
922        if (fPeers)
923        {
924            NSArray * oldPeers = fPeers;
925            fPeers = [[fPeers sortedArrayUsingDescriptors: [self peerSortDescriptors]] retain];
926            [oldPeers release];
927            [tableView reloadData];
928        }
929    }
930    else if (tableView == fWebSeedTable)
931    {
932        if (fWebSeeds)
933        {
934            NSArray * oldWebSeeds = fWebSeeds;
935            fWebSeeds = [[fWebSeeds sortedArrayUsingDescriptors: [fWebSeedTable sortDescriptors]] retain];
936            [oldWebSeeds release];
937            [tableView reloadData];
938        }
939    }
940    else;
941}
942
943- (BOOL) tableView: (NSTableView *) tableView shouldSelectRow: (NSInteger) row
944{
945    return tableView == fTrackerTable;
946}
947
948- (void) tableViewSelectionDidChange: (NSNotification *) notification
949{
950    if ([notification object] == fTrackerTable)
951    {
952        NSInteger numSelected = [fTrackerTable numberOfSelectedRows];
953        [fTrackerAddRemoveControl setEnabled: numSelected > 0 forSegment: TRACKER_REMOVE_TAG];
954    }
955}
956
957- (BOOL) tableView: (NSTableView *) tableView isGroupRow: (NSInteger) row
958{
959    if (tableView == fTrackerTable)
960        return [[fTrackers objectAtIndex: row] isKindOfClass: [NSNumber class]];
961    return NO;
962}
963
964- (NSString *) tableView: (NSTableView *) tableView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect
965                tableColumn: (NSTableColumn *) column row: (NSInteger) row mouseLocation: (NSPoint) mouseLocation
966{
967    if (tableView == fPeerTable)
968    {
969        NSDictionary * peer = [fPeers objectAtIndex: row];
970        NSMutableArray * components = [NSMutableArray arrayWithCapacity: 5];
971       
972        CGFloat progress = [[peer objectForKey: @"Progress"] floatValue];
973        NSString * progressString = [NSString localizedStringWithFormat: NSLocalizedString(@"Progress: %.1f%%",
974                                        "Inspector -> Peers tab -> table row tooltip"), progress * 100.0];
975        if (progress < 1.0 && [[peer objectForKey: @"Seed"] boolValue])
976            progressString = [progressString stringByAppendingFormat: @" (%@)", NSLocalizedString(@"Partial Seed",
977                                "Inspector -> Peers tab -> table row tooltip")];
978        [components addObject: progressString];
979       
980        if ([[peer objectForKey: @"Encryption"] boolValue])
981            [components addObject: NSLocalizedString(@"Encrypted Connection", "Inspector -> Peers tab -> table row tooltip")];
982       
983        NSString * portString;
984        NSInteger port;
985        if ((port = [[peer objectForKey: @"Port"] intValue]) > 0)
986            portString = [NSString stringWithFormat: @"%d", port];
987        else
988            portString = NSLocalizedString(@"N/A", "Inspector -> Peers tab -> table row tooltip");
989        [components addObject: [NSString stringWithFormat: @"%@: %@", NSLocalizedString(@"Port",
990            "Inspector -> Peers tab -> table row tooltip"), portString]];
991       
992        switch ([[peer objectForKey: @"From"] intValue])
993        {
994            case TR_PEER_FROM_TRACKER:
995                [components addObject: NSLocalizedString(@"From: tracker", "Inspector -> Peers tab -> table row tooltip")];
996                break;
997            case TR_PEER_FROM_INCOMING:
998                [components addObject: NSLocalizedString(@"From: incoming connection", "Inspector -> Peers tab -> table row tooltip")];
999                break;
1000            case TR_PEER_FROM_CACHE:
1001                [components addObject: NSLocalizedString(@"From: cache", "Inspector -> Peers tab -> table row tooltip")];
1002                break;
1003            case TR_PEER_FROM_PEX:
1004                [components addObject: NSLocalizedString(@"From: peer exchange", "Inspector -> Peers tab -> table row tooltip")];
1005                break;
1006        }
1007       
1008        //determing status strings from flags
1009        NSMutableArray * statusArray = [NSMutableArray arrayWithCapacity: 6];
1010        NSString * flags = [peer objectForKey: @"Flags"];
1011       
1012        if ([flags rangeOfString: @"D"].location != NSNotFound)
1013            [statusArray addObject: NSLocalizedString(@"Currently downloading (interested and not choked)",
1014                "Inspector -> peer -> status")];
1015        if ([flags rangeOfString: @"d"].location != NSNotFound)
1016            [statusArray addObject: NSLocalizedString(@"You want to download, but peer does not want to send (interested and choked)",
1017                "Inspector -> peer -> status")];
1018        if ([flags rangeOfString: @"U"].location != NSNotFound)
1019            [statusArray addObject: NSLocalizedString(@"Currently uploading (interested and not choked)",
1020                "Inspector -> peer -> status")];
1021        if ([flags rangeOfString: @"u"].location != NSNotFound)
1022            [statusArray addObject: NSLocalizedString(@"Peer wants you to upload, but you do not want to (interested and choked)",
1023                "Inspector -> peer -> status")];
1024        if ([flags rangeOfString: @"K"].location != NSNotFound)
1025            [statusArray addObject: NSLocalizedString(@"Peer is unchoking you, but you are not interested",
1026                "Inspector -> peer -> status")];
1027        if ([flags rangeOfString: @"?"].location != NSNotFound)
1028            [statusArray addObject: NSLocalizedString(@"You unchoked the peer, but the peer is not interested",
1029                "Inspector -> peer -> status")];
1030       
1031        if ([statusArray count] > 0)
1032        {
1033            NSString * statusStrings = [statusArray componentsJoinedByString: @"\n\n"];
1034            [components addObject: [@"\n" stringByAppendingString: statusStrings]];
1035        }
1036       
1037        return [components componentsJoinedByString: @"\n"];
1038    }
1039    return nil;
1040}
1041
1042- (void) tableView: (NSTableView *) tableView setObjectValue: (id) object forTableColumn: (NSTableColumn *) tableColumn
1043    row: (NSInteger) row
1044{
1045    if (tableView != fTrackerTable)
1046        return;
1047   
1048    [fTrackers replaceObjectAtIndex: row withObject: object];
1049   
1050    Torrent * torrent= [fTorrents objectAtIndex: 0];
1051    if (![torrent updateAllTrackersForAdd: fTrackers])
1052        NSBeep();
1053   
1054    //reset table with either new or old value
1055    [fTrackers release];
1056    fTrackers = [[torrent allTrackers: YES] retain];
1057    [fTrackerTable deselectAll: self];
1058   
1059    [fTrackerTable setTrackers: fTrackers];
1060    [fTrackerTable reloadData];
1061}
1062
1063- (void) addRemoveTracker: (id) sender
1064{
1065    //don't allow add/remove when currently adding - it leads to weird results
1066    if ([fTrackerTable editedRow] != -1)
1067        return;
1068   
1069    if ([[sender cell] tagForSegment: [sender selectedSegment]] == TRACKER_REMOVE_TAG)
1070        [self removeTrackers];
1071    else
1072        [self addTrackers];
1073}
1074
1075- (BOOL) tableView: (NSTableView *) tableView shouldEditTableColumn: (NSTableColumn *) tableColumn row: (NSInteger) row
1076{
1077    if (tableView != fTrackerTable)
1078        return NO;
1079   
1080    //only allow modification of custom-added trackers
1081    if ([[fTrackers objectAtIndex: row] isKindOfClass: [NSNumber class]] || ![[fTorrents objectAtIndex: 0] hasAddedTrackers])
1082        return NO;
1083   
1084    NSUInteger i;
1085    for (i = row-1; ![[fTrackers objectAtIndex: i] isKindOfClass: [NSNumber class]]; i--);
1086   
1087    return [[fTrackers objectAtIndex: i] intValue] == 0;
1088}
1089
1090- (BOOL) shouldQuickLookFileView
1091{
1092    return [[self window] isVisible] && fCurrentTabTag == TAB_FILES_TAG && [[fFileController outlineView] numberOfSelectedRows] > 0;
1093}
1094
1095- (NSArray *) quickLookURLs
1096{
1097    FileOutlineView * fileOutlineView = [fFileController outlineView];
1098    Torrent * torrent = [fTorrents objectAtIndex: 0];
1099    NSString * folder = [torrent downloadFolder];
1100    NSIndexSet * indexes = [fileOutlineView selectedRowIndexes];
1101    NSMutableArray * urlArray = [NSMutableArray arrayWithCapacity: [indexes count]];
1102   
1103    for (NSUInteger i = [indexes firstIndex]; i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
1104    {
1105        FileListNode * item = [fileOutlineView itemAtRow: i];
1106        if ([self canQuickLookFile: item])
1107            [urlArray addObject: [NSURL fileURLWithPath: [folder stringByAppendingPathComponent: [item fullPath]]]];
1108    }
1109   
1110    return urlArray;
1111}
1112
1113- (BOOL) canQuickLook
1114{
1115    FileOutlineView * fileOutlineView = [fFileController outlineView];
1116    NSIndexSet * indexes = [fileOutlineView selectedRowIndexes];
1117   
1118    for (NSUInteger i = [indexes firstIndex]; i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
1119        if ([self canQuickLookFile: [fileOutlineView itemAtRow: i]])
1120            return YES;
1121   
1122    return NO;
1123}
1124
1125- (NSRect) quickLookFrameWithURL: (NSURL *) url
1126{
1127    FileOutlineView * fileOutlineView = [fFileController outlineView];
1128   
1129    NSString * fullPath = [url path];
1130    NSString * folder = [[fTorrents objectAtIndex: 0] downloadFolder];
1131    NSRange visibleRows = [fileOutlineView rowsInRect: [fileOutlineView bounds]];
1132   
1133    for (NSUInteger row = visibleRows.location; row < NSMaxRange(visibleRows); row++)
1134    {
1135        FileListNode * rowItem = [fileOutlineView itemAtRow: row];
1136        if ([[folder stringByAppendingPathComponent: [rowItem fullPath]] isEqualToString: fullPath])
1137        {
1138            NSRect frame = [fileOutlineView iconRectForRow: row];
1139            frame.origin = [fileOutlineView convertPoint: frame.origin toView: nil];
1140            frame.origin = [[self window] convertBaseToScreen: frame.origin];
1141            frame.origin.y -= frame.size.height;
1142            return frame;
1143        }
1144    }
1145   
1146    return NSZeroRect;
1147}
1148
1149- (void) setPiecesView: (id) sender
1150{
1151    [self setPiecesViewForAvailable: [sender selectedSegment] == PIECES_CONTROL_AVAILABLE];
1152}
1153
1154- (void) setPiecesViewForAvailable: (BOOL) available
1155{
1156    [fPiecesControl setSelected: available forSegment: PIECES_CONTROL_AVAILABLE];
1157    [fPiecesControl setSelected: !available forSegment: PIECES_CONTROL_PROGRESS];
1158   
1159    [[NSUserDefaults standardUserDefaults] setBool: available forKey: @"PiecesViewShowAvailability"];
1160    [fPiecesView updateView];
1161}
1162
1163- (void) revealTorrentFile: (id) sender
1164{
1165    if ([fTorrents count] > 0)
1166        [[fTorrents objectAtIndex: 0] revealPublicTorrent];
1167}
1168
1169- (void) revealDataFile: (id) sender
1170{
1171    if ([fTorrents count] > 0)
1172        [[fTorrents objectAtIndex: 0] revealData];
1173}
1174
1175- (void) setFileFilterText: (id) sender
1176{
1177    [fFileController setFilterText: [sender stringValue]];
1178}
1179
1180- (void) setUseSpeedLimit: (id) sender
1181{
1182    const BOOL upload = sender == fUploadLimitCheck;
1183    const BOOL limit = [sender state] == NSOnState;
1184   
1185    for (Torrent * torrent in fTorrents)
1186        [torrent setUseSpeedLimit: limit upload: upload];
1187   
1188    NSTextField * field = upload ? fUploadLimitField : fDownloadLimitField;
1189    [field setEnabled: limit];
1190    if (limit)
1191    {
1192        [field selectText: self];
1193        [[self window] makeKeyAndOrderFront: self];
1194    }
1195   
1196    NSTextField * label = upload ? fUploadLimitLabel : fDownloadLimitLabel;
1197    [label setEnabled: limit];
1198}
1199
1200- (void) setSpeedLimit: (id) sender
1201{
1202    BOOL upload = sender == fUploadLimitField;
1203    NSInteger limit = [sender intValue];
1204   
1205    for (Torrent * torrent in fTorrents)
1206        [torrent setSpeedLimit: limit upload: upload];
1207}
1208
1209- (void) setRatioSetting: (id) sender
1210{
1211    NSInteger setting;
1212    bool single = NO;
1213    switch ([sender indexOfSelectedItem])
1214    {
1215        case OPTION_POPUP_LIMIT:
1216            setting = TR_RATIOLIMIT_SINGLE;
1217            single = YES;
1218            break;
1219        case OPTION_POPUP_NO_LIMIT:
1220            setting = TR_RATIOLIMIT_UNLIMITED;
1221            break;
1222        case OPTION_POPUP_GLOBAL:
1223            setting = TR_RATIOLIMIT_GLOBAL;
1224            break;
1225        default:
1226            return;
1227    }
1228   
1229    for (Torrent * torrent in fTorrents)
1230        [torrent setRatioSetting: setting];
1231   
1232    [fRatioLimitField setHidden: !single];
1233    if (single)
1234    {
1235        [fRatioLimitField selectText: self];
1236        [[self window] makeKeyAndOrderFront: self];
1237    }
1238}
1239
1240- (void) setUseGlobalSpeedLimit: (id) sender
1241{
1242    const BOOL limit = [sender state] == NSOnState;
1243   
1244    for (Torrent * torrent in fTorrents)
1245        [torrent setUseGlobalSpeedLimit: limit];
1246}
1247
1248- (void) setRatioLimit: (id) sender
1249{
1250    CGFloat limit = [sender floatValue];
1251   
1252    for (Torrent * torrent in fTorrents)
1253        [torrent setRatioLimit: limit];
1254}
1255
1256- (void) setPeersConnectLimit: (id) sender
1257{
1258    NSInteger limit = [sender intValue];
1259   
1260    for (Torrent * torrent in fTorrents)
1261        [torrent setMaxPeerConnect: limit];
1262}
1263
1264
1265- (BOOL) control: (NSControl *) control textShouldBeginEditing: (NSText *) fieldEditor
1266{
1267    [fInitialString release];
1268    fInitialString = [[control stringValue] retain];
1269   
1270    return YES;
1271}
1272
1273- (BOOL) control: (NSControl *) control didFailToFormatString: (NSString *) string errorDescription: (NSString *) error
1274{
1275    NSBeep();
1276    if (fInitialString)
1277    {
1278        [control setStringValue: fInitialString];
1279        [fInitialString release];
1280        fInitialString = nil;
1281    }
1282    return NO;
1283}
1284
1285@end
1286
1287@implementation InfoWindowController (Private)
1288
1289- (void) updateInfoGeneral
1290{   
1291    if ([fTorrents count] != 1)
1292        return;
1293   
1294    Torrent * torrent = [fTorrents objectAtIndex: 0];
1295   
1296    [fTrackerField setStringValue: [torrent trackerAddressAnnounce]];
1297   
1298    NSString * location = [torrent dataLocation];
1299    [fDataLocationField setStringValue: [location stringByAbbreviatingWithTildeInPath]];
1300    [fDataLocationField setToolTip: location];
1301}
1302
1303- (void) updateInfoActivity
1304{
1305    NSInteger numberSelected = [fTorrents count];
1306    if (numberSelected == 0)
1307        return;
1308   
1309    uint64_t have = 0, haveVerified = 0, downloadedTotal = 0, uploadedTotal = 0, failedHash = 0;
1310    NSDate * lastActivity = nil;
1311    for (Torrent * torrent in fTorrents)
1312    {
1313        have += [torrent haveTotal];
1314        haveVerified += [torrent haveVerified];
1315        downloadedTotal += [torrent downloadedTotal];
1316        uploadedTotal += [torrent uploadedTotal];
1317        failedHash += [torrent failedHash];
1318       
1319        NSDate * nextLastActivity;
1320        if ((nextLastActivity = [torrent dateActivity]))
1321            lastActivity = lastActivity ? [lastActivity laterDate: nextLastActivity] : nextLastActivity;
1322    }
1323   
1324    if (have == 0)
1325        [fHaveField setStringValue: [NSString stringForFileSize: 0]];
1326    else
1327    {
1328        NSString * verifiedString = [NSString stringWithFormat: NSLocalizedString(@"%@ verified", "Inspector -> Activity tab -> have"),
1329                                        [NSString stringForFileSize: haveVerified]];
1330        if (have == haveVerified)
1331            [fHaveField setStringValue: verifiedString];
1332        else
1333            [fHaveField setStringValue: [NSString stringWithFormat: @"%@ (%@)", [NSString stringForFileSize: have], verifiedString]];
1334    }
1335   
1336    [fDownloadedTotalField setStringValue: [NSString stringForFileSize: downloadedTotal]];
1337    [fUploadedTotalField setStringValue: [NSString stringForFileSize: uploadedTotal]];
1338    [fFailedHashField setStringValue: [NSString stringForFileSize: failedHash]];
1339   
1340    [fDateActivityField setObjectValue: lastActivity];
1341   
1342    if (numberSelected == 1)
1343    {
1344        Torrent * torrent = [fTorrents objectAtIndex: 0];
1345       
1346        [fStateField setStringValue: [torrent stateString]];
1347       
1348        if ([torrent isFolder])
1349            [fProgressField setStringValue: [NSString localizedStringWithFormat: NSLocalizedString(@"%.2f%% (%.2f%% selected)",
1350                "Inspector -> Activity tab -> progress"), 100.0 * [torrent progress], 100.0 * [torrent progressDone]]];
1351        else
1352            [fProgressField setStringValue: [NSString localizedStringWithFormat: @"%.2f%%", 100.0 * [torrent progress]]];
1353           
1354        [fRatioField setStringValue: [NSString stringForRatio: [torrent ratio]]];
1355        [fSwarmSpeedField setStringValue: [torrent isActive] ? [NSString stringForSpeed: [torrent swarmSpeed]] : @""];
1356       
1357        NSString * errorMessage = [torrent errorMessage];
1358        if (![errorMessage isEqualToString: [fErrorMessageView string]])
1359        {
1360            [fErrorMessageView setString: errorMessage];
1361            [fErrorMessageView setSelectable: ![errorMessage isEqualToString: @""]];
1362        }
1363       
1364        [fDateCompletedField setObjectValue: [torrent dateCompleted]];
1365       
1366        [fPiecesView updateView];
1367    }
1368    else if (numberSelected > 1)
1369    {
1370        [fRatioField setStringValue: [NSString stringForRatio: tr_getRatio(uploadedTotal, downloadedTotal)]];
1371    }
1372    else;
1373}
1374
1375#warning reload table when necessary?
1376- (void) updateInfoTracker
1377{
1378    if ([fTorrents count] != 1)
1379        return;
1380    Torrent * torrent = [fTorrents objectAtIndex: 0];
1381   
1382    //announce fields
1383    NSString * announceAddress = [torrent trackerAddressAnnounce];
1384    [fAnnounceAddressField setStringValue: announceAddress];
1385    [fAnnounceAddressField setToolTip: announceAddress];
1386   
1387    [fAnnounceLastField setObjectValue: [torrent lastAnnounceTime]];
1388   
1389    NSString * announceResponse = [torrent announceResponse];
1390    [fAnnounceResponseField setStringValue: announceResponse];
1391    [fAnnounceResponseField setToolTip: announceResponse];
1392    [fAnnounceResponseField setSelectable: ![announceResponse isEqualToString: @""]];
1393   
1394    NSInteger announceNext = [torrent nextAnnounceTime];
1395    NSString * announceNextString;
1396    switch (announceNext)
1397    {
1398        case STAT_TIME_NOW:
1399            announceNextString = [NSLocalizedString(@"In progress", "Inspector -> tracker tab") stringByAppendingEllipsis];
1400            break;
1401        case STAT_TIME_NONE:
1402            announceNextString = @"";
1403            break;
1404        default:
1405            announceNextString = [NSString timeString: announceNext showSeconds: YES];
1406    }
1407    [fAnnounceNextField setStringValue: announceNextString];
1408   
1409    //scrape fields
1410    NSString * scrapeAddress;
1411    if ((scrapeAddress = [torrent trackerAddressScrape]))
1412    {
1413        [fScrapeAddressField setStringValue: scrapeAddress];
1414        [fScrapeAddressField setToolTip: scrapeAddress];
1415    }
1416    else
1417    {
1418        [fScrapeAddressField setStringValue: @""];
1419        [fScrapeAddressField setToolTip: @""];
1420    }
1421   
1422    [fScrapeLastField setObjectValue: [torrent lastScrapeTime]];
1423   
1424    NSString * scrapeResponse = [torrent scrapeResponse];
1425    [fScrapeResponseField setStringValue: scrapeResponse];
1426    [fScrapeResponseField setToolTip: scrapeResponse];
1427    [fScrapeResponseField setSelectable: ![scrapeResponse isEqualToString: @""]];
1428   
1429    NSInteger scrapeNext = [torrent nextScrapeTime];
1430    NSString * scrapeNextString;
1431    switch (scrapeNext)
1432    {
1433        case STAT_TIME_NOW:
1434            scrapeNextString = [NSLocalizedString(@"In progress", "Inspector -> tracker tab") stringByAppendingEllipsis];
1435            break;
1436        case STAT_TIME_NONE:
1437            scrapeNextString = @"";
1438            break;
1439        default:
1440            scrapeNextString = [NSString timeString: scrapeNext showSeconds: YES];
1441    }
1442    [fScrapeNextField setStringValue: scrapeNextString];
1443}
1444
1445- (void) updateInfoPeers
1446{
1447    if ([fTorrents count] != 1)
1448        return;
1449    Torrent * torrent = [fTorrents objectAtIndex: 0];
1450   
1451    NSInteger seeders = [torrent seeders], leechers = [torrent leechers], completed = [torrent completedFromTracker];
1452    [fSeedersField setStringValue: seeders >= 0 ? [NSString stringWithFormat: @"%d", seeders] : @""];
1453    [fLeechersField setStringValue: leechers >= 0 ? [NSString stringWithFormat: @"%d", leechers] : @""];
1454    [fCompletedFromTrackerField setStringValue: completed >= 0 ? [NSString stringWithFormat: @"%d", completed] : @""];
1455   
1456    BOOL active = [torrent isActive];
1457   
1458    if (active)
1459    {
1460        NSInteger total = [torrent totalPeersConnected];
1461        NSString * connected = [NSString stringWithFormat:
1462                                NSLocalizedString(@"%d Connected", "Inspector -> Peers tab -> peers"), total];
1463       
1464        if (total > 0)
1465        {
1466            NSMutableArray * components = [NSMutableArray arrayWithCapacity: 4];
1467            NSInteger count;
1468            if ((count = [torrent totalPeersTracker]) > 0)
1469                [components addObject: [NSString stringWithFormat:
1470                                        NSLocalizedString(@"%d tracker", "Inspector -> Peers tab -> peers"), count]];
1471            if ((count = [torrent totalPeersIncoming]) > 0)
1472                [components addObject: [NSString stringWithFormat:
1473                                        NSLocalizedString(@"%d incoming", "Inspector -> Peers tab -> peers"), count]];
1474            if ((count = [torrent totalPeersPex]) > 0)
1475                [components addObject: [NSString stringWithFormat:
1476                                        NSLocalizedString(@"%d PEX", "Inspector -> Peers tab -> peers"), count]];
1477            if ((count = [torrent totalPeersCache]) > 0)
1478                [components addObject: [NSString stringWithFormat:
1479                                        NSLocalizedString(@"%d cache", "Inspector -> Peers tab -> peers"), count]];
1480           
1481            connected = [connected stringByAppendingFormat: @": %@", [components componentsJoinedByString: @", "]];
1482        }
1483       
1484        [fConnectedPeersField setStringValue: connected];
1485       
1486        [fDownloadingFromField setIntValue: [torrent peersSendingToUs]];
1487        [fUploadingToField setIntValue: [torrent peersGettingFromUs]];
1488    }
1489    else
1490    {
1491        [fConnectedPeersField setStringValue: @""];
1492        [fDownloadingFromField setStringValue: @""];
1493        [fUploadingToField setStringValue: @""];
1494    }
1495   
1496    [fKnownField setIntValue: [torrent totalPeersKnown]];
1497   
1498    [fPeers release];
1499    fPeers = [[[torrent peers] sortedArrayUsingDescriptors: [self peerSortDescriptors]] retain];
1500    [fPeerTable reloadData];
1501   
1502    if ([torrent webSeedCount] > 0)
1503    {
1504        [fWebSeeds release];
1505        fWebSeeds = [[[torrent webSeeds] sortedArrayUsingDescriptors: [fWebSeedTable sortDescriptors]] retain];
1506        [fWebSeedTable reloadData];
1507    }
1508}
1509
1510- (void) updateInfoFiles
1511{
1512    if ([fTorrents count] == 1)
1513        [fFileController reloadData];
1514}
1515
1516- (NSView *) tabViewForTag: (NSInteger) tag
1517{
1518    switch (tag)
1519    {
1520        case TAB_INFO_TAG:
1521            return fInfoView;
1522        case TAB_ACTIVITY_TAG:
1523            return fActivityView;
1524        case TAB_TRACKER_TAG:
1525            return fTrackerView;
1526        case TAB_PEERS_TAG:
1527            return fPeersView;
1528        case TAB_FILES_TAG:
1529            return fFilesView;
1530        case TAB_OPTIONS_TAG:
1531            return fOptionsView;
1532        default:
1533            return nil;
1534    }
1535}
1536
1537- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate
1538{
1539    if (fCurrentTabTag != TAB_PEERS_TAG || ![[self window] isVisible])
1540        animate = NO;
1541   
1542    if (fWebSeedTableAnimation)
1543    {
1544        [fWebSeedTableAnimation stopAnimation];
1545        [fWebSeedTableAnimation release];
1546        fWebSeedTableAnimation = nil;
1547    }
1548   
1549    NSRect webSeedFrame = [[fWebSeedTable enclosingScrollView] frame];
1550    NSRect peerFrame = [[fPeerTable enclosingScrollView] frame];
1551   
1552    if (hide)
1553    {
1554        CGFloat webSeedFrameMaxY = NSMaxY(webSeedFrame);
1555        webSeedFrame.size.height = 0;
1556        webSeedFrame.origin.y = webSeedFrameMaxY;
1557       
1558        peerFrame.size.height = webSeedFrameMaxY - peerFrame.origin.y;
1559    }
1560    else
1561    {
1562        webSeedFrame.origin.y -= fWebSeedTableHeight - webSeedFrame.size.height;
1563        webSeedFrame.size.height = fWebSeedTableHeight;
1564       
1565        peerFrame.size.height = (webSeedFrame.origin.y - fSpaceBetweenWebSeedAndPeer) - peerFrame.origin.y;
1566    }
1567   
1568    [[fWebSeedTable enclosingScrollView] setHidden: NO]; //this is needed for some reason
1569   
1570    //actually resize tables
1571    if (animate)
1572    {
1573        NSDictionary * webSeedDict = [NSDictionary dictionaryWithObjectsAndKeys:
1574                                    [fWebSeedTable enclosingScrollView], NSViewAnimationTargetKey,
1575                                    [NSValue valueWithRect: [[fWebSeedTable enclosingScrollView] frame]], NSViewAnimationStartFrameKey,
1576                                    [NSValue valueWithRect: webSeedFrame], NSViewAnimationEndFrameKey, nil],
1577                    * peerDict = [NSDictionary dictionaryWithObjectsAndKeys:
1578                                    [fPeerTable enclosingScrollView], NSViewAnimationTargetKey,
1579                                    [NSValue valueWithRect: [[fPeerTable enclosingScrollView] frame]], NSViewAnimationStartFrameKey,
1580                                    [NSValue valueWithRect: peerFrame], NSViewAnimationEndFrameKey, nil];
1581       
1582        fWebSeedTableAnimation = [[NSViewAnimation alloc] initWithViewAnimations:
1583                                        [NSArray arrayWithObjects: webSeedDict, peerDict, nil]];
1584        [fWebSeedTableAnimation setDuration: 0.125];
1585        [fWebSeedTableAnimation setAnimationBlockingMode: NSAnimationNonblocking];
1586        [fWebSeedTableAnimation setDelegate: self];
1587       
1588        [fWebSeedTableAnimation startAnimation];
1589    }
1590    else
1591    {
1592        [[fWebSeedTable enclosingScrollView] setFrame: webSeedFrame];
1593        [[fPeerTable enclosingScrollView] setFrame: peerFrame];
1594    }
1595}
1596
1597- (NSArray *) peerSortDescriptors
1598{
1599    NSMutableArray * descriptors = [NSMutableArray arrayWithCapacity: 2];
1600   
1601    NSArray * oldDescriptors = [fPeerTable sortDescriptors];
1602    BOOL useSecond = YES, asc = YES;
1603    if ([oldDescriptors count] > 0)
1604    {
1605        NSSortDescriptor * descriptor = [oldDescriptors objectAtIndex: 0];
1606        [descriptors addObject: descriptor];
1607       
1608        if ((useSecond = ![[descriptor key] isEqualToString: @"IP"]))
1609            asc = [descriptor ascending];
1610    }
1611   
1612    //sort by IP after primary sort
1613    if (useSecond)
1614    {
1615        NSSortDescriptor * secondDescriptor = [[NSSortDescriptor alloc] initWithKey: @"IP" ascending: asc
1616                                                                        selector: @selector(compareNumeric:)];
1617        [descriptors addObject: secondDescriptor];
1618        [secondDescriptor release];
1619    }
1620   
1621    return descriptors;
1622}
1623
1624- (BOOL) canQuickLookFile: (FileListNode *) item
1625{
1626    Torrent * torrent = [fTorrents objectAtIndex: 0];
1627   
1628    if (![[NSFileManager defaultManager] fileExistsAtPath: [[torrent downloadFolder] stringByAppendingPathComponent: [item fullPath]]])
1629        return NO;
1630   
1631    return [item isFolder] || [torrent fileProgress: item] == 1.0;
1632}
1633
1634- (void) addTrackers
1635{
1636    [[self window] makeKeyWindow];
1637   
1638    NSUInteger index = 1;
1639    if ([[fTorrents objectAtIndex: 0] hasAddedTrackers])
1640    {
1641        for (; index < [fTrackers count]; index++)
1642            if ([[fTrackers objectAtIndex: index] isKindOfClass: [NSNumber class]])
1643                break;
1644    }
1645    else
1646        [fTrackers insertObject: [NSNumber numberWithInt: 0] atIndex: 0];
1647   
1648    [fTrackers insertObject: @"" atIndex: index];
1649    [fTrackerTable reloadData];
1650    [fTrackerTable selectRow: index byExtendingSelection: NO];
1651    [fTrackerTable editColumn: 0 row: index withEvent: nil select: YES];
1652}
1653
1654- (void) removeTrackers
1655{
1656    NSMutableIndexSet * indexes = [[[fTrackerTable selectedRowIndexes] mutableCopy] autorelease];
1657   
1658    //get all rows to remove and determine if any built-in trackers are being remove
1659    NSUInteger i = 0, numberBuiltIn = 0;
1660    while (i < [fTrackers count])
1661    {
1662        BOOL builtIn = i != 0 || [[fTrackers objectAtIndex: i] intValue] != 0;
1663       
1664        //if a group is selected, remove all trackers in the group
1665        if ([indexes containsIndex: i])
1666        {
1667            for (i = i+1; i < [fTrackers count] && ![[fTrackers objectAtIndex: i] isKindOfClass: [NSNumber class]]; i++)
1668            {
1669                [indexes addIndex: i];
1670                if (builtIn)
1671                    numberBuiltIn++;
1672            }
1673        }
1674        //remove empty groups
1675        else
1676        {
1677            BOOL allSelected = YES;
1678            NSUInteger j;
1679            for (j = i+1; j < [fTrackers count] && ![[fTrackers objectAtIndex: j] isKindOfClass: [NSNumber class]]; j++)
1680            {
1681                if (![indexes containsIndex: j])
1682                    allSelected = NO;
1683                else if (builtIn)
1684                    numberBuiltIn++;
1685                else;
1686            }
1687           
1688            if (allSelected)
1689                [indexes addIndex: i];
1690           
1691            i = j;
1692        }
1693    }
1694   
1695    if ([fTrackers count] == [indexes count])
1696    {
1697        NSBeep();
1698        return;
1699    }
1700   
1701    Torrent * torrent = [fTorrents objectAtIndex: 0];
1702   
1703    //determine if removing trackers built into the torrent
1704    if (numberBuiltIn > 0 && [[NSUserDefaults standardUserDefaults] boolForKey: @"WarningRemoveBuiltInTracker"])
1705    {
1706        NSAlert * alert = [[NSAlert alloc] init];
1707       
1708        if (numberBuiltIn > 1)
1709        {
1710            [alert setMessageText: [NSString stringWithFormat:
1711                                    NSLocalizedString(@"Are you sure you want to remove %d built-in trackers?",
1712                                    "Remove built-in tracker alert -> title"), numberBuiltIn]];
1713            [alert setInformativeText: NSLocalizedString(@"These tracker addresses are part of the torrent file."
1714                " Once removed, Transmission will no longer attempt to contact them.", "Remove built-in tracker alert -> message")];
1715        }
1716        else
1717        {
1718            [alert setMessageText: NSLocalizedString(@"Are you sure you want to remove a built-in tracker?",
1719                                    "Remove built-in tracker alert -> title")];
1720            [alert setInformativeText: NSLocalizedString(@"The tracker address is part of the torrent file."
1721                " Once removed, Transmission will no longer attempt to contact it.", "Remove built-in tracker alert -> message")];
1722        }
1723       
1724        [alert addButtonWithTitle: NSLocalizedString(@"Remove", "Remove built-in tracker alert -> button")];
1725        [alert addButtonWithTitle: NSLocalizedString(@"Cancel", "Remove built-in tracker alert -> button")];
1726       
1727        [alert setShowsSuppressionButton: YES];
1728
1729        NSInteger result = [alert runModal];
1730        if ([[alert suppressionButton] state] == NSOnState)
1731            [[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"WarningRemoveBuiltInTracker"];
1732        [alert release];
1733       
1734        if (result != NSAlertFirstButtonReturn)
1735            return;
1736    }
1737   
1738    [fTrackers removeObjectsAtIndexes: indexes];
1739   
1740    [torrent updateAllTrackersForRemove: fTrackers];
1741    [fTrackerTable deselectAll: self];
1742   
1743    //reset table with either new or old value
1744    [fTrackers release];
1745    fTrackers = [[torrent allTrackers: YES] retain];
1746   
1747    [fTrackerTable setTrackers: fTrackers];
1748    [fTrackerTable reloadData];
1749}
1750
1751@end
Note: See TracBrowser for help on using the repository browser.