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