transmission/macosx/InfoPeersViewController.m

545 lines
22 KiB
Objective-C

/******************************************************************************
* $Id$
*
* Copyright (c) 2010-2012 Transmission authors and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*****************************************************************************/
#import "InfoPeersViewController.h"
#import "NSApplicationAdditions.h"
#import "NSStringAdditions.h"
#import "PeerProgressIndicatorCell.h"
#import "Torrent.h"
#import "WebSeedTableView.h"
#import "transmission.h" // required by utils.h
#import "utils.h"
#define ANIMATION_ID_KEY @"animationId"
#define WEB_SEED_ANIMATION_ID @"webSeed"
@interface InfoPeersViewController (Private)
- (void) setupInfo;
- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate;
- (NSArray *) peerSortDescriptors;
@end
@implementation InfoPeersViewController
- (id) init
{
if ((self = [super initWithNibName: @"InfoPeersView" bundle: nil]))
{
[self setTitle: NSLocalizedString(@"Peers", "Inspector view -> title")];
}
return self;
}
- (void) awakeFromNib
{
const CGFloat height = [[NSUserDefaults standardUserDefaults] floatForKey: @"InspectorContentHeightPeers"];
if (height != 0.0)
{
NSRect viewRect = [[self view] frame];
viewRect.size.height = height;
[[self view] setFrame: viewRect];
}
//set table header text
[[[fPeerTable tableColumnWithIdentifier: @"IP"] headerCell] setStringValue: NSLocalizedString(@"IP Address",
"inspector -> peer table -> header")];
[[[fPeerTable tableColumnWithIdentifier: @"Client"] headerCell] setStringValue: NSLocalizedString(@"Client",
"inspector -> peer table -> header")];
[[[fPeerTable tableColumnWithIdentifier: @"DL From"] headerCell] setStringValue: NSLocalizedString(@"DL",
"inspector -> peer table -> header")];
[[[fPeerTable tableColumnWithIdentifier: @"UL To"] headerCell] setStringValue: NSLocalizedString(@"UL",
"inspector -> peer table -> header")];
[[[fWebSeedTable tableColumnWithIdentifier: @"Address"] headerCell] setStringValue: NSLocalizedString(@"Web Seeds",
"inspector -> web seed table -> header")];
[[[fWebSeedTable tableColumnWithIdentifier: @"DL From"] headerCell] setStringValue: NSLocalizedString(@"DL",
"inspector -> web seed table -> header")];
//set table header tool tips
[[fPeerTable tableColumnWithIdentifier: @"Encryption"] setHeaderToolTip: NSLocalizedString(@"Encrypted Connection",
"inspector -> peer table -> header tool tip")];
[[fPeerTable tableColumnWithIdentifier: @"Progress"] setHeaderToolTip: NSLocalizedString(@"Available",
"inspector -> peer table -> header tool tip")];
[[fPeerTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Peer",
"inspector -> peer table -> header tool tip")];
[[fPeerTable tableColumnWithIdentifier: @"UL To"] setHeaderToolTip: NSLocalizedString(@"Uploading To Peer",
"inspector -> peer table -> header tool tip")];
[[fWebSeedTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Web Seed",
"inspector -> web seed table -> header tool tip")];
//prepare for animating peer table and web seed table
fViewTopMargin = fWebSeedTableTopConstraint.constant;
CABasicAnimation * webSeedTableAnimation = [CABasicAnimation animation];
[webSeedTableAnimation setTimingFunction: [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear]];
[webSeedTableAnimation setDuration: 0.125];
[webSeedTableAnimation setDelegate: self];
[webSeedTableAnimation setValue: WEB_SEED_ANIMATION_ID forKey: ANIMATION_ID_KEY];
[fWebSeedTableTopConstraint setAnimations: @{ @"constant": webSeedTableAnimation }];
[self setWebSeedTableHidden: YES animate: NO];
}
- (void) dealloc
{
[fTorrents release];
[fPeers release];
[fWebSeeds release];
[super dealloc];
}
#warning subclass?
- (void) setInfoForTorrents: (NSArray *) torrents
{
//don't check if it's the same in case the metadata changed
[fTorrents release];
fTorrents = [torrents retain];
fSet = NO;
}
- (void) updateInfo
{
if (!fSet)
[self setupInfo];
if ([fTorrents count] == 0)
return;
if (!fPeers)
fPeers = [[NSMutableArray alloc] init];
else
[fPeers removeAllObjects];
if (!fWebSeeds)
fWebSeeds = [[NSMutableArray alloc] init];
else
[fWebSeeds removeAllObjects];
NSUInteger connected = 0, tracker = 0, incoming = 0, cache = 0, lpd = 0, pex = 0, dht = 0, ltep = 0,
toUs = 0, fromUs = 0;
BOOL anyActive = false;
for (Torrent * torrent in fTorrents)
{
if ([torrent webSeedCount] > 0)
[fWebSeeds addObjectsFromArray: [torrent webSeeds]];
if ([torrent isActive])
{
anyActive = YES;
[fPeers addObjectsFromArray: [torrent peers]];
const NSUInteger connectedThis = [torrent totalPeersConnected];
if (connectedThis > 0)
{
connected += [torrent totalPeersConnected];
tracker += [torrent totalPeersTracker];
incoming += [torrent totalPeersIncoming];
cache += [torrent totalPeersCache];
lpd += [torrent totalPeersLocal];
pex += [torrent totalPeersPex];
dht += [torrent totalPeersDHT];
ltep += [torrent totalPeersLTEP];
toUs += [torrent peersSendingToUs];
fromUs += [torrent peersGettingFromUs];
}
}
}
[fPeers sortUsingDescriptors: [self peerSortDescriptors]];
[fPeerTable reloadData];
[fWebSeeds sortUsingDescriptors: [fWebSeedTable sortDescriptors]];
[fWebSeedTable reloadData];
[fWebSeedTable setWebSeeds: fWebSeeds];
if (anyActive)
{
NSString * connectedText = [NSString stringWithFormat: NSLocalizedString(@"%d Connected", "Inspector -> Peers tab -> peers"),
connected];
if (connected > 0)
{
NSMutableArray * upDownComponents = [NSMutableArray arrayWithCapacity: 2];
if (toUs > 0)
[upDownComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"DL from %d", "Inspector -> Peers tab -> peers"), toUs]];
if (fromUs > 0)
[upDownComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"UL to %d", "Inspector -> Peers tab -> peers"), fromUs]];
if ([upDownComponents count] > 0)
connectedText = [connectedText stringByAppendingFormat: @": %@", [upDownComponents componentsJoinedByString: @", "]];
NSMutableArray * fromComponents = [NSMutableArray arrayWithCapacity: 7];
if (tracker > 0)
[fromComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"%d tracker", "Inspector -> Peers tab -> peers"), tracker]];
if (incoming > 0)
[fromComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"%d incoming", "Inspector -> Peers tab -> peers"), incoming]];
if (cache > 0)
[fromComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"%d cache", "Inspector -> Peers tab -> peers"), cache]];
if (lpd > 0)
[fromComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"%d local discovery", "Inspector -> Peers tab -> peers"), lpd]];
if (pex > 0)
[fromComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"%d PEX", "Inspector -> Peers tab -> peers"), pex]];
if (dht > 0)
[fromComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"%d DHT", "Inspector -> Peers tab -> peers"), dht]];
if (ltep > 0)
[fromComponents addObject: [NSString stringWithFormat:
NSLocalizedString(@"%d LTEP", "Inspector -> Peers tab -> peers"), ltep]];
connectedText = [connectedText stringByAppendingFormat: @"\n%@", [fromComponents componentsJoinedByString: @", "]];
}
[fConnectedPeersField setStringValue: connectedText];
}
else
{
NSString * notActiveString;
if ([fTorrents count] == 1)
notActiveString = NSLocalizedString(@"Transfer Not Active", "Inspector -> Peers tab -> peers");
else
notActiveString = NSLocalizedString(@"Transfers Not Active", "Inspector -> Peers tab -> peers");
[fConnectedPeersField setStringValue: notActiveString];
}
}
- (void) saveViewSize
{
[[NSUserDefaults standardUserDefaults] setFloat: NSHeight([[self view] frame]) forKey: @"InspectorContentHeightPeers"];
}
- (void) clearView
{
[fPeers release];
fPeers = nil;
[fWebSeeds release];
fWebSeeds = nil;
}
- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
{
if (tableView == fWebSeedTable)
return fWebSeeds ? [fWebSeeds count] : 0;
else
return fPeers ? [fPeers count] : 0;
}
- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) column row: (NSInteger) row
{
if (tableView == fWebSeedTable)
{
NSString * ident = [column identifier];
NSDictionary * webSeed = [fWebSeeds objectAtIndex: row];
if ([ident isEqualToString: @"DL From"])
{
NSNumber * rate;
return (rate = [webSeed objectForKey: @"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @"";
}
else
return [webSeed objectForKey: @"Address"];
}
else
{
NSString * ident = [column identifier];
NSDictionary * peer = [fPeers objectAtIndex: row];
if ([ident isEqualToString: @"Encryption"])
return [[peer objectForKey: @"Encryption"] boolValue] ? [NSImage imageNamed: @"Lock"] : nil;
else if ([ident isEqualToString: @"Client"])
return [peer objectForKey: @"Client"];
else if ([ident isEqualToString: @"Progress"])
return [peer objectForKey: @"Progress"];
else if ([ident isEqualToString: @"UL To"])
{
NSNumber * rate;
return (rate = [peer objectForKey: @"UL To Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @"";
}
else if ([ident isEqualToString: @"DL From"])
{
NSNumber * rate;
return (rate = [peer objectForKey: @"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @"";
}
else
return [peer objectForKey: @"IP"];
}
}
- (void) tableView: (NSTableView *) tableView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn
row: (NSInteger) row
{
if (tableView == fPeerTable)
{
NSString * ident = [tableColumn identifier];
if ([ident isEqualToString: @"Progress"])
{
NSDictionary * peer = [fPeers objectAtIndex: row];
[(PeerProgressIndicatorCell *)cell setSeed: [[peer objectForKey: @"Seed"] boolValue]];
}
}
}
- (void) tableView: (NSTableView *) tableView didClickTableColumn: (NSTableColumn *) tableColumn
{
if (tableView == fWebSeedTable)
{
if (fWebSeeds)
{
[fWebSeeds sortUsingDescriptors: [fWebSeedTable sortDescriptors]];
[tableView reloadData];
}
}
else
{
if (fPeers)
{
[fPeers sortUsingDescriptors: [self peerSortDescriptors]];
[tableView reloadData];
}
}
}
- (BOOL) tableView: (NSTableView *) tableView shouldSelectRow: (NSInteger) row
{
return tableView != fPeerTable;
}
- (NSString *) tableView: (NSTableView *) tableView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect
tableColumn: (NSTableColumn *) column row: (NSInteger) row mouseLocation: (NSPoint) mouseLocation
{
if (tableView == fPeerTable)
{
const BOOL multiple = [fTorrents count] > 1;
NSDictionary * peer = [fPeers objectAtIndex: row];
NSMutableArray * components = [NSMutableArray arrayWithCapacity: multiple ? 6 : 5];
if (multiple)
[components addObject: [peer objectForKey: @"Name"]];
const CGFloat progress = [[peer objectForKey: @"Progress"] floatValue];
NSString * progressString = [NSString stringWithFormat: NSLocalizedString(@"Progress: %@",
"Inspector -> Peers tab -> table row tooltip"),
[NSString percentString: progress longDecimals: NO]];
if (progress < 1.0 && [[peer objectForKey: @"Seed"] boolValue])
progressString = [progressString stringByAppendingFormat: @" (%@)", NSLocalizedString(@"Partial Seed",
"Inspector -> Peers tab -> table row tooltip")];
[components addObject: progressString];
NSString * protocolString = [[peer objectForKey: @"uTP"] boolValue] ? @"\u00b5TP" : @"TCP";
if ([[peer objectForKey: @"Encryption"] boolValue])
protocolString = [protocolString stringByAppendingFormat: @" (%@)",
NSLocalizedString(@"encrypted", "Inspector -> Peers tab -> table row tooltip")];
[components addObject: [NSString stringWithFormat:
NSLocalizedString(@"Protocol: %@", "Inspector -> Peers tab -> table row tooltip"),
protocolString]];
NSString * portString;
NSInteger port;
if ((port = [[peer objectForKey: @"Port"] intValue]) > 0)
portString = [NSString stringWithFormat: @"%ld", port];
else
portString = NSLocalizedString(@"N/A", "Inspector -> Peers tab -> table row tooltip");
[components addObject: [NSString stringWithFormat: @"%@: %@", NSLocalizedString(@"Port",
"Inspector -> Peers tab -> table row tooltip"), portString]];
const NSInteger peerFrom = [[peer objectForKey: @"From"] integerValue];
switch (peerFrom)
{
case TR_PEER_FROM_TRACKER:
[components addObject: NSLocalizedString(@"From: tracker", "Inspector -> Peers tab -> table row tooltip")];
break;
case TR_PEER_FROM_INCOMING:
[components addObject: NSLocalizedString(@"From: incoming connection", "Inspector -> Peers tab -> table row tooltip")];
break;
case TR_PEER_FROM_RESUME:
[components addObject: NSLocalizedString(@"From: cache", "Inspector -> Peers tab -> table row tooltip")];
break;
case TR_PEER_FROM_LPD:
[components addObject: NSLocalizedString(@"From: local peer discovery", "Inspector -> Peers tab -> table row tooltip")];
break;
case TR_PEER_FROM_PEX:
[components addObject: NSLocalizedString(@"From: peer exchange", "Inspector -> Peers tab -> table row tooltip")];
break;
case TR_PEER_FROM_DHT:
[components addObject: NSLocalizedString(@"From: distributed hash table", "Inspector -> Peers tab -> table row tooltip")];
break;
case TR_PEER_FROM_LTEP:
[components addObject: NSLocalizedString(@"From: libtorrent extension protocol handshake",
"Inspector -> Peers tab -> table row tooltip")];
break;
default:
NSAssert1(NO, @"Peer from unknown source: %ld", peerFrom);
}
//determing status strings from flags
NSMutableArray * statusArray = [NSMutableArray arrayWithCapacity: 6];
NSString * flags = [peer objectForKey: @"Flags"];
if ([flags rangeOfString: @"D"].location != NSNotFound)
[statusArray addObject: NSLocalizedString(@"Currently downloading (interested and not choked)",
"Inspector -> peer -> status")];
if ([flags rangeOfString: @"d"].location != NSNotFound)
[statusArray addObject: NSLocalizedString(@"You want to download, but peer does not want to send (interested and choked)",
"Inspector -> peer -> status")];
if ([flags rangeOfString: @"U"].location != NSNotFound)
[statusArray addObject: NSLocalizedString(@"Currently uploading (interested and not choked)",
"Inspector -> peer -> status")];
if ([flags rangeOfString: @"u"].location != NSNotFound)
[statusArray addObject: NSLocalizedString(@"Peer wants you to upload, but you do not want to (interested and choked)",
"Inspector -> peer -> status")];
if ([flags rangeOfString: @"K"].location != NSNotFound)
[statusArray addObject: NSLocalizedString(@"Peer is unchoking you, but you are not interested",
"Inspector -> peer -> status")];
if ([flags rangeOfString: @"?"].location != NSNotFound)
[statusArray addObject: NSLocalizedString(@"You unchoked the peer, but the peer is not interested",
"Inspector -> peer -> status")];
if ([statusArray count] > 0)
{
NSString * statusStrings = [statusArray componentsJoinedByString: @"\n\n"];
[components addObject: [@"\n" stringByAppendingString: statusStrings]];
}
return [components componentsJoinedByString: @"\n"];
}
else
{
if ([fTorrents count] > 1)
return [[fWebSeeds objectAtIndex: row] objectForKey: @"Name"];
}
return nil;
}
- (void) animationDidStart: (CAAnimation *) animation
{
if (![[animation valueForKey: ANIMATION_ID_KEY] isEqualToString: WEB_SEED_ANIMATION_ID])
return;
[[fWebSeedTable enclosingScrollView] setHidden: NO];
}
- (void) animationDidStop: (CAAnimation *) animation finished: (BOOL) finished
{
if (![[animation valueForKey: ANIMATION_ID_KEY] isEqualToString: WEB_SEED_ANIMATION_ID])
return;
[[fWebSeedTable enclosingScrollView] setHidden: finished && fWebSeedTableTopConstraint.constant < 0];
}
@end
@implementation InfoPeersViewController (Private)
- (void) setupInfo
{
__block BOOL hasWebSeeds = NO;
if ([fTorrents count] == 0)
{
[fPeers release];
fPeers = nil;
[fPeerTable reloadData];
[fConnectedPeersField setStringValue: @""];
}
else
{
[fTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(Torrent * torrent, NSUInteger idx, BOOL *stop) {
if ([torrent webSeedCount] > 0)
{
hasWebSeeds = YES;
*stop = YES;
}
}];
}
if (!hasWebSeeds)
{
[fWebSeeds release];
fWebSeeds = nil;
[fWebSeedTable reloadData];
}
else
[fWebSeedTable deselectAll: self];
[self setWebSeedTableHidden: !hasWebSeeds animate: YES];
fSet = YES;
}
- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate
{
if (animate && (![[self view] window] || ![[[self view] window] isVisible]))
animate = NO;
const CGFloat webSeedTableTopMargin = hide ? -NSHeight([[fWebSeedTable enclosingScrollView] frame]) : fViewTopMargin;
[(animate ? [fWebSeedTableTopConstraint animator] : fWebSeedTableTopConstraint) setConstant: webSeedTableTopMargin];
}
- (NSArray *) peerSortDescriptors
{
NSMutableArray * descriptors = [NSMutableArray arrayWithCapacity: 2];
NSArray * oldDescriptors = [fPeerTable sortDescriptors];
BOOL useSecond = YES, asc = YES;
if ([oldDescriptors count] > 0)
{
NSSortDescriptor * descriptor = [oldDescriptors objectAtIndex: 0];
[descriptors addObject: descriptor];
if ((useSecond = ![[descriptor key] isEqualToString: @"IP"]))
asc = [descriptor ascending];
}
//sort by IP after primary sort
if (useSecond)
{
NSSortDescriptor * secondDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"IP" ascending: asc selector: @selector(compareNumeric:)];
[descriptors addObject: secondDescriptor];
}
return descriptors;
}
@end