1/******************************************************************************
2 * Copyright (c) 2005-2019 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 "TorrentTableView.h"
24#import "Controller.h"
25#import "FileListNode.h"
26#import "InfoOptionsViewController.h"
27#import "NSApplicationAdditions.h"
28#import "NSStringAdditions.h"
29#import "Torrent.h"
30#import "TorrentCell.h"
31#import "TorrentGroup.h"
32
33#define MAX_GROUP 999999
34
35//eliminate when Lion-only
36#define ACTION_MENU_GLOBAL_TAG 101
37#define ACTION_MENU_UNLIMITED_TAG 102
38#define ACTION_MENU_LIMIT_TAG 103
39
40#define ACTION_MENU_PRIORITY_HIGH_TAG 101
41#define ACTION_MENU_PRIORITY_NORMAL_TAG 102
42#define ACTION_MENU_PRIORITY_LOW_TAG 103
43
44#define TOGGLE_PROGRESS_SECONDS 0.175
45
46@interface TorrentTableView (Private)
47
48- (BOOL) pointInGroupStatusRect: (NSPoint) point;
49
50- (void) setGroupStatusColumns;
51
52@end
53
54@implementation TorrentTableView
55
56- (id) initWithCoder: (NSCoder *) decoder
57{
58    if ((self = [super initWithCoder: decoder]))
59    {
60        fDefaults = [NSUserDefaults standardUserDefaults];
61
62        fTorrentCell = [[TorrentCell alloc] init];
63
64        NSData * groupData = [fDefaults dataForKey: @"CollapsedGroups"];
65        if (groupData)
66            fCollapsedGroups = [[NSUnarchiver unarchiveObjectWithData: groupData] mutableCopy];
67        else
68            fCollapsedGroups = [[NSMutableIndexSet alloc] init];
69
70        fMouseRow = -1;
71        fMouseControlRow = -1;
72        fMouseRevealRow = -1;
73        fMouseActionRow = -1;
74
75        fActionPopoverShown = NO;
76
77        [self setDelegate: self];
78
79        fPiecesBarPercent = [fDefaults boolForKey: @"PiecesBar"] ? 1.0 : 0.0;
80    }
81
82    return self;
83}
84
85- (void) dealloc
86{
87    [[NSNotificationCenter defaultCenter] removeObserver: self];
88}
89
90- (void) awakeFromNib
91{
92    //set group columns to show ratio, needs to be in awakeFromNib to size columns correctly
93    [self setGroupStatusColumns];
94
95    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(setNeedsDisplay) name: @"RefreshTorrentTable" object: nil];
96}
97
98- (BOOL) isGroupCollapsed: (NSInteger) value
99{
100    if (value == -1)
101        value = MAX_GROUP;
102
103    return [fCollapsedGroups containsIndex: value];
104}
105
106- (void) removeCollapsedGroup: (NSInteger) value
107{
108    if (value == -1)
109        value = MAX_GROUP;
110
111    [fCollapsedGroups removeIndex: value];
112}
113
114- (void) removeAllCollapsedGroups
115{
116    [fCollapsedGroups removeAllIndexes];
117}
118
119- (void) saveCollapsedGroups
120{
121    [fDefaults setObject: [NSArchiver archivedDataWithRootObject: fCollapsedGroups] forKey: @"CollapsedGroups"];
122}
123
124- (BOOL) outlineView: (NSOutlineView *) outlineView isGroupItem: (id) item
125{
126    return ![item isKindOfClass: [Torrent class]];
127}
128
129- (CGFloat) outlineView: (NSOutlineView *) outlineView heightOfRowByItem: (id) item
130{
131    return [item isKindOfClass: [Torrent class]] ? [self rowHeight] : GROUP_SEPARATOR_HEIGHT;
132}
133
134- (NSCell *) outlineView: (NSOutlineView *) outlineView dataCellForTableColumn: (NSTableColumn *) tableColumn item: (id) item
135{
136    const BOOL group = ![item isKindOfClass: [Torrent class]];
137    if (!tableColumn)
138        return !group ? fTorrentCell : nil;
139    else
140        return group ? [tableColumn dataCellForRow: [self rowForItem: item]] : nil;
141}
142
143- (void) outlineView: (NSOutlineView *) outlineView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn
144    item: (id) item
145{
146    if ([item isKindOfClass: [Torrent class]])
147    {
148        if (!tableColumn)
149        {
150            [cell setRepresentedObject: item];
151
152            const NSInteger row = [self rowForItem: item];
153            [cell setHover: row == fMouseRow];
154            [cell setControlHover: row == fMouseControlRow];
155            [cell setRevealHover: row == fMouseRevealRow];
156            [cell setActionHover: row == fMouseActionRow];
157        }
158    }
159}
160
161- (NSRect) frameOfCellAtColumn: (NSInteger) column row: (NSInteger) row
162{
163    if (column == -1)
164        return [self rectOfRow: row];
165    else
166    {
167        NSRect rect = [super frameOfCellAtColumn: column row: row];
168
169        //adjust placement for proper vertical alignment
170        if (column == [self columnWithIdentifier: @"Group"])
171            rect.size.height -= 1.0f;
172
173        return rect;
174    }
175}
176
177- (NSString *) outlineView: (NSOutlineView *) outlineView typeSelectStringForTableColumn: (NSTableColumn *) tableColumn item: (id) item
178{
179    if ([item isKindOfClass: [Torrent class]])
180        return [(Torrent *)item name];
181    else
182        return [[self dataSource] outlineView:outlineView objectValueForTableColumn:[self tableColumnWithIdentifier:@"Group"] byItem:item];
183}
184
185- (NSString *) outlineView: (NSOutlineView *) outlineView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect tableColumn: (NSTableColumn *) column item: (id) item mouseLocation: (NSPoint) mouseLocation
186{
187    NSString * ident = [column identifier];
188    if ([ident isEqualToString: @"DL"] || [ident isEqualToString: @"DL Image"])
189        return NSLocalizedString(@"Download speed", "Torrent table -> group row -> tooltip");
190    else if ([ident isEqualToString: @"UL"] || [ident isEqualToString: @"UL Image"])
191        return [fDefaults boolForKey: @"DisplayGroupRowRatio"] ? NSLocalizedString(@"Ratio", "Torrent table -> group row -> tooltip")
192                : NSLocalizedString(@"Upload speed", "Torrent table -> group row -> tooltip");
193    else if (ident)
194    {
195        NSUInteger count = [[item torrents] count];
196        if (count == 1)
197            return NSLocalizedString(@"1 transfer", "Torrent table -> group row -> tooltip");
198        else
199            return [NSString stringWithFormat: NSLocalizedString(@"%@ transfers", "Torrent table -> group row -> tooltip"),
200                    [NSString formattedUInteger: count]];
201    }
202    else
203        return nil;
204}
205
206- (void) updateTrackingAreas
207{
208    [super updateTrackingAreas];
209    [self removeTrackingAreas];
210
211    const NSRange rows = [self rowsInRect: [self visibleRect]];
212    if (rows.length == 0)
213        return;
214
215    NSPoint mouseLocation = [self convertPoint: [[self window] mouseLocationOutsideOfEventStream] fromView: nil];
216    for (NSUInteger row = rows.location; row < NSMaxRange(rows); row++)
217    {
218        if (![[self itemAtRow: row] isKindOfClass: [Torrent class]])
219            continue;
220
221        NSDictionary * userInfo = @{@"Row": @(row)};
222        TorrentCell * cell = (TorrentCell *)[self preparedCellAtColumn: -1 row: row];
223        [cell addTrackingAreasForView: self inRect: [self rectOfRow: row] withUserInfo: userInfo mouseLocation: mouseLocation];
224    }
225}
226
227- (void) removeTrackingAreas
228{
229    fMouseRow = -1;
230    fMouseControlRow = -1;
231    fMouseRevealRow = -1;
232    fMouseActionRow = -1;
233
234    for (NSTrackingArea * area in [self trackingAreas])
235    {
236        if ([area owner] == self && [area userInfo][@"Row"])
237            [self removeTrackingArea: area];
238    }
239}
240
241- (void) setRowHover: (NSInteger) row
242{
243    NSAssert([fDefaults boolForKey: @"SmallView"], @"cannot set a hover row when not in compact view");
244
245    fMouseRow = row;
246    if (row >= 0)
247        [self setNeedsDisplayInRect: [self rectOfRow: row]];
248}
249
250- (void) setControlButtonHover: (NSInteger) row
251{
252    fMouseControlRow = row;
253    if (row >= 0)
254        [self setNeedsDisplayInRect: [self rectOfRow: row]];
255}
256
257- (void) setRevealButtonHover: (NSInteger) row
258{
259    fMouseRevealRow = row;
260    if (row >= 0)
261        [self setNeedsDisplayInRect: [self rectOfRow: row]];
262}
263
264- (void) setActionButtonHover: (NSInteger) row
265{
266    fMouseActionRow = row;
267    if (row >= 0)
268        [self setNeedsDisplayInRect: [self rectOfRow: row]];
269}
270
271- (void) mouseEntered: (NSEvent *) event
272{
273    NSDictionary * dict = (NSDictionary *)[event userData];
274
275    NSNumber * row;
276    if ((row = dict[@"Row"]))
277    {
278        NSInteger rowVal = [row integerValue];
279        NSString * type = dict[@"Type"];
280        if ([type isEqualToString: @"Action"])
281            fMouseActionRow = rowVal;
282        else if ([type isEqualToString: @"Control"])
283            fMouseControlRow = rowVal;
284        else if ([type isEqualToString: @"Reveal"])
285            fMouseRevealRow = rowVal;
286        else
287        {
288            fMouseRow = rowVal;
289            if (![fDefaults boolForKey: @"SmallView"])
290                return;
291        }
292
293        [self setNeedsDisplayInRect: [self rectOfRow: rowVal]];
294    }
295}
296
297- (void) mouseExited: (NSEvent *) event
298{
299    NSDictionary * dict = (NSDictionary *)[event userData];
300
301    NSNumber * row;
302    if ((row = dict[@"Row"]))
303    {
304        NSString * type = dict[@"Type"];
305        if ([type isEqualToString: @"Action"])
306            fMouseActionRow = -1;
307        else if ([type isEqualToString: @"Control"])
308            fMouseControlRow = -1;
309        else if ([type isEqualToString: @"Reveal"])
310            fMouseRevealRow = -1;
311        else
312        {
313            fMouseRow = -1;
314            if (![fDefaults boolForKey: @"SmallView"])
315                return;
316        }
317
318        [self setNeedsDisplayInRect: [self rectOfRow: [row integerValue]]];
319    }
320}
321
322- (void) outlineViewSelectionIsChanging: (NSNotification *) notification
323{
324    #warning elliminate when view-based?
325    //if pushing a button, don't change the selected rows
326    if (fSelectedValues)
327        [self selectValues: fSelectedValues];
328}
329
330- (void) outlineViewItemDidExpand: (NSNotification *) notification
331{
332    NSInteger value = [[notification userInfo][@"NSObject"] groupIndex];
333    if (value < 0)
334        value = MAX_GROUP;
335
336    if ([fCollapsedGroups containsIndex: value])
337    {
338        [fCollapsedGroups removeIndex: value];
339        [[NSNotificationCenter defaultCenter] postNotificationName: @"OutlineExpandCollapse" object: self];
340    }
341}
342
343- (void) outlineViewItemDidCollapse: (NSNotification *) notification
344{
345    NSInteger value = [[notification userInfo][@"NSObject"] groupIndex];
346    if (value < 0)
347        value = MAX_GROUP;
348
349    [fCollapsedGroups addIndex: value];
350    [[NSNotificationCenter defaultCenter] postNotificationName: @"OutlineExpandCollapse" object: self];
351}
352
353- (void) mouseDown: (NSEvent *) event
354{
355    NSPoint point = [self convertPoint: [event locationInWindow] fromView: nil];
356    const NSInteger row = [self rowAtPoint: point];
357
358    //check to toggle group status before anything else
359    if ([self pointInGroupStatusRect: point])
360    {
361        [fDefaults setBool: ![fDefaults boolForKey: @"DisplayGroupRowRatio"] forKey: @"DisplayGroupRowRatio"];
362        [self setGroupStatusColumns];
363
364        return;
365    }
366
367    const BOOL pushed = row != -1 && (fMouseActionRow == row || fMouseRevealRow == row || fMouseControlRow == row);
368
369    //if pushing a button, don't change the selected rows
370    if (pushed)
371        fSelectedValues = [self selectedValues];
372
373    [super mouseDown: event];
374
375    fSelectedValues = nil;
376
377    //avoid weird behavior when showing menu by doing this after mouse down
378    if (row != -1 && fMouseActionRow == row)
379    {
380        #warning maybe make appear on mouse down
381        [self displayTorrentActionPopoverForEvent: event];
382    }
383    else if (!pushed && [event clickCount] == 2) //double click
384    {
385        id item = nil;
386        if (row != -1)
387            item = [self itemAtRow: row];
388
389        if (!item || [item isKindOfClass: [Torrent class]])
390            [fController showInfo: nil];
391        else
392        {
393            if ([self isItemExpanded: item])
394                [self collapseItem: item];
395            else
396                [self expandItem: item];
397        }
398    }
399    else;
400}
401
402- (void) selectValues: (NSArray *) values
403{
404    NSMutableIndexSet * indexSet = [NSMutableIndexSet indexSet];
405
406    for (id item in values)
407    {
408        if ([item isKindOfClass: [Torrent class]])
409        {
410            const NSInteger index = [self rowForItem: item];
411            if (index != -1)
412                [indexSet addIndex: index];
413        }
414        else
415        {
416            const NSInteger group = [item groupIndex];
417            for (NSInteger i = 0; i < [self numberOfRows]; i++)
418            {
419                id tableItem = [self itemAtRow: i];
420                if ([tableItem isKindOfClass: [TorrentGroup class]] && group == [tableItem groupIndex])
421                {
422                    [indexSet addIndex: i];
423                    break;
424                }
425            }
426        }
427    }
428
429    [self selectRowIndexes: indexSet byExtendingSelection: NO];
430}
431
432- (NSArray *) selectedValues
433{
434    NSIndexSet * selectedIndexes = [self selectedRowIndexes];
435    NSMutableArray * values = [NSMutableArray arrayWithCapacity: [selectedIndexes count]];
436
437    for (NSUInteger i = [selectedIndexes firstIndex]; i != NSNotFound; i = [selectedIndexes indexGreaterThanIndex: i])
438        [values addObject: [self itemAtRow: i]];
439
440    return values;
441}
442
443- (NSArray *) selectedTorrents
444{
445    NSIndexSet * selectedIndexes = [self selectedRowIndexes];
446    NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [selectedIndexes count]]; //take a shot at guessing capacity
447
448    for (NSUInteger i = [selectedIndexes firstIndex]; i != NSNotFound; i = [selectedIndexes indexGreaterThanIndex: i])
449    {
450        id item = [self itemAtRow: i];
451        if ([item isKindOfClass: [Torrent class]])
452            [torrents addObject: item];
453        else
454        {
455            NSArray * groupTorrents = [item torrents];
456            [torrents addObjectsFromArray: groupTorrents];
457            if ([self isItemExpanded: item])
458                i +=[groupTorrents count];
459        }
460    }
461
462    return torrents;
463}
464
465- (NSMenu *) menuForEvent: (NSEvent *) event
466{
467    NSInteger row = [self rowAtPoint: [self convertPoint: [event locationInWindow] fromView: nil]];
468    if (row >= 0)
469    {
470        if (![self isRowSelected: row])
471            [self selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection: NO];
472        return fContextRow;
473    }
474    else
475    {
476        [self deselectAll: self];
477        return fContextNoRow;
478    }
479}
480
481//make sure that the pause buttons become orange when holding down the option key
482- (void) flagsChanged: (NSEvent *) event
483{
484    [self display];
485    [super flagsChanged: event];
486}
487
488//option-command-f will focus the filter bar's search field
489- (void) keyDown: (NSEvent *) event
490{
491    const unichar firstChar = [[event charactersIgnoringModifiers] characterAtIndex: 0];
492
493    if (firstChar == 'f' && [event modifierFlags] & NSAlternateKeyMask && [event modifierFlags] & NSCommandKeyMask)
494        [fController focusFilterField];
495    else if (firstChar == ' ')
496        [fController toggleQuickLook: nil];
497    else if ([event keyCode] == 53) //esc key
498        [self deselectAll: nil];
499    else
500        [super keyDown: event];
501}
502
503- (NSRect) iconRectForRow: (NSInteger) row
504{
505    return [fTorrentCell iconRectForBounds: [self rectOfRow: row]];
506}
507
508- (void) paste: (id) sender
509{
510    NSURL * url;
511    if ((url = [NSURL URLFromPasteboard: [NSPasteboard generalPasteboard]]))
512        [fController openURL: [url absoluteString]];
513    else
514    {
515        NSArray * items = [[NSPasteboard generalPasteboard] readObjectsForClasses: @[[NSString class]] options: nil];
516        if (items)
517        {
518            NSDataDetector * detector = [NSDataDetector dataDetectorWithTypes: NSTextCheckingTypeLink error: nil];
519            for (__strong NSString * pbItem in items)
520            {
521                pbItem = [pbItem stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
522                if ([pbItem rangeOfString: @"magnet:" options: (NSAnchoredSearch | NSCaseInsensitiveSearch)].location != NSNotFound)
523                    [fController openURL: pbItem];
524                else
525                {
526                    #warning only accept full text?
527                    for (NSTextCheckingResult * result in [detector matchesInString: pbItem options: 0 range: NSMakeRange(0, [pbItem length])])
528                        [fController openURL: [[result URL] absoluteString]];
529                }
530            }
531        }
532    }
533}
534
535- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
536{
537    SEL action = [menuItem action];
538
539    if (action == @selector(paste:))
540    {
541        if ([[[NSPasteboard generalPasteboard] types] containsObject: NSURLPboardType])
542            return YES;
543
544        NSArray * items = [[NSPasteboard generalPasteboard] readObjectsForClasses: @[[NSString class]] options: nil];
545        if (items)
546        {
547            NSDataDetector * detector = [NSDataDetector dataDetectorWithTypes: NSTextCheckingTypeLink error: nil];
548            for (__strong NSString * pbItem in items)
549            {
550                pbItem = [pbItem stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
551                if (([pbItem rangeOfString: @"magnet:" options: (NSAnchoredSearch | NSCaseInsensitiveSearch)].location != NSNotFound)
552                    || [detector firstMatchInString: pbItem options: 0 range: NSMakeRange(0, [pbItem length])])
553                    return YES;
554            }
555        }
556
557    return NO;
558    }
559
560    return YES;
561}
562
563- (void) toggleControlForTorrent: (Torrent *) torrent
564{
565    if ([torrent isActive])
566        [fController stopTorrents: @[torrent]];
567    else
568    {
569        if ([NSEvent modifierFlags] & NSAlternateKeyMask)
570            [fController resumeTorrentsNoWait: @[torrent]];
571        else if ([torrent waitingToStart])
572            [fController stopTorrents: @[torrent]];
573        else
574            [fController resumeTorrents: @[torrent]];
575    }
576}
577
578- (void) displayTorrentActionPopoverForEvent: (NSEvent *) event
579{
580    const NSInteger row = [self rowAtPoint: [self convertPoint: [event locationInWindow] fromView: nil]];
581    if (row < 0)
582        return;
583
584    const NSRect rect = [fTorrentCell iconRectForBounds: [self rectOfRow: row]];
585
586    if (fActionPopoverShown)
587        return;
588
589    Torrent * torrent = [self itemAtRow: row];
590
591    NSPopover * popover = [[NSPopover alloc] init];
592    [popover setBehavior: NSPopoverBehaviorTransient];
593    InfoOptionsViewController * infoViewController = [[InfoOptionsViewController alloc] init];
594    [popover setContentViewController: infoViewController];
595    [popover setDelegate: self];
596
597    [popover showRelativeToRect: rect ofView: self preferredEdge: NSMaxYEdge];
598    [infoViewController setInfoForTorrents: @[torrent]];
599    [infoViewController updateInfo];
600
601}
602
603//don't show multiple popovers when clicking the gear button repeatedly
604- (void) popoverWillShow: (NSNotification *) notification
605{
606    fActionPopoverShown = YES;
607}
608
609- (void) popoverWillClose: (NSNotification *) notification
610{
611    fActionPopoverShown = NO;
612}
613
614//eliminate when Lion-only, along with all the menu item instance variables
615- (void) menuNeedsUpdate: (NSMenu *) menu
616{
617    //this method seems to be called when it shouldn't be
618    if (!fMenuTorrent || ![menu supermenu])
619        return;
620
621    if (menu == fUploadMenu || menu == fDownloadMenu)
622    {
623        NSMenuItem * item;
624        if ([menu numberOfItems] == 3)
625        {
626            const NSInteger speedLimitActionValue[] = { 0, 5, 10, 20, 30, 40, 50, 75, 100, 150, 200, 250, 500,
627                                                        750, 1000, 1500, 2000, -1 };
628
629            for (NSInteger i = 0; speedLimitActionValue[i] != -1; i++)
630            {
631                item = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: NSLocalizedString(@"%d KB/s",
632                        "Action menu -> upload/download limit"), speedLimitActionValue[i]] action: @selector(setQuickLimit:)
633                        keyEquivalent: @""];
634                [item setTarget: self];
635                [item setRepresentedObject: @(speedLimitActionValue[i])];
636                [menu addItem: item];
637            }
638        }
639
640        const BOOL upload = menu == fUploadMenu;
641        const BOOL limit = [fMenuTorrent usesSpeedLimit: upload];
642
643        item = [menu itemWithTag: ACTION_MENU_LIMIT_TAG];
644        [item setState: limit ? NSOnState : NSOffState];
645        [item setTitle: [NSString stringWithFormat: NSLocalizedString(@"Limit (%d KB/s)",
646                            "torrent action menu -> upload/download limit"), [fMenuTorrent speedLimit: upload]]];
647
648        item = [menu itemWithTag: ACTION_MENU_UNLIMITED_TAG];
649        [item setState: !limit ? NSOnState : NSOffState];
650    }
651    else if (menu == fRatioMenu)
652    {
653        NSMenuItem * item;
654        if ([menu numberOfItems] == 4)
655        {
656            const float ratioLimitActionValue[] = { 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, -1.0 };
657
658            for (NSInteger i = 0; ratioLimitActionValue[i] != -1.0; i++)
659            {
660                item = [[NSMenuItem alloc] initWithTitle: [NSString localizedStringWithFormat: @"%.2f", ratioLimitActionValue[i]]
661                        action: @selector(setQuickRatio:) keyEquivalent: @""];
662                [item setTarget: self];
663                [item setRepresentedObject: @(ratioLimitActionValue[i])];
664                [menu addItem: item];
665            }
666        }
667
668        const tr_ratiolimit mode = [fMenuTorrent ratioSetting];
669
670        item = [menu itemWithTag: ACTION_MENU_LIMIT_TAG];
671        [item setState: mode == TR_RATIOLIMIT_SINGLE ? NSOnState : NSOffState];
672        [item setTitle: [NSString localizedStringWithFormat: NSLocalizedString(@"Stop at Ratio (%.2f)",
673            "torrent action menu -> ratio stop"), [fMenuTorrent ratioLimit]]];
674
675        item = [menu itemWithTag: ACTION_MENU_UNLIMITED_TAG];
676        [item setState: mode == TR_RATIOLIMIT_UNLIMITED ? NSOnState : NSOffState];
677
678        item = [menu itemWithTag: ACTION_MENU_GLOBAL_TAG];
679        [item setState: mode == TR_RATIOLIMIT_GLOBAL ? NSOnState : NSOffState];
680    }
681    else if (menu == fPriorityMenu)
682    {
683        const tr_priority_t priority = [fMenuTorrent priority];
684
685        NSMenuItem * item = [menu itemWithTag: ACTION_MENU_PRIORITY_HIGH_TAG];
686        [item setState: priority == TR_PRI_HIGH ? NSOnState : NSOffState];
687
688        item = [menu itemWithTag: ACTION_MENU_PRIORITY_NORMAL_TAG];
689        [item setState: priority == TR_PRI_NORMAL ? NSOnState : NSOffState];
690
691        item = [menu itemWithTag: ACTION_MENU_PRIORITY_LOW_TAG];
692        [item setState: priority == TR_PRI_LOW ? NSOnState : NSOffState];
693    }
694}
695
696//the following methods might not be needed when Lion-only
697- (void) setQuickLimitMode: (id) sender
698{
699    const BOOL limit = [sender tag] == ACTION_MENU_LIMIT_TAG;
700    [fMenuTorrent setUseSpeedLimit: limit upload: [sender menu] == fUploadMenu];
701
702    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil];
703}
704
705- (void) setQuickLimit: (id) sender
706{
707    const BOOL upload = [sender menu] == fUploadMenu;
708    [fMenuTorrent setUseSpeedLimit: YES upload: upload];
709    [fMenuTorrent setSpeedLimit: [[sender representedObject] intValue] upload: upload];
710
711    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil];
712}
713
714- (void) setGlobalLimit: (id) sender
715{
716    [fMenuTorrent setUseGlobalSpeedLimit: [(NSButton *)sender state] != NSOnState];
717
718    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil];
719}
720
721- (void) setQuickRatioMode: (id) sender
722{
723    tr_ratiolimit mode;
724    switch ([sender tag])
725    {
726        case ACTION_MENU_UNLIMITED_TAG:
727            mode = TR_RATIOLIMIT_UNLIMITED;
728            break;
729        case ACTION_MENU_LIMIT_TAG:
730            mode = TR_RATIOLIMIT_SINGLE;
731            break;
732        case ACTION_MENU_GLOBAL_TAG:
733            mode = TR_RATIOLIMIT_GLOBAL;
734            break;
735        default:
736            return;
737    }
738
739    [fMenuTorrent setRatioSetting: mode];
740
741    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil];
742}
743
744- (void) setQuickRatio: (id) sender
745{
746    [fMenuTorrent setRatioSetting: TR_RATIOLIMIT_SINGLE];
747    [fMenuTorrent setRatioLimit: [[sender representedObject] floatValue]];
748
749    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil];
750}
751
752- (void) setPriority: (id) sender
753{
754    tr_priority_t priority;
755    switch ([sender tag])
756    {
757        case ACTION_MENU_PRIORITY_HIGH_TAG:
758            priority = TR_PRI_HIGH;
759            break;
760        case ACTION_MENU_PRIORITY_NORMAL_TAG:
761            priority = TR_PRI_NORMAL;
762            break;
763        case ACTION_MENU_PRIORITY_LOW_TAG:
764            priority = TR_PRI_LOW;
765            break;
766        default:
767            NSAssert1(NO, @"Unknown priority: %ld", [sender tag]);
768            priority = TR_PRI_NORMAL;
769    }
770
771    [fMenuTorrent setPriority: priority];
772
773    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil];
774}
775
776- (void) togglePiecesBar
777{
778    NSMutableArray * progressMarks = [NSMutableArray arrayWithCapacity: 16];
779    for (NSAnimationProgress i = 0.0625; i <= 1.0; i += 0.0625)
780        [progressMarks addObject: @(i)];
781
782    //this stops a previous animation
783    fPiecesBarAnimation = [[NSAnimation alloc] initWithDuration: TOGGLE_PROGRESS_SECONDS animationCurve: NSAnimationEaseIn];
784    [fPiecesBarAnimation setAnimationBlockingMode: NSAnimationNonblocking];
785    [fPiecesBarAnimation setProgressMarks: progressMarks];
786    [fPiecesBarAnimation setDelegate: self];
787
788    [fPiecesBarAnimation startAnimation];
789}
790
791- (void) animationDidEnd: (NSAnimation *) animation
792{
793    if (animation == fPiecesBarAnimation)
794    {
795        fPiecesBarAnimation = nil;
796    }
797}
798
799- (void) animation: (NSAnimation *) animation didReachProgressMark: (NSAnimationProgress) progress
800{
801    if (animation == fPiecesBarAnimation)
802    {
803        if ([fDefaults boolForKey: @"PiecesBar"])
804            fPiecesBarPercent = progress;
805        else
806            fPiecesBarPercent = 1.0 - progress;
807
808        [self setNeedsDisplay: YES];
809    }
810}
811
812- (CGFloat) piecesBarPercent
813{
814    return fPiecesBarPercent;
815}
816
817- (void) selectAndScrollToRow: (NSInteger) row
818{
819    NSParameterAssert(row >= 0);
820    NSParameterAssert(row < [self numberOfRows]);
821
822    [self selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection: NO];
823
824    const NSRect rowRect = [self rectOfRow: row];
825    const NSRect viewRect = [[self superview] frame];
826
827    NSPoint scrollOrigin = rowRect.origin;
828    scrollOrigin.y += (rowRect.size.height - viewRect.size.height) / 2;
829    if (scrollOrigin.y < 0)
830        scrollOrigin.y = 0;
831
832    [[[self superview] animator] setBoundsOrigin: scrollOrigin];
833}
834
835@end
836
837@implementation TorrentTableView (Private)
838
839- (BOOL) pointInGroupStatusRect: (NSPoint) point
840{
841    NSInteger row = [self rowAtPoint: point];
842    if (row < 0 || [[self itemAtRow: row] isKindOfClass: [Torrent class]])
843        return NO;
844
845    NSString * ident = [[self tableColumns][[self columnAtPoint: point]] identifier];
846    return [ident isEqualToString: @"UL"] || [ident isEqualToString: @"UL Image"]
847            || [ident isEqualToString: @"DL"] || [ident isEqualToString: @"DL Image"];
848}
849
850- (void) setGroupStatusColumns
851{
852    const BOOL ratio = [fDefaults boolForKey: @"DisplayGroupRowRatio"];
853
854    [[self tableColumnWithIdentifier: @"DL"] setHidden: ratio];
855    [[self tableColumnWithIdentifier: @"DL Image"] setHidden: ratio];
856}
857
858@end
859