1/*****************************************************************************
2 * VLCDefaultValueSliderCell.m: SliderCell subclass for VLCDefaultValueSlider
3 *****************************************************************************
4 * Copyright (C) 2016 VLC authors and VideoLAN
5 * $Id: 4bec1b52b50632e314cbe3d3faa6da20a070a0c4 $
6 *
7 * Authors: Marvin Scholz <epirat07 -at- gmail -dot- com>
8 *
9 * This file uses parts of code from the GNUstep NSSliderCell code:
10 *
11 *   Copyright (C) 1996,1999 Free Software Foundation, Inc.
12 *   Author: Ovidiu Predescu <ovidiu@net-community.com>
13 *   Date: September 1997
14 *   Rewrite: Richard Frith-Macdonald <richard@brainstorm.co.uk>
15 *   Date: 1999
16 *
17 * This program is free software; you can redistribute it and/or modify
18 * it under the terms of the GNU General Public License as published by
19 * the Free Software Foundation; either version 2 of the License, or
20 * (at your option) any later version.
21 *
22 * This program is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 * GNU General Public License for more details.
26 *
27 * You should have received a copy of the GNU General Public License
28 * along with this program; if not, write to the Free Software
29 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
30 *****************************************************************************/
31
32#import "VLCDefaultValueSliderCell.h"
33#import "CompatibilityFixes.h"
34
35@interface VLCDefaultValueSliderCell (){
36    BOOL _isRTL;
37    BOOL _isFlipped;
38    double _defaultValue;
39    double _normalizedDefaultValue;
40    NSColor *_defaultTickMarkColor;
41}
42@end
43
44@implementation VLCDefaultValueSliderCell
45
46#pragma mark -
47#pragma mark Public interface
48
49- (instancetype)init
50{
51    self = [super init];
52    if (self) {
53        [self setupSelf];
54    }
55    return self;
56}
57
58- (instancetype)initWithCoder:(NSCoder *)coder
59{
60    self = [super initWithCoder:coder];
61    if (self) {
62        [self setupSelf];
63    }
64    return self;
65}
66
67- (void)setDefaultValue:(double)value
68{
69    if (value > self.maxValue || value < self.minValue)
70        value = DBL_MAX;
71
72    if (_defaultValue == DBL_MAX && value != DBL_MAX) {
73        _drawTickMarkForDefault = YES;
74        _snapsToDefault = YES;
75    } else if (value == DBL_MAX) {
76        _drawTickMarkForDefault = NO;
77        _snapsToDefault = NO;
78    }
79    _defaultValue = value;
80    _normalizedDefaultValue = (value == DBL_MAX) ? DBL_MAX : [self normalizedValue:_defaultValue];
81    [[self controlView] setNeedsDisplay:YES];
82}
83
84- (double)defaultValue
85{
86    return _defaultValue;
87}
88
89- (void)setDefaultTickMarkColor:(NSColor *)color
90{
91    _defaultTickMarkColor = color;
92    [[self controlView] setNeedsDisplay:YES];
93
94}
95
96- (NSColor *)defaultTickMarkColor
97{
98    return _defaultTickMarkColor;
99}
100
101- (void)drawDefaultTickMarkWithFrame:(NSRect)rect
102{
103    [_defaultTickMarkColor setFill];
104    NSRectFill(rect);
105}
106
107#pragma mark -
108#pragma mark Internal helpers
109
110- (void)setupSelf
111{
112    _defaultValue = DBL_MAX;
113    _normalizedDefaultValue = DBL_MAX;
114    _isRTL = ([self userInterfaceLayoutDirection] == NSUserInterfaceLayoutDirectionRightToLeft);
115    _isFlipped = [[self controlView] isFlipped];
116    _defaultTickMarkColor = [NSColor grayColor];
117}
118
119/*
120 * Calculates the knobRect for a given position
121 * This is later used to draw the default tick mark in the center of
122 * where the knob would be, when it is at the default value.
123 */
124#pragma clang diagnostic push
125#pragma clang diagnostic ignored "-Wpartial-availability"
126
127- (NSRect)knobRectFlipped:(BOOL)flipped forValue:(double)doubleValue
128 {
129     NSRect resultRect;
130     NSRect trackRect = self.trackRect;
131     double val = [self normalizedValue:doubleValue] / 100;
132
133     if (self.isVertical) {
134         resultRect.origin.x = -1;
135         resultRect.origin.y = (NSHeight(trackRect) - self.knobThickness) * val;
136         if (_isRTL)
137             resultRect.origin.y = (NSHeight(trackRect) - self.knobThickness) - resultRect.origin.y;
138     } else {
139         resultRect.origin.x = (NSWidth(trackRect) - self.knobThickness) * val;
140         resultRect.origin.y = -1;
141         if (_isRTL)
142             resultRect.origin.x = (NSWidth(trackRect) - self.knobThickness) - resultRect.origin.x;
143     }
144
145     resultRect.size.height = self.knobThickness;
146     resultRect.size.width = self.knobThickness;
147
148     return [self.controlView backingAlignedRect:resultRect options:NSAlignAllEdgesNearest];
149 }
150
151#pragma mark -
152#pragma mark Overwritten super methods
153
154- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
155{
156    // Do all other drawing
157    [super drawWithFrame:cellFrame inView:controlView];
158
159    // Default tick mark
160    if (_drawTickMarkForDefault && _defaultValue != DBL_MAX) {
161
162        // Calculate rect for default tick mark
163        CGFloat tickThickness = 1.0;
164
165        NSRect tickFrame = [self knobRectFlipped:_isFlipped
166                                        forValue:_defaultValue];
167
168        if ([self isVertical]) {
169            CGFloat mid = NSMidY(tickFrame);
170            tickFrame.origin.x = cellFrame.origin.x;
171            tickFrame.origin.y = mid - tickThickness/2.0;
172            tickFrame.size.width = cellFrame.size.width;
173            tickFrame.size.height = tickThickness;
174        } else {
175            CGFloat mid = NSMidX(tickFrame);
176            // Ugly workaround
177            // Corrects minor alignment issue on non-retina
178            CGFloat scale = [[[self controlView] window] backingScaleFactor];
179            tickFrame.size.height = cellFrame.size.height;
180            tickFrame.origin.y = cellFrame.origin.y;
181            if (scale > 1.0) {
182                tickFrame.origin.x = mid;
183            } else {
184                tickFrame.origin.x = mid - tickThickness;
185                if (OSX_YOSEMITE_AND_HIGHER) {
186                    tickFrame.size.height = cellFrame.size.height - 1;
187                    tickFrame.origin.y = cellFrame.origin.y - 1;
188                }
189            }
190            tickFrame.size.width = tickThickness;
191        }
192
193        NSAlignmentOptions alignOpts = NSAlignMinXOutward | NSAlignMinYOutward |
194                                       NSAlignWidthOutward | NSAlignMaxYOutward;
195        NSRect alignedRect = [[self controlView] backingAlignedRect:tickFrame options:alignOpts];
196
197        // Draw default tick mark
198        [self drawDefaultTickMarkWithFrame:alignedRect];
199    }
200
201    // Redraw knob
202    [super drawKnob];
203}
204#pragma clang diagnostic pop
205
206- (double)normalizedValue:(double)value
207{
208    double min = [self minValue];
209    double max = [self maxValue];
210
211    max -= min;
212    value -= min;
213
214    return (value / max) * 100;
215}
216
217- (BOOL)continueTracking:(NSPoint)lastPoint at:(NSPoint)currentPoint inView:(NSView *)controlView
218{
219    double oldValue = [self normalizedValue:self.doubleValue];
220    BOOL result = [super continueTracking:lastPoint at:currentPoint inView:controlView];
221    double newValue = [self normalizedValue:self.doubleValue];
222
223    // If no change, nothing to do.
224    if (newValue == oldValue)
225        return result;
226
227    // Determine in which direction slider is moving
228    BOOL sliderMovingForward = (oldValue > newValue) ? NO : YES;
229
230    // Claculate snap-threshhold
231    double thresh = 100 * (self.knobThickness/3) / self.trackRect.size.width;
232
233    // Snap to default value
234    if (_snapsToDefault && ABS(newValue - _normalizedDefaultValue) < thresh) {
235        if (sliderMovingForward && newValue > _normalizedDefaultValue) {
236            [self setDoubleValue:_defaultValue];
237        } else if (!sliderMovingForward && newValue < _normalizedDefaultValue) {
238            [self setDoubleValue:_defaultValue];
239        }
240    }
241
242    return result;
243}
244
245@end
246