transmission/macosx/InfoTrackersViewController.mm

471 lines
14 KiB
Plaintext

/******************************************************************************
* 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 "InfoTrackersViewController.h"
#import "NSApplicationAdditions.h"
#import "Torrent.h"
#import "TrackerCell.h"
#import "TrackerNode.h"
#import "TrackerTableView.h"
#define TRACKER_GROUP_SEPARATOR_HEIGHT 14.0
#define TRACKER_ADD_TAG 0
#define TRACKER_REMOVE_TAG 1
@interface InfoTrackersViewController (Private)
- (void)setupInfo;
- (void)addTrackers;
- (void)removeTrackers;
@end
@implementation InfoTrackersViewController
- (instancetype)init
{
if ((self = [super initWithNibName:@"InfoTrackersView" bundle:nil]))
{
self.title = NSLocalizedString(@"Trackers", "Inspector view -> title");
fTrackerCell = [[TrackerCell alloc] init];
}
return self;
}
- (void)awakeFromNib
{
[fTrackerAddRemoveControl.cell setToolTip:NSLocalizedString(@"Add a tracker", "Inspector view -> tracker buttons")
forSegment:TRACKER_ADD_TAG];
[fTrackerAddRemoveControl.cell setToolTip:NSLocalizedString(@"Remove selected trackers", "Inspector view -> tracker buttons")
forSegment:TRACKER_REMOVE_TAG];
CGFloat const height = [NSUserDefaults.standardUserDefaults floatForKey:@"InspectorContentHeightTracker"];
if (height != 0.0)
{
NSRect viewRect = self.view.frame;
viewRect.size.height = height;
self.view.frame = viewRect;
}
}
- (void)setInfoForTorrents:(NSArray*)torrents
{
//don't check if it's the same in case the metadata changed
fTorrents = torrents;
fSet = NO;
}
- (void)updateInfo
{
if (!fSet)
{
[self setupInfo];
}
if (fTorrents.count == 0)
{
return;
}
//get updated tracker stats
if (fTrackerTable.editedRow == -1)
{
NSArray* oldTrackers = fTrackers;
if (fTorrents.count == 1)
{
fTrackers = ((Torrent*)fTorrents[0]).allTrackerStats;
}
else
{
fTrackers = [[NSMutableArray alloc] init];
for (Torrent* torrent in fTorrents)
{
[fTrackers addObjectsFromArray:torrent.allTrackerStats];
}
}
[fTrackerTable setTrackers:fTrackers];
if (oldTrackers && [fTrackers isEqualToArray:oldTrackers])
{
fTrackerTable.needsDisplay = YES;
}
else
{
[fTrackerTable reloadData];
}
}
else
{
NSAssert1(fTorrents.count == 1, @"Attempting to add tracker with %ld transfers selected", fTorrents.count);
NSIndexSet* addedIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(fTrackers.count - 2, 2)];
NSArray* tierAndTrackerBeingAdded = [fTrackers objectsAtIndexes:addedIndexes];
fTrackers = ((Torrent*)fTorrents[0]).allTrackerStats;
[fTrackers addObjectsFromArray:tierAndTrackerBeingAdded];
[fTrackerTable setTrackers:fTrackers];
NSIndexSet *updateIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, fTrackers.count - 2)],
*columnIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, fTrackerTable.tableColumns.count)];
[fTrackerTable reloadDataForRowIndexes:updateIndexes columnIndexes:columnIndexes];
}
}
- (void)saveViewSize
{
[NSUserDefaults.standardUserDefaults setFloat:NSHeight(self.view.frame) forKey:@"InspectorContentHeightTracker"];
}
- (void)clearView
{
fTrackers = nil;
}
- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView
{
return fTrackers ? fTrackers.count : 0;
}
- (id)tableView:(NSTableView*)tableView objectValueForTableColumn:(NSTableColumn*)column row:(NSInteger)row
{
id item = fTrackers[row];
if ([item isKindOfClass:[NSDictionary class]])
{
NSInteger const tier = [item[@"Tier"] integerValue];
NSString* tierString = tier == -1 ?
NSLocalizedString(@"New Tier", "Inspector -> tracker table") :
[NSString stringWithFormat:NSLocalizedString(@"Tier %d", "Inspector -> tracker table"), tier];
if (fTorrents.count > 1)
{
tierString = [tierString stringByAppendingFormat:@" - %@", item[@"Name"]];
}
return tierString;
}
else
{
return item; //TrackerNode or NSString
}
}
- (NSCell*)tableView:(NSTableView*)tableView dataCellForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row
{
BOOL const tracker = [fTrackers[row] isKindOfClass:[TrackerNode class]];
return tracker ? fTrackerCell : [tableColumn dataCellForRow:row];
}
- (CGFloat)tableView:(NSTableView*)tableView heightOfRow:(NSInteger)row
{
//check for NSDictionay instead of TrackerNode because of display issue when adding a row
if ([fTrackers[row] isKindOfClass:[NSDictionary class]])
{
return TRACKER_GROUP_SEPARATOR_HEIGHT;
}
else
{
return tableView.rowHeight;
}
}
- (BOOL)tableView:(NSTableView*)tableView shouldEditTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row
{
//don't allow tier row to be edited by double-click
return NO;
}
- (void)tableViewSelectionDidChange:(NSNotification*)notification
{
[fTrackerAddRemoveControl setEnabled:fTrackerTable.numberOfSelectedRows > 0 forSegment:TRACKER_REMOVE_TAG];
}
- (BOOL)tableView:(NSTableView*)tableView isGroupRow:(NSInteger)row
{
return ![fTrackers[row] isKindOfClass:[TrackerNode class]] && tableView.editedRow != row;
}
- (NSString*)tableView:(NSTableView*)tableView
toolTipForCell:(NSCell*)cell
rect:(NSRectPointer)rect
tableColumn:(NSTableColumn*)column
row:(NSInteger)row
mouseLocation:(NSPoint)mouseLocation
{
id node = fTrackers[row];
if ([node isKindOfClass:[TrackerNode class]])
{
return ((TrackerNode*)node).fullAnnounceAddress;
}
else
{
return nil;
}
}
- (void)tableView:(NSTableView*)tableView
setObjectValue:(id)object
forTableColumn:(NSTableColumn*)tableColumn
row:(NSInteger)row
{
Torrent* torrent = fTorrents[0];
BOOL added = NO;
for (NSString* tracker in [object componentsSeparatedByString:@"\n"])
{
if ([torrent addTrackerToNewTier:tracker])
{
added = YES;
}
}
if (!added)
{
NSBeep();
}
//reset table with either new or old value
fTrackers = torrent.allTrackerStats;
[fTrackerTable setTrackers:fTrackers];
[fTrackerTable reloadData];
[fTrackerTable deselectAll:self];
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateUI" object:nil]; //incase sort by tracker
}
- (void)addRemoveTracker:(id)sender
{
//don't allow add/remove when currently adding - it leads to weird results
if (fTrackerTable.editedRow != -1)
{
return;
}
[self updateInfo];
if ([[sender cell] tagForSegment:[sender selectedSegment]] == TRACKER_REMOVE_TAG)
{
[self removeTrackers];
}
else
{
[self addTrackers];
}
}
@end
@implementation InfoTrackersViewController (Private)
- (void)setupInfo
{
NSUInteger const numberSelected = fTorrents.count;
if (numberSelected != 1)
{
if (numberSelected == 0)
{
fTrackers = nil;
[fTrackerTable setTrackers:nil];
[fTrackerTable reloadData];
}
[fTrackerTable setTorrent:nil];
[fTrackerAddRemoveControl setEnabled:NO forSegment:TRACKER_ADD_TAG];
[fTrackerAddRemoveControl setEnabled:NO forSegment:TRACKER_REMOVE_TAG];
}
else
{
[fTrackerTable setTorrent:fTorrents[0]];
[fTrackerAddRemoveControl setEnabled:YES forSegment:TRACKER_ADD_TAG];
[fTrackerAddRemoveControl setEnabled:NO forSegment:TRACKER_REMOVE_TAG];
}
[fTrackerTable deselectAll:self];
fSet = YES;
}
#warning doesn't like blank addresses
- (void)addTrackers
{
[self.view.window makeKeyWindow];
NSAssert1(fTorrents.count == 1, @"Attempting to add tracker with %ld transfers selected", fTorrents.count);
[fTrackers addObject:@{ @"Tier" : @-1 }];
[fTrackers addObject:@""];
[fTrackerTable setTrackers:fTrackers];
[fTrackerTable reloadData];
[fTrackerTable selectRowIndexes:[NSIndexSet indexSetWithIndex:fTrackers.count - 1] byExtendingSelection:NO];
[fTrackerTable editColumn:[fTrackerTable columnWithIdentifier:@"Tracker"] row:fTrackers.count - 1 withEvent:nil select:YES];
}
- (void)removeTrackers
{
NSMutableDictionary* removeIdentifiers = [NSMutableDictionary dictionaryWithCapacity:fTorrents.count];
NSUInteger removeTrackerCount = 0;
NSIndexSet* selectedIndexes = fTrackerTable.selectedRowIndexes;
BOOL groupSelected = NO;
NSUInteger groupRowIndex = NSNotFound;
NSMutableIndexSet* removeIndexes = [NSMutableIndexSet indexSet];
for (NSUInteger i = 0; i < fTrackers.count; ++i)
{
id object = fTrackers[i];
if ([object isKindOfClass:[TrackerNode class]])
{
TrackerNode* node = (TrackerNode*)object;
if (groupSelected || [selectedIndexes containsIndex:i])
{
Torrent* torrent = node.torrent;
NSMutableSet* removeSet;
if (!(removeSet = removeIdentifiers[torrent]))
{
removeSet = [NSMutableSet set];
removeIdentifiers[torrent] = removeSet;
}
[removeSet addObject:node.fullAnnounceAddress];
++removeTrackerCount;
[removeIndexes addIndex:i];
}
else
{
groupRowIndex = NSNotFound; //don't remove the group row
}
}
else
{
//mark the previous group row for removal, if necessary
if (groupRowIndex != NSNotFound)
{
[removeIndexes addIndex:groupRowIndex];
}
groupSelected = [selectedIndexes containsIndex:i];
if (!groupSelected && i > selectedIndexes.lastIndex)
{
groupRowIndex = NSNotFound;
break;
}
groupRowIndex = i;
}
}
//mark the last group for removal, too
if (groupRowIndex != NSNotFound)
{
[removeIndexes addIndex:groupRowIndex];
}
NSAssert2(
removeTrackerCount <= removeIndexes.count,
@"Marked %ld trackers to remove, but only removing %ld rows",
removeTrackerCount,
removeIndexes.count);
//we might have no trackers if remove right after a failed add (race condition ftw)
#warning look into having a failed add apply right away, so that this can become an assert
if (removeTrackerCount == 0)
{
return;
}
if ([NSUserDefaults.standardUserDefaults boolForKey:@"WarningRemoveTrackers"])
{
NSAlert* alert = [[NSAlert alloc] init];
if (removeTrackerCount > 1)
{
alert.messageText = [NSString
stringWithFormat:NSLocalizedString(@"Are you sure you want to remove %d trackers?", "Remove trackers alert -> title"),
removeTrackerCount];
alert.informativeText = NSLocalizedString(
@"Once removed, Transmission will no longer attempt to contact them."
" This cannot be undone.",
"Remove trackers alert -> message");
}
else
{
alert.messageText = NSLocalizedString(@"Are you sure you want to remove this tracker?", "Remove trackers alert -> title");
alert.informativeText = NSLocalizedString(
@"Once removed, Transmission will no longer attempt to contact it."
" This cannot be undone.",
"Remove trackers alert -> message");
}
[alert addButtonWithTitle:NSLocalizedString(@"Remove", "Remove trackers alert -> button")];
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", "Remove trackers alert -> button")];
alert.showsSuppressionButton = YES;
NSInteger result = [alert runModal];
if (alert.suppressionButton.state == NSControlStateValueOn)
{
[NSUserDefaults.standardUserDefaults setBool:NO forKey:@"WarningRemoveTrackers"];
}
if (result != NSAlertFirstButtonReturn)
{
return;
}
}
[fTrackerTable beginUpdates];
for (Torrent* torrent in removeIdentifiers)
{
[torrent removeTrackers:removeIdentifiers[torrent]];
}
//reset table with either new or old value
fTrackers = [[NSMutableArray alloc] init];
for (Torrent* torrent in fTorrents)
{
[fTrackers addObjectsFromArray:torrent.allTrackerStats];
}
[fTrackerTable removeRowsAtIndexes:removeIndexes withAnimation:NSTableViewAnimationSlideLeft];
[fTrackerTable setTrackers:fTrackers];
[fTrackerTable endUpdates];
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateUI" object:nil]; //incase sort by tracker
}
@end