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