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