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