API  1.0.0
CPSlider.j
Go to the documentation of this file.
1 /*
2  * CPSlider.j
3  * AppKit
4  *
5  * Created by Francisco Tolmasky.
6  * Copyright 2009, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 
29 
34 @implementation CPSlider : CPControl
35 {
36  double _minValue;
37  double _maxValue;
38  double _altIncrementValue;
39 
40  BOOL _isVertical;
41 
42  CGSize _dragOffset;
43 }
44 
45 + (CPString)defaultThemeClass
46 {
47  return "slider";
48 }
49 
50 + (CPDictionary)themeAttributes
51 {
52  return @{
53  @"knob-color": [CPNull null],
54  @"knob-size": CGSizeMakeZero(),
55  @"track-width": 0.0,
56  @"track-color": [CPNull null],
57  };
58 }
59 
60 - (id)initWithFrame:(CGRect)aFrame
61 {
62  self = [super initWithFrame:aFrame];
63 
64  if (self)
65  {
66  _minValue = 0.0;
67  _maxValue = 100.0;
68 
69  [self setObjectValue:50.0];
70 
71  [self setContinuous:YES];
72 
73  [self _recalculateIsVertical];
74  }
75 
76  return self;
77 }
78 
79 - (void)setMinValue:(float)aMinimumValue
80 {
81  if (_minValue === aMinimumValue)
82  return;
83 
84  _minValue = aMinimumValue;
85 
86  var doubleValue = [self doubleValue];
87 
88  if (doubleValue < _minValue)
89  [self setDoubleValue:_minValue];
90 
91  // The relative position may have (did) change.
92  [self setNeedsLayout];
93  [self setNeedsDisplay:YES];
94 }
95 
96 - (float)minValue
97 {
98  return _minValue;
99 }
100 
101 - (void)setMaxValue:(float)aMaximumValue
102 {
103  if (_maxValue === aMaximumValue)
104  return;
105 
106  _maxValue = aMaximumValue;
107 
108  var doubleValue = [self doubleValue];
109 
110  if (doubleValue > _maxValue)
111  [self setDoubleValue:_maxValue];
112 
113  // The relative position may have (did) change.
114  [self setNeedsLayout];
115  [self setNeedsDisplay:YES];
116 }
117 
118 - (float)maxValue
119 {
120  return _maxValue;
121 }
122 
123 - (void)setObjectValue:(id)aValue
124 {
125  [super setObjectValue:MIN(MAX(aValue, _minValue), _maxValue)];
126 
127  [self setNeedsLayout];
128  [self setNeedsDisplay:YES];
129 }
130 
131 - (void)setSliderType:(CPSliderType)aSliderType
132 {
133  if (aSliderType === CPCircularSlider)
134  [self setThemeState:CPThemeStateCircular];
135  else
136  [self unsetThemeState:CPThemeStateCircular];
137 }
138 
139 - (CPSliderType)sliderType
140 {
141  return [self hasThemeState:CPThemeStateCircular] ? CPCircularSlider : CPLinearSlider;
142 }
143 
144 - (CGRect)trackRectForBounds:(CGRect)bounds
145 {
146  if ([self hasThemeState:CPThemeStateCircular])
147  {
148  var originalBounds = CGRectCreateCopy(bounds);
149 
150  bounds.size.width = MIN(bounds.size.width, bounds.size.height);
151  bounds.size.height = bounds.size.width;
152 
153  if (bounds.size.width < originalBounds.size.width)
154  bounds.origin.x += (originalBounds.size.width - bounds.size.width) / 2.0;
155  else
156  bounds.origin.y += (originalBounds.size.height - bounds.size.height) / 2.0;
157  }
158  else
159  {
160  var trackWidth = [self currentValueForThemeAttribute:@"track-width"];
161 
162  if (trackWidth <= 0)
163  return CGRectMakeZero();
164 
165  if ([self isVertical])
166  {
167  bounds.origin.x = (CGRectGetWidth(bounds) - trackWidth) / 2.0;
168  bounds.size.width = trackWidth;
169  }
170  else
171  {
172  bounds.origin.y = (CGRectGetHeight(bounds) - trackWidth) / 2.0;
173  bounds.size.height = trackWidth;
174  }
175  }
176 
177  return bounds;
178 }
179 
180 - (CGRect)knobRectForBounds:(CGRect)bounds
181 {
182  var knobSize = [self currentValueForThemeAttribute:@"knob-size"];
183 
184  if (knobSize.width <= 0 || knobSize.height <= 0)
185  return CGRectMakeZero();
186 
187  var knobRect = CGRectMake(0.0, 0.0, knobSize.width, knobSize.height),
188  trackRect = [self trackRectForBounds:bounds];
189 
190  // No track, do our best to approximate a place for this thing.
191  if (!trackRect || CGRectIsEmpty(trackRect))
192  trackRect = bounds;
193 
194  if ([self hasThemeState:CPThemeStateCircular])
195  {
196  var angle = 3 * PI_2 - (1.0 - [self doubleValue] - _minValue) / (_maxValue - _minValue) * PI2,
197  radius = CGRectGetWidth(trackRect) / 2.0 - 8.0;
198 
199  knobRect.origin.x = radius * COS(angle) + CGRectGetMidX(trackRect) - 3.0;
200  knobRect.origin.y = radius * SIN(angle) + CGRectGetMidY(trackRect) - 2.0;
201  }
202  else if ([self isVertical])
203  {
204  knobRect.origin.x = CGRectGetMidX(trackRect) - knobSize.width / 2.0;
205  knobRect.origin.y = ((_maxValue - [self doubleValue]) / (_maxValue - _minValue)) * (CGRectGetHeight(trackRect) - knobSize.height);
206  }
207  else
208  {
209  knobRect.origin.x = (([self doubleValue] - _minValue) / (_maxValue - _minValue)) * (CGRectGetWidth(trackRect) - knobSize.width);
210  knobRect.origin.y = CGRectGetMidY(trackRect) - knobSize.height / 2.0;
211  }
212 
213  return knobRect;
214 }
215 
216 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
217 {
218  if (aName === "track-view")
219  return [self trackRectForBounds:[self bounds]];
220 
221  else if (aName === "knob-view")
222  return [self knobRectForBounds:[self bounds]];
223 
224  return [super rectForEphemeralSubviewNamed:aName];
225 }
226 
227 - (CPView)createEphemeralSubviewNamed:(CPString)aName
228 {
229  if (aName === "track-view" || aName === "knob-view")
230  {
231  var view = [[CPView alloc] init];
232 
233  [view setHitTests:NO];
234 
235  return view;
236  }
237 
238  return [super createEphemeralSubviewNamed:aName];
239 }
240 
241 - (void)setAltIncrementValue:(float)anAltIncrementValue
242 {
243  _altIncrementValue = anAltIncrementValue;
244 }
245 
246 - (float)altIncrementValue
247 {
248  return _altIncrementValue;
249 }
250 
251 - (void)setFrameSize:(CGSize)aSize
252 {
253  [super setFrameSize:aSize];
254  [self _recalculateIsVertical];
255 }
256 
257 - (void)_recalculateIsVertical
258 {
259  // Recalculate isVertical.
260  var bounds = [self bounds],
261  width = CGRectGetWidth(bounds),
262  height = CGRectGetHeight(bounds);
263 
264  _isVertical = width < height ? 1 : (width > height ? 0 : -1);
265 
266  if (_isVertical === 1)
267  [self setThemeState:CPThemeStateVertical];
268  else if (_isVertical === 0)
269  [self unsetThemeState:CPThemeStateVertical];
270 }
271 
272 - (int)isVertical
273 {
274  return _isVertical;
275 }
276 
277 - (void)layoutSubviews
278 {
279  var trackView = [self layoutEphemeralSubviewNamed:@"track-view"
280  positioned:CPWindowBelow
281  relativeToEphemeralSubviewNamed:@"knob-view"];
282 
283  if (trackView)
284  [trackView setBackgroundColor:[self currentValueForThemeAttribute:@"track-color"]];
285 
286  var knobView = [self layoutEphemeralSubviewNamed:@"knob-view"
287  positioned:CPWindowAbove
288  relativeToEphemeralSubviewNamed:@"track-view"];
289 
290  if (knobView)
291  [knobView setBackgroundColor:[self currentValueForThemeAttribute:"knob-color"]];
292 }
293 
294 - (BOOL)tracksMouseOutsideOfFrame
295 {
296  return YES;
297 }
298 
299 - (float)_valueAtPoint:(CGPoint)aPoint
300 {
301  var bounds = [self bounds],
302  knobRect = [self knobRectForBounds:bounds],
303  trackRect = [self trackRectForBounds:bounds];
304 
305  if ([self hasThemeState:CPThemeStateCircular])
306  {
307  var knobWidth = CGRectGetWidth(knobRect);
308 
309  trackRect.origin.x += knobWidth / 2;
310  trackRect.size.width -= knobWidth;
311 
312  var minValue = [self minValue],
313  dx = aPoint.x - CGRectGetMidX(trackRect),
314  dy = aPoint.y - CGRectGetMidY(trackRect);
315 
316  return MAX(0.0, MIN(1.0, 1.0 - (3 * PI_2 - ATAN2(dy, dx)) % PI2 / PI2)) * ([self maxValue] - minValue) + minValue;
317  }
318  else if ([self isVertical])
319  {
320  var knobHeight = CGRectGetHeight(knobRect);
321 
322  trackRect.origin.y += knobHeight / 2;
323  trackRect.size.height -= knobHeight;
324 
325  var minValue = [self minValue];
326 
327  return MAX(0.0, MIN(1.0, (CGRectGetMaxY(trackRect) - aPoint.y) / CGRectGetHeight(trackRect))) * ([self maxValue] - minValue) + minValue;
328  }
329  else
330  {
331  var knobWidth = CGRectGetWidth(knobRect);
332 
333  trackRect.origin.x += knobWidth / 2;
334  trackRect.size.width -= knobWidth;
335 
336  var minValue = [self minValue];
337 
338  return MAX(0.0, MIN(1.0, (aPoint.x - CGRectGetMinX(trackRect)) / CGRectGetWidth(trackRect))) * ([self maxValue] - minValue) + minValue;
339  }
340 }
341 
342 - (BOOL)startTrackingAt:(CGPoint)aPoint
343 {
344  var bounds = [self bounds],
345  knobRect = [self knobRectForBounds:CGRectMakeCopy(bounds)];
346 
347  if (CGRectContainsPoint(knobRect, aPoint))
348  _dragOffset = CGSizeMake(CGRectGetMidX(knobRect) - aPoint.x, CGRectGetMidY(knobRect) - aPoint.y);
349  else
350  {
351  var trackRect = [self trackRectForBounds:bounds];
352 
353  if (trackRect && CGRectContainsPoint(trackRect, aPoint))
354  {
355  _dragOffset = CGSizeMakeZero();
356 
357  [self setObjectValue:[self _valueAtPoint:aPoint]];
358  }
359 
360  else
361  return NO;
362  }
363 
364  [self setHighlighted:YES];
365 
366  [self setNeedsLayout];
367  [self setNeedsDisplay:YES];
368 
369  return YES;
370 }
371 
372 - (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint
373 {
374  [self setObjectValue:[self _valueAtPoint:CGPointMake(aPoint.x + _dragOffset.width, aPoint.y + _dragOffset.height)]];
375 
376  return YES;
377 }
378 
379 - (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp
380 {
381  [self setHighlighted:NO];
382 
383  if ([_target respondsToSelector:@selector(sliderDidFinish:)])
384  [_target sliderDidFinish:self];
385 
386  [self setNeedsLayout];
387  [self setNeedsDisplay:YES];
388 }
389 
390 - (BOOL)isContinuous
391 {
392  return (_sendActionOn & CPLeftMouseDraggedMask) !== 0;
393 }
394 
399 - (void)setContinuous:(BOOL)flag
400 {
401  if (flag)
402  _sendActionOn |= CPLeftMouseDraggedMask;
403  else
404  _sendActionOn &= ~CPLeftMouseDraggedMask;
405 }
406 
407 - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
408 {
409  var count = objects.length,
410  value = [objects[0] valueForKeyPath:aKeyPath];
411 
412  [self setObjectValue:value];
413 
414  while (count-- > 1)
415  if (value !== ([objects[count] valueForKeyPath:aKeyPath]))
416  return [self setFloatValue:1.0];
417 }
418 
419 @end
420 
421 var CPSliderMinValueKey = "CPSliderMinValueKey",
422  CPSliderMaxValueKey = "CPSliderMaxValueKey",
423  CPSliderAltIncrValueKey = "CPSliderAltIncrValueKey";
424 
425 @implementation CPSlider (CPCoding)
426 
427 - (id)initWithCoder:(CPCoder)aCoder
428 {
429  _minValue = [aCoder decodeDoubleForKey:CPSliderMinValueKey];
430  _maxValue = [aCoder decodeDoubleForKey:CPSliderMaxValueKey];
431 
432  self = [super initWithCoder:aCoder];
433 
434  if (self)
435  {
436  _altIncrementValue = [aCoder decodeDoubleForKey:CPSliderAltIncrValueKey];
437 
438  [self _recalculateIsVertical];
439 
440  [self setNeedsLayout];
441  [self setNeedsDisplay:YES];
442  }
443 
444  return self;
445 }
446 
447 - (void)encodeWithCoder:(CPCoder)aCoder
448 {
449  [super encodeWithCoder:aCoder];
450 
451  [aCoder encodeDouble:_minValue forKey:CPSliderMinValueKey];
452  [aCoder encodeDouble:_maxValue forKey:CPSliderMaxValueKey];
453  [aCoder encodeDouble:_altIncrementValue forKey:CPSliderAltIncrValueKey];
454 }
455 
456 @end
457 
458 @implementation CPSlider (Deprecated)
459 
460 - (id)value
461 {
462  CPLog.warn("[CPSlider value] is deprecated, use doubleValue or objectValue instead.");
463 
464  return [self doubleValue];
465 }
466 
467 - (void)setValue:(id)aValue
468 {
469  CPLog.warn("[CPSlider setValue:] is deprecated, use setDoubleValue: or setObjectValue: instead.");
470 
471  [self setObjectValue:aValue];
472 }
473 
474 @end
BOOL setThemeState:(ThemeState aState)
Definition: CPView.j:3255
An object representation of nil.
Definition: CPNull.h:2
CPLinearSlider
Definition: CPSlider.j:27
int width
id initWithFrame:(CGRect aFrame)
Definition: CPControl.j:183
CGRect bounds()
Definition: CPView.j:1326
CGRect knobRectForBounds:(CGRect bounds)
Definition: CPSlider.j:180
A mutable key-value pair collection.
Definition: CPDictionary.h:2
void setObjectValue:(id aValue)
Definition: CPSlider.j:123
CGRect bounds()
Definition: CALayer.j:203
CPView createEphemeralSubviewNamed:(CPString aViewName)
Definition: CPView.j:3397
An immutable string (collection of characters).
Definition: CPString.h:2
CPNull null()
Definition: CPNull.j:51
CPThemeStateCircular
Definition: CPTheme.j:623
void setFloatValue:(float aValue)
Definition: CPControl.j:554
id initWithCoder:(CPCoder aCoder)
Definition: CPControl.j:1092
void setNeedsDisplay:(BOOL aFlag)
Definition: CPView.j:2597
void setObjectValue:(id anObject)
Definition: CPControl.j:534
int length()
Definition: CPString.j:186
void setHighlighted:(BOOL isHighlighted)
Definition: CPControl.j:990
var CPSliderMinValueKey
Definition: CPSlider.j:421
CGRect trackRectForBounds:(CGRect bounds)
Definition: CPSlider.j:144
void setNeedsLayout()
Definition: CPView.j:2748
void setContinuous:(BOOL flag)
Definition: CPSlider.j:399
CPCircularSlider
Definition: CPSlider.j:28
double doubleValue()
Definition: CPControl.j:562
Defines methods for use when archiving & restoring (enc/decoding).
Definition: CPCoder.h:2
CPLeftMouseDraggedMask
BOOL unsetThemeState:(ThemeState aState)
Definition: CPView.j:3268
void setFrameSize:(CGSize aSize)
Definition: CPView.j:1124
var CPSliderAltIncrValueKey
Definition: CPSlider.j:423
void setDoubleValue:(double anObject)
Definition: CPControl.j:571
var CPSliderMaxValueKey
Definition: CPSlider.j:422
void encodeWithCoder:(CPCoder aCoder)
Definition: CPControl.j:1121
CPView layoutEphemeralSubviewNamed:positioned:relativeToEphemeralSubviewNamed:(CPString aViewName, [positioned] CPWindowOrderingMode anOrderingMode, [relativeToEphemeralSubviewNamed] CPString relativeToViewName)
Definition: CPView.j:3407
CGRect rectForEphemeralSubviewNamed:(CPString aViewName)
Definition: CPView.j:3402
Definition: CPView.j:137