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#import "InfoTrackersViewController.h" 24#import "NSApplicationAdditions.h" 25#import "Torrent.h" 26#import "TrackerCell.h" 27#import "TrackerNode.h" 28#import "TrackerTableView.h" 29 30#define TRACKER_GROUP_SEPARATOR_HEIGHT 14.0 31 32#define TRACKER_ADD_TAG 0 33#define TRACKER_REMOVE_TAG 1 34 35 36@interface InfoTrackersViewController (Private) 37 38- (void) setupInfo; 39 40- (void) addTrackers; 41- (void) removeTrackers; 42 43@end 44 45@implementation InfoTrackersViewController 46 47- (id) init 48{ 49 if ((self = [super initWithNibName: @"InfoTrackersView" bundle: nil])) 50 { 51 [self setTitle: NSLocalizedString(@"Trackers", "Inspector view -> title")]; 52 53 fTrackerCell = [[TrackerCell alloc] init]; 54 } 55 56 return self; 57} 58 59- (void) awakeFromNib 60{ 61 [[fTrackerAddRemoveControl cell] setToolTip: NSLocalizedString(@"Add a tracker", "Inspector view -> tracker buttons") 62 forSegment: TRACKER_ADD_TAG]; 63 [[fTrackerAddRemoveControl cell] setToolTip: NSLocalizedString(@"Remove selected trackers", "Inspector view -> tracker buttons") 64 forSegment: TRACKER_REMOVE_TAG]; 65 66 const CGFloat height = [[NSUserDefaults standardUserDefaults] floatForKey: @"InspectorContentHeightTracker"]; 67 if (height != 0.0) 68 { 69 NSRect viewRect = [[self view] frame]; 70 viewRect.size.height = height; 71 [[self view] setFrame: viewRect]; 72 } 73} 74 75 76- (void) setInfoForTorrents: (NSArray *) torrents 77{ 78 //don't check if it's the same in case the metadata changed 79 fTorrents = torrents; 80 81 fSet = NO; 82} 83 84- (void) updateInfo 85{ 86 if (!fSet) 87 [self setupInfo]; 88 89 if ([fTorrents count] == 0) 90 return; 91 92 //get updated tracker stats 93 if ([fTrackerTable editedRow] == -1) 94 { 95 NSArray * oldTrackers = fTrackers; 96 97 if ([fTorrents count] == 1) 98 fTrackers = [fTorrents[0] allTrackerStats]; 99 else 100 { 101 fTrackers = [[NSMutableArray alloc] init]; 102 for (Torrent * torrent in fTorrents) 103 [fTrackers addObjectsFromArray: [torrent allTrackerStats]]; 104 } 105 106 [fTrackerTable setTrackers: fTrackers]; 107 108 if (oldTrackers && [fTrackers isEqualToArray: oldTrackers]) 109 [fTrackerTable setNeedsDisplay: YES]; 110 else 111 [fTrackerTable reloadData]; 112 113 } 114 else 115 { 116 NSAssert1([fTorrents count] == 1, @"Attempting to add tracker with %ld transfers selected", [fTorrents count]); 117 118 NSIndexSet * addedIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange([fTrackers count]-2, 2)]; 119 NSArray * tierAndTrackerBeingAdded = [fTrackers objectsAtIndexes: addedIndexes]; 120 121 fTrackers = [fTorrents[0] allTrackerStats]; 122 [fTrackers addObjectsFromArray: tierAndTrackerBeingAdded]; 123 124 [fTrackerTable setTrackers: fTrackers]; 125 126 NSIndexSet * updateIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTrackers count]-2)], 127 * columnIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [[fTrackerTable tableColumns] count])]; 128 [fTrackerTable reloadDataForRowIndexes: updateIndexes columnIndexes: columnIndexes]; 129 } 130} 131 132- (void) saveViewSize 133{ 134 [[NSUserDefaults standardUserDefaults] setFloat: NSHeight([[self view] frame]) forKey: @"InspectorContentHeightTracker"]; 135} 136 137- (void) clearView 138{ 139 fTrackers = nil; 140} 141 142- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView 143{ 144 return fTrackers ? [fTrackers count] : 0; 145} 146 147- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) column row: (NSInteger) row 148{ 149 id item = fTrackers[row]; 150 151 if ([item isKindOfClass: [NSDictionary class]]) 152 { 153 const NSInteger tier = [item[@"Tier"] integerValue]; 154 NSString * tierString = tier == -1 ? NSLocalizedString(@"New Tier", "Inspector -> tracker table") 155 : [NSString stringWithFormat: NSLocalizedString(@"Tier %d", "Inspector -> tracker table"), tier]; 156 157 if ([fTorrents count] > 1) 158 tierString = [tierString stringByAppendingFormat: @" - %@", item[@"Name"]]; 159 return tierString; 160 } 161 else 162 return item; //TrackerNode or NSString 163} 164 165- (NSCell *) tableView: (NSTableView *) tableView dataCellForTableColumn: (NSTableColumn *) tableColumn row: (NSInteger) row 166{ 167 const BOOL tracker = [fTrackers[row] isKindOfClass: [TrackerNode class]]; 168 return tracker ? fTrackerCell : [tableColumn dataCellForRow: row]; 169} 170 171- (CGFloat) tableView: (NSTableView *) tableView heightOfRow: (NSInteger) row 172{ 173 //check for NSDictionay instead of TrackerNode because of display issue when adding a row 174 if ([fTrackers[row] isKindOfClass: [NSDictionary class]]) 175 return TRACKER_GROUP_SEPARATOR_HEIGHT; 176 else 177 return [tableView rowHeight]; 178} 179 180- (BOOL) tableView: (NSTableView *) tableView shouldEditTableColumn: (NSTableColumn *) tableColumn row: (NSInteger) row 181{ 182 //don't allow tier row to be edited by double-click 183 return NO; 184} 185 186- (void) tableViewSelectionDidChange: (NSNotification *) notification 187{ 188 [fTrackerAddRemoveControl setEnabled: [fTrackerTable numberOfSelectedRows] > 0 forSegment: TRACKER_REMOVE_TAG]; 189} 190 191- (BOOL) tableView: (NSTableView *) tableView isGroupRow: (NSInteger) row 192{ 193 return ![fTrackers[row] isKindOfClass: [TrackerNode class]] && [tableView editedRow] != row; 194} 195 196- (NSString *) tableView: (NSTableView *) tableView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect 197 tableColumn: (NSTableColumn *) column row: (NSInteger) row mouseLocation: (NSPoint) mouseLocation 198{ 199 id node = fTrackers[row]; 200 if ([node isKindOfClass: [TrackerNode class]]) 201 return [(TrackerNode *)node fullAnnounceAddress]; 202 else 203 return nil; 204} 205 206- (void) tableView: (NSTableView *) tableView setObjectValue: (id) object forTableColumn: (NSTableColumn *) tableColumn 207 row: (NSInteger) row 208{ 209 Torrent * torrent= fTorrents[0]; 210 211 BOOL added = NO; 212 for (NSString * tracker in [object componentsSeparatedByString: @"\n"]) 213 if ([torrent addTrackerToNewTier: tracker]) 214 added = YES; 215 216 if (!added) 217 NSBeep(); 218 219 //reset table with either new or old value 220 fTrackers = [torrent allTrackerStats]; 221 222 [fTrackerTable setTrackers: fTrackers]; 223 [fTrackerTable reloadData]; 224 [fTrackerTable deselectAll: self]; 225 226 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil]; //incase sort by tracker 227} 228 229- (void) addRemoveTracker: (id) sender 230{ 231 //don't allow add/remove when currently adding - it leads to weird results 232 if ([fTrackerTable editedRow] != -1) 233 return; 234 235 [self updateInfo]; 236 237 if ([[sender cell] tagForSegment: [sender selectedSegment]] == TRACKER_REMOVE_TAG) 238 [self removeTrackers]; 239 else 240 [self addTrackers]; 241} 242 243@end 244 245@implementation InfoTrackersViewController (Private) 246 247- (void) setupInfo 248{ 249 const NSUInteger numberSelected = [fTorrents count]; 250 if (numberSelected != 1) 251 { 252 if (numberSelected == 0) 253 { 254 fTrackers = nil; 255 256 [fTrackerTable setTrackers: nil]; 257 [fTrackerTable reloadData]; 258 } 259 260 [fTrackerTable setTorrent: nil]; 261 262 [fTrackerAddRemoveControl setEnabled: NO forSegment: TRACKER_ADD_TAG]; 263 [fTrackerAddRemoveControl setEnabled: NO forSegment: TRACKER_REMOVE_TAG]; 264 } 265 else 266 { 267 [fTrackerTable setTorrent: fTorrents[0]]; 268 269 [fTrackerAddRemoveControl setEnabled: YES forSegment: TRACKER_ADD_TAG]; 270 [fTrackerAddRemoveControl setEnabled: NO forSegment: TRACKER_REMOVE_TAG]; 271 } 272 273 [fTrackerTable deselectAll: self]; 274 275 fSet = YES; 276} 277 278#warning doesn't like blank addresses 279- (void) addTrackers 280{ 281 [[[self view] window] makeKeyWindow]; 282 283 NSAssert1([fTorrents count] == 1, @"Attempting to add tracker with %ld transfers selected", [fTorrents count]); 284 285 [fTrackers addObject: @{@"Tier": @-1}]; 286 [fTrackers addObject: @""]; 287 288 [fTrackerTable setTrackers: fTrackers]; 289 [fTrackerTable reloadData]; 290 [fTrackerTable selectRowIndexes: [NSIndexSet indexSetWithIndex: [fTrackers count]-1] byExtendingSelection: NO]; 291 [fTrackerTable editColumn: [fTrackerTable columnWithIdentifier: @"Tracker"] row: [fTrackers count]-1 withEvent: nil select: YES]; 292} 293 294- (void) removeTrackers 295{ 296 NSMutableDictionary * removeIdentifiers = [NSMutableDictionary dictionaryWithCapacity: [fTorrents count]]; 297 NSUInteger removeTrackerCount = 0; 298 299 NSIndexSet * selectedIndexes = [fTrackerTable selectedRowIndexes]; 300 BOOL groupSelected = NO; 301 NSUInteger groupRowIndex = NSNotFound; 302 NSMutableIndexSet * removeIndexes = [NSMutableIndexSet indexSet]; 303 for (NSUInteger i = 0; i < [fTrackers count]; ++i) 304 { 305 id object = fTrackers[i]; 306 if ([object isKindOfClass: [TrackerNode class]]) 307 { 308 if (groupSelected || [selectedIndexes containsIndex: i]) 309 { 310 Torrent * torrent = [(TrackerNode *)object torrent]; 311 NSMutableSet * removeSet; 312 if (!(removeSet = removeIdentifiers[torrent])) 313 { 314 removeSet = [NSMutableSet set]; 315 removeIdentifiers[torrent] = removeSet; 316 } 317 318 [removeSet addObject: [(TrackerNode *)object fullAnnounceAddress]]; 319 ++removeTrackerCount; 320 321 [removeIndexes addIndex: i]; 322 } 323 else 324 groupRowIndex = NSNotFound; //don't remove the group row 325 } 326 else 327 { 328 //mark the previous group row for removal, if necessary 329 if (groupRowIndex != NSNotFound) 330 [removeIndexes addIndex: groupRowIndex]; 331 332 groupSelected = [selectedIndexes containsIndex: i]; 333 if (!groupSelected && i > [selectedIndexes lastIndex]) 334 { 335 groupRowIndex = NSNotFound; 336 break; 337 } 338 339 groupRowIndex = i; 340 } 341 } 342 343 //mark the last group for removal, too 344 if (groupRowIndex != NSNotFound) 345 [removeIndexes addIndex: groupRowIndex]; 346 347 NSAssert2(removeTrackerCount <= [removeIndexes count], @"Marked %ld trackers to remove, but only removing %ld rows", removeTrackerCount, [removeIndexes count]); 348 349 //we might have no trackers if remove right after a failed add (race condition ftw) 350 #warning look into having a failed add apply right away, so that this can become an assert 351 if (removeTrackerCount == 0) 352 return; 353 354 if ([[NSUserDefaults standardUserDefaults] boolForKey: @"WarningRemoveTrackers"]) 355 { 356 NSAlert * alert = [[NSAlert alloc] init]; 357 358 if (removeTrackerCount > 1) 359 { 360 [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Are you sure you want to remove %d trackers?", 361 "Remove trackers alert -> title"), removeTrackerCount]]; 362 [alert setInformativeText: NSLocalizedString(@"Once removed, Transmission will no longer attempt to contact them." 363 " This cannot be undone.", "Remove trackers alert -> message")]; 364 } 365 else 366 { 367 [alert setMessageText: NSLocalizedString(@"Are you sure you want to remove this tracker?", "Remove trackers alert -> title")]; 368 [alert setInformativeText: NSLocalizedString(@"Once removed, Transmission will no longer attempt to contact it." 369 " This cannot be undone.", "Remove trackers alert -> message")]; 370 } 371 372 [alert addButtonWithTitle: NSLocalizedString(@"Remove", "Remove trackers alert -> button")]; 373 [alert addButtonWithTitle: NSLocalizedString(@"Cancel", "Remove trackers alert -> button")]; 374 375 [alert setShowsSuppressionButton: YES]; 376 377 NSInteger result = [alert runModal]; 378 if ([[alert suppressionButton] state] == NSOnState) 379 [[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"WarningRemoveTrackers"]; 380 381 if (result != NSAlertFirstButtonReturn) 382 return; 383 } 384 385 386 [fTrackerTable beginUpdates]; 387 388 for (Torrent * torrent in removeIdentifiers) 389 [torrent removeTrackers: removeIdentifiers[torrent]]; 390 391 //reset table with either new or old value 392 fTrackers = [[NSMutableArray alloc] init]; 393 for (Torrent * torrent in fTorrents) 394 [fTrackers addObjectsFromArray: [torrent allTrackerStats]]; 395 396 [fTrackerTable removeRowsAtIndexes: removeIndexes withAnimation: NSTableViewAnimationSlideLeft]; 397 398 [fTrackerTable setTrackers: fTrackers]; 399 400 [fTrackerTable endUpdates]; 401 402 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil]; //incase sort by tracker 403} 404 405@end 406