1/****************************************************************************** 2 * Copyright (c) 2010-2012 Transmission authors and contributors 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a 5 * copy of this software and associated documentation files (the "Software"), 6 * to deal in the Software without restriction, including without limitation 7 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 * and/or sell copies of the Software, and to permit persons to whom the 9 * Software is furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 * DEALINGS IN THE SOFTWARE. 21 *****************************************************************************/ 22 23#include <libtransmission/transmission.h> 24#include <libtransmission/utils.h> 25 26#import "InfoPeersViewController.h" 27#import "NSApplicationAdditions.h" 28#import "NSStringAdditions.h" 29#import "PeerProgressIndicatorCell.h" 30#import "Torrent.h" 31#import "WebSeedTableView.h" 32 33#define ANIMATION_ID_KEY @"animationId" 34#define WEB_SEED_ANIMATION_ID @"webSeed" 35 36@interface InfoPeersViewController (Private) 37 38- (void) setupInfo; 39 40- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate; 41- (NSArray *) peerSortDescriptors; 42 43@end 44 45@implementation InfoPeersViewController 46 47- (id) init 48{ 49 if ((self = [super initWithNibName: @"InfoPeersView" bundle: nil])) 50 { 51 [self setTitle: NSLocalizedString(@"Peers", "Inspector view -> title")]; 52 } 53 54 return self; 55} 56 57- (void) awakeFromNib 58{ 59 const CGFloat height = [[NSUserDefaults standardUserDefaults] floatForKey: @"InspectorContentHeightPeers"]; 60 if (height != 0.0) 61 { 62 NSRect viewRect = [[self view] frame]; 63 viewRect.size.height = height; 64 [[self view] setFrame: viewRect]; 65 } 66 67 //set table header text 68 [[[fPeerTable tableColumnWithIdentifier: @"IP"] headerCell] setStringValue: NSLocalizedString(@"IP Address", 69 "inspector -> peer table -> header")]; 70 [[[fPeerTable tableColumnWithIdentifier: @"Client"] headerCell] setStringValue: NSLocalizedString(@"Client", 71 "inspector -> peer table -> header")]; 72 [[[fPeerTable tableColumnWithIdentifier: @"DL From"] headerCell] setStringValue: NSLocalizedString(@"DL", 73 "inspector -> peer table -> header")]; 74 [[[fPeerTable tableColumnWithIdentifier: @"UL To"] headerCell] setStringValue: NSLocalizedString(@"UL", 75 "inspector -> peer table -> header")]; 76 77 [[[fWebSeedTable tableColumnWithIdentifier: @"Address"] headerCell] setStringValue: NSLocalizedString(@"Web Seeds", 78 "inspector -> web seed table -> header")]; 79 [[[fWebSeedTable tableColumnWithIdentifier: @"DL From"] headerCell] setStringValue: NSLocalizedString(@"DL", 80 "inspector -> web seed table -> header")]; 81 82 //set table header tool tips 83 [[fPeerTable tableColumnWithIdentifier: @"Encryption"] setHeaderToolTip: NSLocalizedString(@"Encrypted Connection", 84 "inspector -> peer table -> header tool tip")]; 85 [[fPeerTable tableColumnWithIdentifier: @"Progress"] setHeaderToolTip: NSLocalizedString(@"Available", 86 "inspector -> peer table -> header tool tip")]; 87 [[fPeerTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Peer", 88 "inspector -> peer table -> header tool tip")]; 89 [[fPeerTable tableColumnWithIdentifier: @"UL To"] setHeaderToolTip: NSLocalizedString(@"Uploading To Peer", 90 "inspector -> peer table -> header tool tip")]; 91 92 [[fWebSeedTable tableColumnWithIdentifier: @"DL From"] setHeaderToolTip: NSLocalizedString(@"Downloading From Web Seed", 93 "inspector -> web seed table -> header tool tip")]; 94 95 //prepare for animating peer table and web seed table 96 fViewTopMargin = fWebSeedTableTopConstraint.constant; 97 98 CABasicAnimation * webSeedTableAnimation = [CABasicAnimation animation]; 99 [webSeedTableAnimation setTimingFunction: [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear]]; 100 [webSeedTableAnimation setDuration: 0.125]; 101 [webSeedTableAnimation setDelegate: self]; 102 [webSeedTableAnimation setValue: WEB_SEED_ANIMATION_ID forKey: ANIMATION_ID_KEY]; 103 [fWebSeedTableTopConstraint setAnimations: @{ @"constant": webSeedTableAnimation }]; 104 105 [self setWebSeedTableHidden: YES animate: NO]; 106} 107 108 109#warning subclass? 110- (void) setInfoForTorrents: (NSArray *) torrents 111{ 112 //don't check if it's the same in case the metadata changed 113 fTorrents = torrents; 114 115 fSet = NO; 116} 117 118- (void) updateInfo 119{ 120 if (!fSet) 121 [self setupInfo]; 122 123 if ([fTorrents count] == 0) 124 return; 125 126 if (!fPeers) 127 fPeers = [[NSMutableArray alloc] init]; 128 else 129 [fPeers removeAllObjects]; 130 131 if (!fWebSeeds) 132 fWebSeeds = [[NSMutableArray alloc] init]; 133 else 134 [fWebSeeds removeAllObjects]; 135 136 NSUInteger connected = 0, tracker = 0, incoming = 0, cache = 0, lpd = 0, pex = 0, dht = 0, ltep = 0, 137 toUs = 0, fromUs = 0; 138 BOOL anyActive = false; 139 for (Torrent * torrent in fTorrents) 140 { 141 if ([torrent webSeedCount] > 0) 142 [fWebSeeds addObjectsFromArray: [torrent webSeeds]]; 143 144 if ([torrent isActive]) 145 { 146 anyActive = YES; 147 [fPeers addObjectsFromArray: [torrent peers]]; 148 149 const NSUInteger connectedThis = [torrent totalPeersConnected]; 150 if (connectedThis > 0) 151 { 152 connected += [torrent totalPeersConnected]; 153 tracker += [torrent totalPeersTracker]; 154 incoming += [torrent totalPeersIncoming]; 155 cache += [torrent totalPeersCache]; 156 lpd += [torrent totalPeersLocal]; 157 pex += [torrent totalPeersPex]; 158 dht += [torrent totalPeersDHT]; 159 ltep += [torrent totalPeersLTEP]; 160 161 toUs += [torrent peersSendingToUs]; 162 fromUs += [torrent peersGettingFromUs]; 163 } 164 } 165 } 166 167 [fPeers sortUsingDescriptors: [self peerSortDescriptors]]; 168 [fPeerTable reloadData]; 169 170 [fWebSeeds sortUsingDescriptors: [fWebSeedTable sortDescriptors]]; 171 [fWebSeedTable reloadData]; 172 [fWebSeedTable setWebSeeds: fWebSeeds]; 173 174 if (anyActive) 175 { 176 NSString * connectedText = [NSString stringWithFormat: NSLocalizedString(@"%d Connected", "Inspector -> Peers tab -> peers"), 177 connected]; 178 179 if (connected > 0) 180 { 181 NSMutableArray * upDownComponents = [NSMutableArray arrayWithCapacity: 2]; 182 if (toUs > 0) 183 [upDownComponents addObject: [NSString stringWithFormat: 184 NSLocalizedString(@"DL from %d", "Inspector -> Peers tab -> peers"), toUs]]; 185 if (fromUs > 0) 186 [upDownComponents addObject: [NSString stringWithFormat: 187 NSLocalizedString(@"UL to %d", "Inspector -> Peers tab -> peers"), fromUs]]; 188 if ([upDownComponents count] > 0) 189 connectedText = [connectedText stringByAppendingFormat: @": %@", [upDownComponents componentsJoinedByString: @", "]]; 190 191 NSMutableArray * fromComponents = [NSMutableArray arrayWithCapacity: 7]; 192 if (tracker > 0) 193 [fromComponents addObject: [NSString stringWithFormat: 194 NSLocalizedString(@"%d tracker", "Inspector -> Peers tab -> peers"), tracker]]; 195 if (incoming > 0) 196 [fromComponents addObject: [NSString stringWithFormat: 197 NSLocalizedString(@"%d incoming", "Inspector -> Peers tab -> peers"), incoming]]; 198 if (cache > 0) 199 [fromComponents addObject: [NSString stringWithFormat: 200 NSLocalizedString(@"%d cache", "Inspector -> Peers tab -> peers"), cache]]; 201 if (lpd > 0) 202 [fromComponents addObject: [NSString stringWithFormat: 203 NSLocalizedString(@"%d local discovery", "Inspector -> Peers tab -> peers"), lpd]]; 204 if (pex > 0) 205 [fromComponents addObject: [NSString stringWithFormat: 206 NSLocalizedString(@"%d PEX", "Inspector -> Peers tab -> peers"), pex]]; 207 if (dht > 0) 208 [fromComponents addObject: [NSString stringWithFormat: 209 NSLocalizedString(@"%d DHT", "Inspector -> Peers tab -> peers"), dht]]; 210 if (ltep > 0) 211 [fromComponents addObject: [NSString stringWithFormat: 212 NSLocalizedString(@"%d LTEP", "Inspector -> Peers tab -> peers"), ltep]]; 213 214 connectedText = [connectedText stringByAppendingFormat: @"\n%@", [fromComponents componentsJoinedByString: @", "]]; 215 } 216 217 [fConnectedPeersField setStringValue: connectedText]; 218 } 219 else 220 { 221 NSString * notActiveString; 222 if ([fTorrents count] == 1) 223 notActiveString = NSLocalizedString(@"Transfer Not Active", "Inspector -> Peers tab -> peers"); 224 else 225 notActiveString = NSLocalizedString(@"Transfers Not Active", "Inspector -> Peers tab -> peers"); 226 227 [fConnectedPeersField setStringValue: notActiveString]; 228 } 229} 230 231- (void) saveViewSize 232{ 233 [[NSUserDefaults standardUserDefaults] setFloat: NSHeight([[self view] frame]) forKey: @"InspectorContentHeightPeers"]; 234} 235 236- (void) clearView 237{ 238 fPeers = nil; 239 fWebSeeds = nil; 240} 241 242- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView 243{ 244 if (tableView == fWebSeedTable) 245 return fWebSeeds ? [fWebSeeds count] : 0; 246 else 247 return fPeers ? [fPeers count] : 0; 248} 249 250- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) column row: (NSInteger) row 251{ 252 if (tableView == fWebSeedTable) 253 { 254 NSString * ident = [column identifier]; 255 NSDictionary * webSeed = fWebSeeds[row]; 256 257 if ([ident isEqualToString: @"DL From"]) 258 { 259 NSNumber * rate; 260 return (rate = webSeed[@"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @""; 261 } 262 else 263 return webSeed[@"Address"]; 264 } 265 else 266 { 267 NSString * ident = [column identifier]; 268 NSDictionary * peer = fPeers[row]; 269 270 if ([ident isEqualToString: @"Encryption"]) 271 return [peer[@"Encryption"] boolValue] ? [NSImage imageNamed: @"Lock"] : nil; 272 else if ([ident isEqualToString: @"Client"]) 273 return peer[@"Client"]; 274 else if ([ident isEqualToString: @"Progress"]) 275 return peer[@"Progress"]; 276 else if ([ident isEqualToString: @"UL To"]) 277 { 278 NSNumber * rate; 279 return (rate = peer[@"UL To Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @""; 280 } 281 else if ([ident isEqualToString: @"DL From"]) 282 { 283 NSNumber * rate; 284 return (rate = peer[@"DL From Rate"]) ? [NSString stringForSpeedAbbrev: [rate doubleValue]] : @""; 285 } 286 else 287 return peer[@"IP"]; 288 } 289} 290 291- (void) tableView: (NSTableView *) tableView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn 292 row: (NSInteger) row 293{ 294 if (tableView == fPeerTable) 295 { 296 NSString * ident = [tableColumn identifier]; 297 298 if ([ident isEqualToString: @"Progress"]) 299 { 300 NSDictionary * peer = fPeers[row]; 301 [(PeerProgressIndicatorCell *)cell setSeed: [peer[@"Seed"] boolValue]]; 302 } 303 } 304} 305 306- (void) tableView: (NSTableView *) tableView didClickTableColumn: (NSTableColumn *) tableColumn 307{ 308 if (tableView == fWebSeedTable) 309 { 310 if (fWebSeeds) 311 { 312 [fWebSeeds sortUsingDescriptors: [fWebSeedTable sortDescriptors]]; 313 [tableView reloadData]; 314 } 315 } 316 else 317 { 318 if (fPeers) 319 { 320 [fPeers sortUsingDescriptors: [self peerSortDescriptors]]; 321 [tableView reloadData]; 322 } 323 } 324} 325 326- (BOOL) tableView: (NSTableView *) tableView shouldSelectRow: (NSInteger) row 327{ 328 return tableView != fPeerTable; 329} 330 331- (NSString *) tableView: (NSTableView *) tableView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect 332 tableColumn: (NSTableColumn *) column row: (NSInteger) row mouseLocation: (NSPoint) mouseLocation 333{ 334 if (tableView == fPeerTable) 335 { 336 const BOOL multiple = [fTorrents count] > 1; 337 338 NSDictionary * peer = fPeers[row]; 339 NSMutableArray * components = [NSMutableArray arrayWithCapacity: multiple ? 6 : 5]; 340 341 if (multiple) 342 [components addObject: peer[@"Name"]]; 343 344 const CGFloat progress = [peer[@"Progress"] floatValue]; 345 NSString * progressString = [NSString stringWithFormat: NSLocalizedString(@"Progress: %@", 346 "Inspector -> Peers tab -> table row tooltip"), 347 [NSString percentString: progress longDecimals: NO]]; 348 if (progress < 1.0 && [peer[@"Seed"] boolValue]) 349 progressString = [progressString stringByAppendingFormat: @" (%@)", NSLocalizedString(@"Partial Seed", 350 "Inspector -> Peers tab -> table row tooltip")]; 351 [components addObject: progressString]; 352 353 NSString * protocolString = [peer[@"uTP"] boolValue] ? @"\u00b5TP" : @"TCP"; 354 if ([peer[@"Encryption"] boolValue]) 355 protocolString = [protocolString stringByAppendingFormat: @" (%@)", 356 NSLocalizedString(@"encrypted", "Inspector -> Peers tab -> table row tooltip")]; 357 [components addObject: [NSString stringWithFormat: 358 NSLocalizedString(@"Protocol: %@", "Inspector -> Peers tab -> table row tooltip"), 359 protocolString]]; 360 361 NSString * portString; 362 NSInteger port; 363 if ((port = [peer[@"Port"] intValue]) > 0) 364 portString = [NSString stringWithFormat: @"%ld", port]; 365 else 366 portString = NSLocalizedString(@"N/A", "Inspector -> Peers tab -> table row tooltip"); 367 [components addObject: [NSString stringWithFormat: @"%@: %@", NSLocalizedString(@"Port", 368 "Inspector -> Peers tab -> table row tooltip"), portString]]; 369 370 const NSInteger peerFrom = [peer[@"From"] integerValue]; 371 switch (peerFrom) 372 { 373 case TR_PEER_FROM_TRACKER: 374 [components addObject: NSLocalizedString(@"From: tracker", "Inspector -> Peers tab -> table row tooltip")]; 375 break; 376 case TR_PEER_FROM_INCOMING: 377 [components addObject: NSLocalizedString(@"From: incoming connection", "Inspector -> Peers tab -> table row tooltip")]; 378 break; 379 case TR_PEER_FROM_RESUME: 380 [components addObject: NSLocalizedString(@"From: cache", "Inspector -> Peers tab -> table row tooltip")]; 381 break; 382 case TR_PEER_FROM_LPD: 383 [components addObject: NSLocalizedString(@"From: local peer discovery", "Inspector -> Peers tab -> table row tooltip")]; 384 break; 385 case TR_PEER_FROM_PEX: 386 [components addObject: NSLocalizedString(@"From: peer exchange", "Inspector -> Peers tab -> table row tooltip")]; 387 break; 388 case TR_PEER_FROM_DHT: 389 [components addObject: NSLocalizedString(@"From: distributed hash table", "Inspector -> Peers tab -> table row tooltip")]; 390 break; 391 case TR_PEER_FROM_LTEP: 392 [components addObject: NSLocalizedString(@"From: libtorrent extension protocol handshake", 393 "Inspector -> Peers tab -> table row tooltip")]; 394 break; 395 default: 396 NSAssert1(NO, @"Peer from unknown source: %ld", peerFrom); 397 } 398 399 //determing status strings from flags 400 NSMutableArray * statusArray = [NSMutableArray arrayWithCapacity: 6]; 401 NSString * flags = peer[@"Flags"]; 402 403 if ([flags rangeOfString: @"D"].location != NSNotFound) 404 [statusArray addObject: NSLocalizedString(@"Currently downloading (interested and not choked)", 405 "Inspector -> peer -> status")]; 406 if ([flags rangeOfString: @"d"].location != NSNotFound) 407 [statusArray addObject: NSLocalizedString(@"You want to download, but peer does not want to send (interested and choked)", 408 "Inspector -> peer -> status")]; 409 if ([flags rangeOfString: @"U"].location != NSNotFound) 410 [statusArray addObject: NSLocalizedString(@"Currently uploading (interested and not choked)", 411 "Inspector -> peer -> status")]; 412 if ([flags rangeOfString: @"u"].location != NSNotFound) 413 [statusArray addObject: NSLocalizedString(@"Peer wants you to upload, but you do not want to (interested and choked)", 414 "Inspector -> peer -> status")]; 415 if ([flags rangeOfString: @"K"].location != NSNotFound) 416 [statusArray addObject: NSLocalizedString(@"Peer is unchoking you, but you are not interested", 417 "Inspector -> peer -> status")]; 418 if ([flags rangeOfString: @"?"].location != NSNotFound) 419 [statusArray addObject: NSLocalizedString(@"You unchoked the peer, but the peer is not interested", 420 "Inspector -> peer -> status")]; 421 422 if ([statusArray count] > 0) 423 { 424 NSString * statusStrings = [statusArray componentsJoinedByString: @"\n\n"]; 425 [components addObject: [@"\n" stringByAppendingString: statusStrings]]; 426 } 427 428 return [components componentsJoinedByString: @"\n"]; 429 } 430 else 431 { 432 if ([fTorrents count] > 1) 433 return fWebSeeds[row][@"Name"]; 434 } 435 436 return nil; 437} 438 439- (void) animationDidStart: (CAAnimation *) animation 440{ 441 if (![[animation valueForKey: ANIMATION_ID_KEY] isEqualToString: WEB_SEED_ANIMATION_ID]) 442 return; 443 444 [[fWebSeedTable enclosingScrollView] setHidden: NO]; 445} 446 447- (void) animationDidStop: (CAAnimation *) animation finished: (BOOL) finished 448{ 449 if (![[animation valueForKey: ANIMATION_ID_KEY] isEqualToString: WEB_SEED_ANIMATION_ID]) 450 return; 451 452 [[fWebSeedTable enclosingScrollView] setHidden: finished && fWebSeedTableTopConstraint.constant < 0]; 453} 454 455@end 456 457@implementation InfoPeersViewController (Private) 458 459- (void) setupInfo 460{ 461 __block BOOL hasWebSeeds = NO; 462 463 if ([fTorrents count] == 0) 464 { 465 fPeers = nil; 466 [fPeerTable reloadData]; 467 468 [fConnectedPeersField setStringValue: @""]; 469 } 470 else 471 { 472 [fTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(Torrent * torrent, NSUInteger idx, BOOL *stop) { 473 if ([torrent webSeedCount] > 0) 474 { 475 hasWebSeeds = YES; 476 *stop = YES; 477 } 478 }]; 479 } 480 481 if (!hasWebSeeds) 482 { 483 fWebSeeds = nil; 484 [fWebSeedTable reloadData]; 485 } 486 else 487 [fWebSeedTable deselectAll: self]; 488 [self setWebSeedTableHidden: !hasWebSeeds animate: YES]; 489 490 fSet = YES; 491} 492 493- (void) setWebSeedTableHidden: (BOOL) hide animate: (BOOL) animate 494{ 495 if (animate && (![[self view] window] || ![[[self view] window] isVisible])) 496 animate = NO; 497 498 const CGFloat webSeedTableTopMargin = hide ? -NSHeight([[fWebSeedTable enclosingScrollView] frame]) : fViewTopMargin; 499 500 [(animate ? [fWebSeedTableTopConstraint animator] : fWebSeedTableTopConstraint) setConstant: webSeedTableTopMargin]; 501} 502 503- (NSArray *) peerSortDescriptors 504{ 505 NSMutableArray * descriptors = [NSMutableArray arrayWithCapacity: 2]; 506 507 NSArray * oldDescriptors = [fPeerTable sortDescriptors]; 508 BOOL useSecond = YES, asc = YES; 509 if ([oldDescriptors count] > 0) 510 { 511 NSSortDescriptor * descriptor = oldDescriptors[0]; 512 [descriptors addObject: descriptor]; 513 514 if ((useSecond = ![[descriptor key] isEqualToString: @"IP"])) 515 asc = [descriptor ascending]; 516 } 517 518 //sort by IP after primary sort 519 if (useSecond) 520 { 521 NSSortDescriptor * secondDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"IP" ascending: asc selector: @selector(compareNumeric:)]; 522 [descriptors addObject: secondDescriptor]; 523 } 524 525 return descriptors; 526} 527 528@end 529