1/******************************************************************************
2 * Copyright (c) 2006-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 "TorrentCell.h"
24#import "GroupsController.h"
25#import "NSImageAdditions.h"
26#import "NSStringAdditions.h"
27#import "ProgressGradients.h"
28#import "Torrent.h"
29#import "TorrentTableView.h"
30
31#define BAR_HEIGHT 12.0
32
33#define IMAGE_SIZE_REG 32.0
34#define IMAGE_SIZE_MIN 16.0
35#define ERROR_IMAGE_SIZE 20.0
36
37#define NORMAL_BUTTON_WIDTH 14.0
38#define ACTION_BUTTON_WIDTH 16.0
39
40#define PRIORITY_ICON_WIDTH 12.0
41#define PRIORITY_ICON_HEIGHT 12.0
42
43//ends up being larger than font height
44#define HEIGHT_TITLE 16.0
45#define HEIGHT_STATUS 12.0
46
47#define PADDING_HORIZONTAL 5.0
48#define PADDING_BETWEEN_BUTTONS 3.0
49#define PADDING_BETWEEN_IMAGE_AND_TITLE (PADDING_HORIZONTAL + 1.0)
50#define PADDING_BETWEEN_IMAGE_AND_BAR PADDING_HORIZONTAL
51#define PADDING_BETWEEN_TITLE_AND_PRIORITY 6.0
52#define PADDING_ABOVE_TITLE 4.0
53#define PADDING_BETWEEN_TITLE_AND_MIN_STATUS 3.0
54#define PADDING_BETWEEN_TITLE_AND_PROGRESS 1.0
55#define PADDING_BETWEEN_PROGRESS_AND_BAR 2.0
56#define PADDING_BETWEEN_BAR_AND_STATUS 2.0
57#define PADDING_BETWEEN_BAR_AND_EDGE_MIN 3.0
58#define PADDING_EXPANSION_FRAME 2.0
59
60#define PIECES_TOTAL_PERCENT 0.6
61
62#define MAX_PIECES (18*18)
63
64@interface TorrentCell (Private)
65
66- (void) drawBar: (NSRect) barRect;
67- (void) drawRegularBar: (NSRect) barRect;
68- (void) drawPiecesBar: (NSRect) barRect;
69
70- (NSRect) rectForMinimalStatusWithString: (NSAttributedString *) string inBounds: (NSRect) bounds;
71- (NSRect) rectForTitleWithString: (NSAttributedString *) string withRightBound: (CGFloat) rightBound inBounds: (NSRect) bounds;
72- (NSRect) rectForProgressWithStringInBounds: (NSRect) bounds;
73- (NSRect) rectForStatusWithStringInBounds: (NSRect) bounds;
74- (NSRect) barRectRegForBounds: (NSRect) bounds;
75- (NSRect) barRectMinForBounds: (NSRect) bounds;
76
77- (NSRect) controlButtonRectForBounds: (NSRect) bounds;
78- (NSRect) revealButtonRectForBounds: (NSRect) bounds;
79- (NSRect) actionButtonRectForBounds: (NSRect) bounds;
80
81- (NSAttributedString *) attributedTitle;
82- (NSAttributedString *) attributedStatusString: (NSString *) string;
83
84- (NSString *) buttonString;
85- (NSString *) statusString;
86- (NSString *) minimalStatusString;
87
88@end
89
90@implementation TorrentCell
91
92//only called once and the main table is always needed, so don't worry about releasing
93- (id) init
94{
95    if ((self = [super init]))
96    {
97        fDefaults = [NSUserDefaults standardUserDefaults];
98
99        NSMutableParagraphStyle * paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
100        [paragraphStyle setLineBreakMode: NSLineBreakByTruncatingMiddle];
101
102        fTitleAttributes = [[NSMutableDictionary alloc] initWithCapacity: 3];
103        fTitleAttributes[NSFontAttributeName] = [NSFont messageFontOfSize: 12.0];
104        fTitleAttributes[NSParagraphStyleAttributeName] = paragraphStyle;
105
106        fStatusAttributes = [[NSMutableDictionary alloc] initWithCapacity: 3];
107        fStatusAttributes[NSFontAttributeName] = [NSFont messageFontOfSize: 9.0];
108        fStatusAttributes[NSParagraphStyleAttributeName] = paragraphStyle;
109
110
111        fBluePieceColor = [NSColor colorWithCalibratedRed: 0.0 green: 0.4 blue: 0.8 alpha: 1.0];
112        fBarBorderColor = [NSColor colorWithCalibratedWhite: 0.0 alpha: 0.2];
113        fBarMinimalBorderColor = [NSColor colorWithCalibratedWhite: 0.0 alpha: 0.015];
114    }
115    return self;
116}
117
118- (id) copyWithZone: (NSZone *) zone
119{
120    id value = [super copyWithZone: zone];
121    [value setRepresentedObject: [self representedObject]];
122    return value;
123}
124
125- (NSRect) iconRectForBounds: (NSRect) bounds
126{
127    const CGFloat imageSize = [fDefaults boolForKey: @"SmallView"] ? IMAGE_SIZE_MIN : IMAGE_SIZE_REG;
128
129    return NSMakeRect(NSMinX(bounds) + PADDING_HORIZONTAL, ceil(NSMidY(bounds) - imageSize * 0.5),
130                        imageSize, imageSize);
131}
132
133- (NSCellHitResult) hitTestForEvent: (NSEvent *) event inRect: (NSRect) cellFrame ofView: (NSView *) controlView
134{
135    NSPoint point = [controlView convertPoint: [event locationInWindow] fromView: nil];
136
137    if (NSMouseInRect(point, [self controlButtonRectForBounds: cellFrame], [controlView isFlipped])
138        || NSMouseInRect(point, [self revealButtonRectForBounds: cellFrame], [controlView isFlipped]))
139        return NSCellHitContentArea | NSCellHitTrackableArea;
140
141    return NSCellHitContentArea;
142}
143
144+ (BOOL) prefersTrackingUntilMouseUp
145{
146    return YES;
147}
148
149- (BOOL) trackMouse: (NSEvent *) event inRect: (NSRect) cellFrame ofView: (NSView *) controlView untilMouseUp: (BOOL) flag
150{
151    fTracking = YES;
152
153    [self setControlView: controlView];
154
155    NSPoint point = [controlView convertPoint: [event locationInWindow] fromView: nil];
156
157    const NSRect controlRect = [self controlButtonRectForBounds: cellFrame];
158    const BOOL checkControl = NSMouseInRect(point, controlRect, [controlView isFlipped]);
159
160    const NSRect revealRect = [self revealButtonRectForBounds: cellFrame];
161    const BOOL checkReveal = NSMouseInRect(point, revealRect, [controlView isFlipped]);
162
163    [(TorrentTableView *)controlView removeTrackingAreas];
164
165    while ([event type] != NSLeftMouseUp)
166    {
167        point = [controlView convertPoint: [event locationInWindow] fromView: nil];
168
169        if (checkControl)
170        {
171            const BOOL inControlButton = NSMouseInRect(point, controlRect, [controlView isFlipped]);
172            if (fMouseDownControlButton != inControlButton)
173            {
174                fMouseDownControlButton = inControlButton;
175                [controlView setNeedsDisplayInRect: cellFrame];
176            }
177        }
178        else if (checkReveal)
179        {
180            const BOOL inRevealButton = NSMouseInRect(point, revealRect, [controlView isFlipped]);
181            if (fMouseDownRevealButton != inRevealButton)
182            {
183                fMouseDownRevealButton = inRevealButton;
184                [controlView setNeedsDisplayInRect: cellFrame];
185            }
186        }
187        else;
188
189        //send events to where necessary
190        if ([event type] == NSMouseEntered || [event type] == NSMouseExited)
191            [NSApp sendEvent: event];
192        event = [[controlView window] nextEventMatchingMask:
193                    (NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSMouseEnteredMask | NSMouseExitedMask)];
194    }
195
196    fTracking = NO;
197
198    if (fMouseDownControlButton)
199    {
200        fMouseDownControlButton = NO;
201
202        [(TorrentTableView *)controlView toggleControlForTorrent: [self representedObject]];
203    }
204    else if (fMouseDownRevealButton)
205    {
206        fMouseDownRevealButton = NO;
207        [controlView setNeedsDisplayInRect: cellFrame];
208
209        NSString * location = [[self representedObject] dataLocation];
210        if (location)
211        {
212            NSURL * file = [NSURL fileURLWithPath: location];
213            [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: @[file]];
214        }
215    }
216    else;
217
218    [controlView updateTrackingAreas];
219
220    return YES;
221}
222
223- (void) addTrackingAreasForView: (NSView *) controlView inRect: (NSRect) cellFrame withUserInfo: (NSDictionary *) userInfo
224            mouseLocation: (NSPoint) mouseLocation
225{
226    const NSTrackingAreaOptions options = NSTrackingEnabledDuringMouseDrag | NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways;
227
228    //whole row
229    if ([fDefaults boolForKey: @"SmallView"])
230    {
231        NSTrackingAreaOptions rowOptions = options;
232        if (NSMouseInRect(mouseLocation, cellFrame, [controlView isFlipped]))
233        {
234            rowOptions |= NSTrackingAssumeInside;
235            [(TorrentTableView *)controlView setRowHover: [userInfo[@"Row"] integerValue]];
236        }
237
238        NSMutableDictionary * rowInfo = [userInfo mutableCopy];
239        rowInfo[@"Type"] = @"Row";
240        NSTrackingArea * area = [[NSTrackingArea alloc] initWithRect: cellFrame options: rowOptions owner: controlView userInfo: rowInfo];
241        [controlView addTrackingArea: area];
242    }
243
244    //control button
245    NSRect controlButtonRect = [self controlButtonRectForBounds: cellFrame];
246    NSTrackingAreaOptions controlOptions = options;
247    if (NSMouseInRect(mouseLocation, controlButtonRect, [controlView isFlipped]))
248    {
249        controlOptions |= NSTrackingAssumeInside;
250        [(TorrentTableView *)controlView setControlButtonHover: [userInfo[@"Row"] integerValue]];
251    }
252
253    NSMutableDictionary * controlInfo = [userInfo mutableCopy];
254    controlInfo[@"Type"] = @"Control";
255    NSTrackingArea * area = [[NSTrackingArea alloc] initWithRect: controlButtonRect options: controlOptions owner: controlView
256                                userInfo: controlInfo];
257    [controlView addTrackingArea: area];
258
259    //reveal button
260    NSRect revealButtonRect = [self revealButtonRectForBounds: cellFrame];
261    NSTrackingAreaOptions revealOptions = options;
262    if (NSMouseInRect(mouseLocation, revealButtonRect, [controlView isFlipped]))
263    {
264        revealOptions |= NSTrackingAssumeInside;
265        [(TorrentTableView *)controlView setRevealButtonHover: [userInfo[@"Row"] integerValue]];
266    }
267
268    NSMutableDictionary * revealInfo = [userInfo mutableCopy];
269    revealInfo[@"Type"] = @"Reveal";
270    area = [[NSTrackingArea alloc] initWithRect: revealButtonRect options: revealOptions owner: controlView
271                                userInfo: revealInfo];
272    [controlView addTrackingArea: area];
273
274    //action button
275    NSRect actionButtonRect = [self iconRectForBounds: cellFrame]; //use the whole icon
276    NSTrackingAreaOptions actionOptions = options;
277    if (NSMouseInRect(mouseLocation, actionButtonRect, [controlView isFlipped]))
278    {
279        actionOptions |= NSTrackingAssumeInside;
280        [(TorrentTableView *)controlView setActionButtonHover: [userInfo[@"Row"] integerValue]];
281    }
282
283    NSMutableDictionary * actionInfo = [userInfo mutableCopy];
284    actionInfo[@"Type"] = @"Action";
285    area = [[NSTrackingArea alloc] initWithRect: actionButtonRect options: actionOptions owner: controlView userInfo: actionInfo];
286    [controlView addTrackingArea: area];
287}
288
289- (void) setHover: (BOOL) hover
290{
291    fHover = hover;
292}
293
294- (void) setControlHover: (BOOL) hover
295{
296    fHoverControl = hover;
297}
298
299- (void) setRevealHover: (BOOL) hover
300{
301    fHoverReveal = hover;
302}
303
304- (void) setActionHover: (BOOL) hover
305{
306    fHoverAction = hover;
307}
308
309- (void) setActionPushed: (BOOL) pushed
310{
311    fMouseDownActionButton = pushed;
312}
313
314- (void) drawInteriorWithFrame: (NSRect) cellFrame inView: (NSView *) controlView
315{
316    Torrent * torrent = [self representedObject];
317    NSAssert(torrent != nil, @"can't have a TorrentCell without a Torrent");
318
319    const BOOL minimal = [fDefaults boolForKey: @"SmallView"];
320
321    //bar
322    [self drawBar: minimal ? [self barRectMinForBounds: cellFrame] : [self barRectRegForBounds: cellFrame]];
323
324    //group coloring
325    const NSRect iconRect = [self iconRectForBounds: cellFrame];
326
327    const NSInteger groupValue = [torrent groupValue];
328    if (groupValue != -1)
329    {
330        NSRect groupRect = NSInsetRect(iconRect, -1.0, -2.0);
331        if (!minimal)
332        {
333            groupRect.size.height -= 1.0;
334            groupRect.origin.y -= 1.0;
335        }
336        const CGFloat radius = minimal ? 3.0 : 6.0;
337
338        NSColor * groupColor = [[GroupsController groups] colorForIndex: groupValue],
339                * darkGroupColor = [groupColor blendedColorWithFraction: 0.2 ofColor: [NSColor whiteColor]];
340
341        //border
342        NSBezierPath * bp = [NSBezierPath bezierPathWithRoundedRect: groupRect xRadius: radius yRadius: radius];
343        [darkGroupColor set];
344        [bp setLineWidth: 2.0];
345        [bp stroke];
346
347        //inside
348        bp = [NSBezierPath bezierPathWithRoundedRect: groupRect xRadius: radius yRadius: radius];
349        NSGradient * gradient = [[NSGradient alloc] initWithStartingColor: [groupColor blendedColorWithFraction: 0.7
350                                    ofColor: [NSColor whiteColor]] endingColor: darkGroupColor];
351        [gradient drawInBezierPath: bp angle: 90.0];
352    }
353
354    const BOOL error = [torrent isAnyErrorOrWarning];
355
356    //icon
357    if (!minimal || !(!fTracking && fHoverAction)) //don't show in minimal mode when hovered over
358    {
359        NSImage * icon = (minimal && error) ? [NSImage imageNamed: NSImageNameCaution]
360                                            : [torrent icon];
361        [icon drawInRect: iconRect fromRect: NSZeroRect operation: NSCompositeSourceOver fraction: 1.0 respectFlipped: YES hints: nil];
362    }
363
364    //error badge
365    if (error && !minimal)
366    {
367        NSImage * errorImage = [NSImage imageNamed: NSImageNameCaution];
368        const NSRect errorRect = NSMakeRect(NSMaxX(iconRect) - ERROR_IMAGE_SIZE, NSMaxY(iconRect) - ERROR_IMAGE_SIZE, ERROR_IMAGE_SIZE, ERROR_IMAGE_SIZE);
369        [errorImage drawInRect: errorRect fromRect: NSZeroRect operation: NSCompositeSourceOver fraction: 1.0 respectFlipped: YES hints: nil];
370    }
371
372    //text color
373    NSColor * titleColor, * statusColor;
374    if ([self backgroundStyle] == NSBackgroundStyleDark)
375        titleColor = statusColor = [NSColor whiteColor];
376    else
377    {
378        titleColor = [NSColor labelColor];
379        statusColor = [NSColor secondaryLabelColor];
380    }
381
382    fTitleAttributes[NSForegroundColorAttributeName] = titleColor;
383    fStatusAttributes[NSForegroundColorAttributeName] = statusColor;
384
385    //minimal status
386    CGFloat minimalTitleRightBound;
387    if (minimal)
388    {
389        NSAttributedString * minimalString = [self attributedStatusString: [self minimalStatusString]];
390        NSRect minimalStatusRect = [self rectForMinimalStatusWithString: minimalString inBounds: cellFrame];
391
392        if (!fHover)
393            [minimalString drawInRect: minimalStatusRect];
394
395        minimalTitleRightBound = NSMinX(minimalStatusRect);
396    }
397
398    //progress
399    if (!minimal)
400    {
401        NSAttributedString * progressString = [self attributedStatusString: [torrent progressString]];
402        NSRect progressRect = [self rectForProgressWithStringInBounds: cellFrame];
403
404        [progressString drawInRect: progressRect];
405    }
406
407    if (!minimal || fHover)
408    {
409        //control button
410        NSString * controlImageSuffix;
411        if (fMouseDownControlButton)
412            controlImageSuffix = @"On";
413        else if (!fTracking && fHoverControl)
414            controlImageSuffix = @"Hover";
415        else
416            controlImageSuffix = @"Off";
417
418        NSImage * controlImage;
419        if ([torrent isActive])
420            controlImage = [NSImage imageNamed: [@"Pause" stringByAppendingString: controlImageSuffix]];
421        else
422        {
423            if ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)
424                controlImage = [NSImage imageNamed: [@"ResumeNoWait" stringByAppendingString: controlImageSuffix]];
425            else if ([torrent waitingToStart])
426                controlImage = [NSImage imageNamed: [@"Pause" stringByAppendingString: controlImageSuffix]];
427            else
428                controlImage = [NSImage imageNamed: [@"Resume" stringByAppendingString: controlImageSuffix]];
429        }
430
431        const NSRect controlRect = [self controlButtonRectForBounds: cellFrame];
432        [controlImage drawInRect: controlRect fromRect: NSZeroRect operation: NSCompositeSourceOver fraction: 1.0 respectFlipped: YES hints: nil];
433        minimalTitleRightBound = MIN(minimalTitleRightBound, NSMinX(controlRect));
434
435        //reveal button
436        NSString * revealImageString;
437        if (fMouseDownRevealButton)
438            revealImageString = @"RevealOn";
439        else if (!fTracking && fHoverReveal)
440            revealImageString = @"RevealHover";
441        else
442            revealImageString = @"RevealOff";
443
444        NSImage * revealImage = [NSImage imageNamed: revealImageString];
445        [revealImage drawInRect: [self revealButtonRectForBounds: cellFrame] fromRect: NSZeroRect operation: NSCompositeSourceOver fraction: 1.0 respectFlipped: YES hints: nil];
446
447        //action button
448        #warning image should use new gear
449        NSString * actionImageString;
450        if (fMouseDownActionButton)
451            #warning we can get rid of this on 10.7
452            actionImageString = @"ActionOn";
453        else if (!fTracking && fHoverAction)
454            actionImageString = @"ActionHover";
455        else
456            actionImageString = nil;
457
458        if (actionImageString)
459        {
460            NSImage * actionImage = [NSImage imageNamed: actionImageString];
461            [actionImage drawInRect: [self actionButtonRectForBounds: cellFrame] fromRect: NSZeroRect operation: NSCompositeSourceOver fraction: 1.0 respectFlipped: YES hints: nil];
462        }
463    }
464
465    //title
466    NSAttributedString * titleString = [self attributedTitle];
467    NSRect titleRect = [self rectForTitleWithString: titleString withRightBound: minimalTitleRightBound inBounds: cellFrame];
468    [titleString drawInRect: titleRect];
469
470    //priority icon
471    if ([torrent priority] != TR_PRI_NORMAL)
472    {
473        const NSRect priorityRect = NSMakeRect(NSMaxX(titleRect) + PADDING_BETWEEN_TITLE_AND_PRIORITY,
474                                               NSMidY(titleRect) - PRIORITY_ICON_HEIGHT  * 0.5,
475                                               PRIORITY_ICON_WIDTH, PRIORITY_ICON_HEIGHT);
476
477        NSColor * priorityColor = [self backgroundStyle] == NSBackgroundStyleDark ? [NSColor whiteColor] : [NSColor labelColor];
478        NSImage * priorityImage = [[NSImage imageNamed: ([torrent priority] == TR_PRI_HIGH ? @"PriorityHighTemplate" : @"PriorityLowTemplate")] imageWithColor: priorityColor];
479        [priorityImage drawInRect: priorityRect fromRect: NSZeroRect operation: NSCompositeSourceOver fraction: 1.0 respectFlipped: YES hints: nil];
480    }
481
482    //status
483    if (!minimal)
484    {
485        NSAttributedString * statusString = [self attributedStatusString: [self statusString]];
486        [statusString drawInRect: [self rectForStatusWithStringInBounds: cellFrame]];
487    }
488}
489
490- (NSRect) expansionFrameWithFrame: (NSRect) cellFrame inView: (NSView *) view
491{
492    BOOL minimal = [fDefaults boolForKey: @"SmallView"];
493
494    //this code needs to match the code in drawInteriorWithFrame:withView:
495    CGFloat minimalTitleRightBound;
496    if (minimal)
497    {
498        NSAttributedString * minimalString = [self attributedStatusString: [self minimalStatusString]];
499        NSRect minimalStatusRect = [self rectForMinimalStatusWithString: minimalString inBounds: cellFrame];
500
501        minimalTitleRightBound = NSMinX(minimalStatusRect);
502    }
503
504    if (!minimal || fHover)
505    {
506        const NSRect controlRect = [self controlButtonRectForBounds: cellFrame];
507        minimalTitleRightBound = MIN(minimalTitleRightBound, NSMinX(controlRect));
508    }
509
510    NSAttributedString * titleString = [self attributedTitle];
511    NSRect realRect = [self rectForTitleWithString: titleString withRightBound: minimalTitleRightBound inBounds: cellFrame];
512
513    NSAssert([titleString size].width >= NSWidth(realRect), @"Full rect width should not be less than the used title rect width!");
514
515    if ([titleString size].width > NSWidth(realRect)
516        && NSMouseInRect([view convertPoint: [[view window] mouseLocationOutsideOfEventStream] fromView: nil], realRect, [view isFlipped]))
517    {
518        realRect.size.width = [titleString size].width;
519        return NSInsetRect(realRect, -PADDING_EXPANSION_FRAME, -PADDING_EXPANSION_FRAME);
520    }
521
522    return NSZeroRect;
523}
524
525- (void) drawWithExpansionFrame: (NSRect) cellFrame inView: (NSView *)view
526{
527    cellFrame.origin.x += PADDING_EXPANSION_FRAME;
528    cellFrame.origin.y += PADDING_EXPANSION_FRAME;
529
530    fTitleAttributes[NSForegroundColorAttributeName] = [NSColor labelColor];
531    NSAttributedString * titleString = [self attributedTitle];
532    [titleString drawInRect: cellFrame];
533}
534
535@end
536
537@implementation TorrentCell (Private)
538
539- (void) drawBar: (NSRect) barRect
540{
541    const BOOL minimal = [fDefaults boolForKey: @"SmallView"];
542
543    const CGFloat piecesBarPercent = [(TorrentTableView *)[self controlView] piecesBarPercent];
544    if (piecesBarPercent > 0.0)
545    {
546        NSRect piecesBarRect, regularBarRect;
547        NSDivideRect(barRect, &piecesBarRect, &regularBarRect, floor(NSHeight(barRect) * PIECES_TOTAL_PERCENT * piecesBarPercent),
548                    NSMaxYEdge);
549
550        [self drawRegularBar: regularBarRect];
551        [self drawPiecesBar: piecesBarRect];
552    }
553    else
554    {
555        [[self representedObject] setPreviousFinishedPieces: nil];
556
557        [self drawRegularBar: barRect];
558    }
559
560    NSColor * borderColor = minimal ? fBarMinimalBorderColor : fBarBorderColor;
561    [borderColor set];
562    [NSBezierPath strokeRect: NSInsetRect(barRect, 0.5, 0.5)];
563}
564
565- (void) drawRegularBar: (NSRect) barRect
566{
567    Torrent * torrent = [self representedObject];
568
569    NSRect haveRect, missingRect;
570    NSDivideRect(barRect, &haveRect, &missingRect, round([torrent progress] * NSWidth(barRect)), NSMinXEdge);
571
572    if (!NSIsEmptyRect(haveRect))
573    {
574        if ([torrent isActive])
575        {
576            if ([torrent isChecking])
577                [[ProgressGradients progressYellowGradient] drawInRect: haveRect angle: 90];
578            else if ([torrent isSeeding])
579            {
580                NSRect ratioHaveRect, ratioRemainingRect;
581                NSDivideRect(haveRect, &ratioHaveRect, &ratioRemainingRect, round([torrent progressStopRatio] * NSWidth(haveRect)),
582                            NSMinXEdge);
583
584                [[ProgressGradients progressGreenGradient] drawInRect: ratioHaveRect angle: 90];
585                [[ProgressGradients progressLightGreenGradient] drawInRect: ratioRemainingRect angle: 90];
586            }
587            else
588                [[ProgressGradients progressBlueGradient] drawInRect: haveRect angle: 90];
589        }
590        else
591        {
592            if ([torrent waitingToStart])
593            {
594                if ([torrent allDownloaded])
595                    [[ProgressGradients progressDarkGreenGradient] drawInRect: haveRect angle: 90];
596                else
597                    [[ProgressGradients progressDarkBlueGradient] drawInRect: haveRect angle: 90];
598            }
599            else
600                [[ProgressGradients progressGrayGradient] drawInRect: haveRect angle: 90];
601        }
602    }
603
604    if (![torrent allDownloaded])
605    {
606        const CGFloat widthRemaining = round(NSWidth(barRect) * [torrent progressLeft]);
607
608        NSRect wantedRect;
609        NSDivideRect(missingRect, &wantedRect, &missingRect, widthRemaining, NSMinXEdge);
610
611        //not-available section
612        if ([torrent isActive] && ![torrent isChecking] && [torrent availableDesired] < 1.0
613            && [fDefaults boolForKey: @"DisplayProgressBarAvailable"])
614        {
615            NSRect unavailableRect;
616            NSDivideRect(wantedRect, &wantedRect, &unavailableRect, round(NSWidth(wantedRect) * [torrent availableDesired]),
617                        NSMinXEdge);
618
619            [[ProgressGradients progressRedGradient] drawInRect: unavailableRect angle: 90];
620        }
621
622        //remaining section
623        [[ProgressGradients progressWhiteGradient] drawInRect: wantedRect angle: 90];
624    }
625
626    //unwanted section
627    if (!NSIsEmptyRect(missingRect))
628    {
629        if (![torrent isMagnet])
630            [[ProgressGradients progressLightGrayGradient] drawInRect: missingRect angle: 90];
631        else
632            [[ProgressGradients progressRedGradient] drawInRect: missingRect angle: 90];
633    }
634}
635
636- (void) drawPiecesBar: (NSRect) barRect
637{
638    Torrent * torrent = [self representedObject];
639
640    //fill an all-white bar for magnet links
641    if ([torrent isMagnet])
642    {
643        [[NSColor colorWithCalibratedWhite: 1.0 alpha: [fDefaults boolForKey: @"SmallView"] ? 0.25 : 1.0] set];
644        NSRectFillUsingOperation(barRect, NSCompositeSourceOver);
645        return;
646    }
647
648    NSInteger pieceCount = MIN([torrent pieceCount], MAX_PIECES);
649    float * piecesPercent = malloc(pieceCount * sizeof(float));
650    [torrent getAmountFinished: piecesPercent size: pieceCount];
651
652    NSBitmapImageRep * bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes: nil
653                                    pixelsWide: pieceCount pixelsHigh: 1 bitsPerSample: 8 samplesPerPixel: 4 hasAlpha: YES
654                                    isPlanar: NO colorSpaceName: NSCalibratedRGBColorSpace bytesPerRow: 0 bitsPerPixel: 0];
655
656    NSIndexSet * previousFinishedIndexes = [torrent previousFinishedPieces];
657    NSMutableIndexSet * finishedIndexes = [NSMutableIndexSet indexSet];
658
659    for (NSInteger i = 0; i < pieceCount; i++)
660    {
661        NSColor * pieceColor;
662        if (piecesPercent[i] == 1.0f)
663        {
664            if (previousFinishedIndexes && ![previousFinishedIndexes containsIndex: i])
665                pieceColor = [NSColor orangeColor];
666            else
667                pieceColor = fBluePieceColor;
668            [finishedIndexes addIndex: i];
669        }
670        else
671            pieceColor = [[NSColor whiteColor] blendedColorWithFraction: piecesPercent[i] ofColor: fBluePieceColor];
672
673        //it's faster to just set color instead of checking previous color
674        [bitmap setColor: pieceColor atX: i y: 0];
675    }
676
677    free(piecesPercent);
678
679    [torrent setPreviousFinishedPieces: [finishedIndexes count] > 0 ? finishedIndexes : nil]; //don't bother saving if none are complete
680
681    //actually draw image
682    [bitmap drawInRect: barRect fromRect: NSZeroRect operation: NSCompositeSourceOver
683        fraction: ([fDefaults boolForKey: @"SmallView"] ? 0.25 : 1.0) respectFlipped: YES hints: nil];
684
685}
686
687- (NSRect) rectForMinimalStatusWithString: (NSAttributedString *) string inBounds: (NSRect) bounds
688{
689    NSRect result;
690    result.size = [string size];
691
692    result.origin.x = NSMaxX(bounds) - (PADDING_HORIZONTAL + NSWidth(result));
693    result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
694
695    return result;
696}
697
698- (NSRect) rectForTitleWithString: (NSAttributedString *) string withRightBound: (CGFloat) rightBound inBounds: (NSRect) bounds
699{
700    const BOOL minimal = [fDefaults boolForKey: @"SmallView"];
701
702    NSRect result;
703    result.origin.x = NSMinX(bounds) + PADDING_HORIZONTAL
704                        + (minimal ? IMAGE_SIZE_MIN : IMAGE_SIZE_REG) + PADDING_BETWEEN_IMAGE_AND_TITLE;
705    result.size.height = HEIGHT_TITLE;
706
707    if (minimal)
708    {
709        result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
710        result.size.width = rightBound - NSMinX(result) - PADDING_BETWEEN_TITLE_AND_MIN_STATUS;
711    }
712    else
713    {
714        result.origin.y = NSMinY(bounds) + PADDING_ABOVE_TITLE;
715        result.size.width = NSMaxX(bounds) - NSMinX(result) - PADDING_HORIZONTAL;
716    }
717
718    if ([(Torrent *)[self representedObject] priority] != TR_PRI_NORMAL)
719        result.size.width -= PRIORITY_ICON_WIDTH + PADDING_BETWEEN_TITLE_AND_PRIORITY;
720    result.size.width = MIN(NSWidth(result), [string size].width);
721
722    return result;
723}
724
725- (NSRect) rectForProgressWithStringInBounds: (NSRect) bounds
726{
727    NSRect result;
728    result.origin.y = NSMinY(bounds) + PADDING_ABOVE_TITLE + HEIGHT_TITLE + PADDING_BETWEEN_TITLE_AND_PROGRESS;
729    result.origin.x = NSMinX(bounds) + PADDING_HORIZONTAL + IMAGE_SIZE_REG + PADDING_BETWEEN_IMAGE_AND_TITLE;
730
731    result.size.height = HEIGHT_STATUS;
732    result.size.width = NSMaxX(bounds) - NSMinX(result) - PADDING_HORIZONTAL;
733
734    return result;
735}
736
737- (NSRect) rectForStatusWithStringInBounds: (NSRect) bounds
738{
739    NSRect result;
740    result.origin.y = NSMinY(bounds) + PADDING_ABOVE_TITLE + HEIGHT_TITLE + PADDING_BETWEEN_TITLE_AND_PROGRESS + HEIGHT_STATUS
741                        + PADDING_BETWEEN_PROGRESS_AND_BAR + BAR_HEIGHT + PADDING_BETWEEN_BAR_AND_STATUS;
742    result.origin.x = NSMinX(bounds) + PADDING_HORIZONTAL + IMAGE_SIZE_REG + PADDING_BETWEEN_IMAGE_AND_TITLE;
743
744    result.size.height = HEIGHT_STATUS;
745    result.size.width = NSMaxX(bounds) - NSMinX(result) - PADDING_HORIZONTAL;
746
747    return result;
748}
749
750- (NSRect) barRectRegForBounds: (NSRect) bounds
751{
752    NSRect result;
753    result.size.height = BAR_HEIGHT;
754    result.origin.x = NSMinX(bounds) + PADDING_HORIZONTAL + IMAGE_SIZE_REG + PADDING_BETWEEN_IMAGE_AND_BAR;
755    result.origin.y = NSMinY(bounds) + PADDING_ABOVE_TITLE + HEIGHT_TITLE + PADDING_BETWEEN_TITLE_AND_PROGRESS
756                        + HEIGHT_STATUS + PADDING_BETWEEN_PROGRESS_AND_BAR;
757
758    result.size.width = floor(NSMaxX(bounds) - NSMinX(result) - PADDING_HORIZONTAL
759                        - 2.0 * (PADDING_BETWEEN_BUTTONS + NORMAL_BUTTON_WIDTH));
760
761    return result;
762}
763
764- (NSRect) barRectMinForBounds: (NSRect) bounds
765{
766    NSRect result;
767    result.origin.x = NSMinX(bounds) + PADDING_HORIZONTAL + IMAGE_SIZE_MIN + PADDING_BETWEEN_IMAGE_AND_BAR;
768    result.origin.y = NSMinY(bounds) + PADDING_BETWEEN_BAR_AND_EDGE_MIN;
769    result.size.height = NSHeight(bounds) - 2.0 * PADDING_BETWEEN_BAR_AND_EDGE_MIN;
770    result.size.width = NSMaxX(bounds) - NSMinX(result) - PADDING_BETWEEN_BAR_AND_EDGE_MIN;
771
772    return result;
773}
774
775- (NSRect) controlButtonRectForBounds: (NSRect) bounds
776{
777    NSRect result;
778    result.size.height = NORMAL_BUTTON_WIDTH;
779    result.size.width = NORMAL_BUTTON_WIDTH;
780    result.origin.x = NSMaxX(bounds) - (PADDING_HORIZONTAL + NORMAL_BUTTON_WIDTH + PADDING_BETWEEN_BUTTONS + NORMAL_BUTTON_WIDTH);
781
782    if (![fDefaults boolForKey: @"SmallView"])
783        result.origin.y = NSMinY(bounds) + PADDING_ABOVE_TITLE + HEIGHT_TITLE - (NORMAL_BUTTON_WIDTH - BAR_HEIGHT) * 0.5
784                            + PADDING_BETWEEN_TITLE_AND_PROGRESS + HEIGHT_STATUS + PADDING_BETWEEN_PROGRESS_AND_BAR;
785    else
786        result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
787
788    return result;
789}
790
791- (NSRect) revealButtonRectForBounds: (NSRect) bounds
792{
793    NSRect result;
794    result.size.height = NORMAL_BUTTON_WIDTH;
795    result.size.width = NORMAL_BUTTON_WIDTH;
796    result.origin.x = NSMaxX(bounds) - (PADDING_HORIZONTAL + NORMAL_BUTTON_WIDTH);
797
798    if (![fDefaults boolForKey: @"SmallView"])
799        result.origin.y = NSMinY(bounds) + PADDING_ABOVE_TITLE + HEIGHT_TITLE - (NORMAL_BUTTON_WIDTH - BAR_HEIGHT) * 0.5
800                            + PADDING_BETWEEN_TITLE_AND_PROGRESS + HEIGHT_STATUS + PADDING_BETWEEN_PROGRESS_AND_BAR;
801    else
802        result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
803
804    return result;
805}
806
807- (NSRect) actionButtonRectForBounds: (NSRect) bounds
808{
809    const NSRect iconRect = [self iconRectForBounds: bounds];
810
811    //in minimal view the rect will be the icon rect, but avoid the extra defaults lookup with some cheap math
812    return NSMakeRect(NSMidX(iconRect) - ACTION_BUTTON_WIDTH * 0.5, NSMidY(iconRect) - ACTION_BUTTON_WIDTH * 0.5,
813                        ACTION_BUTTON_WIDTH, ACTION_BUTTON_WIDTH);
814}
815
816- (NSAttributedString *) attributedTitle
817{
818    NSString * title = [(Torrent *)[self representedObject] name];
819    return [[NSAttributedString alloc] initWithString: title attributes: fTitleAttributes];
820}
821
822- (NSAttributedString *) attributedStatusString: (NSString *) string
823{
824    return [[NSAttributedString alloc] initWithString: string attributes: fStatusAttributes];
825}
826
827- (NSString *) buttonString
828{
829    if (fMouseDownRevealButton || (!fTracking && fHoverReveal))
830        return NSLocalizedString(@"Show the data file in Finder", "Torrent cell -> button info");
831    else if (fMouseDownControlButton || (!fTracking && fHoverControl))
832    {
833        Torrent * torrent = [self representedObject];
834        if ([torrent isActive])
835            return NSLocalizedString(@"Pause the transfer", "Torrent Table -> tooltip");
836        else
837        {
838            if ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)
839                return NSLocalizedString(@"Resume the transfer right away", "Torrent cell -> button info");
840            else if ([torrent waitingToStart])
841                return NSLocalizedString(@"Stop waiting to start", "Torrent cell -> button info");
842            else
843                return NSLocalizedString(@"Resume the transfer", "Torrent cell -> button info");
844        }
845    }
846    else if (!fTracking && fHoverAction)
847        return NSLocalizedString(@"Change transfer settings", "Torrent Table -> tooltip");
848    else
849        return nil;
850}
851
852- (NSString *) statusString
853{
854    NSString * buttonString;
855    if ((buttonString = [self buttonString]))
856        return buttonString;
857    else
858        return [[self representedObject] statusString];
859}
860
861- (NSString *) minimalStatusString
862{
863    Torrent * torrent = [self representedObject];
864    return [fDefaults boolForKey: @"DisplaySmallStatusRegular"] ? [torrent shortStatusString] : [torrent remainingTimeString];
865}
866
867@end
868