API  0.9.6
 All Classes Files Functions Variables Macros Groups Pages
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 
43 + (CPString)defaultThemeClass
44 {
45  return "slider";
46 }
47 
48 + (id)themeAttributes
49 {
50  return [CPDictionary dictionaryWithObjects:[[CPNull null], _CGSizeMakeZero(), 0.0, [CPNull null]]
51  forKeys:[@"knob-color", @"knob-size", @"track-width", @"track-color"]];
52 }
53 
54 - (id)initWithFrame:(CGRect)aFrame
55 {
56  self = [super initWithFrame:aFrame];
57 
58  if (self)
59  {
60  _minValue = 0.0;
61  _maxValue = 100.0;
62 
63  [self setObjectValue:50.0];
64 
65  [self setContinuous:YES];
66 
67  [self _recalculateIsVertical];
68  }
69 
70  return self;
71 }
72 
73 - (void)setMinValue:(float)aMinimumValue
74 {
75  if (_minValue === aMinimumValue)
76  return;
77 
78  _minValue = aMinimumValue;
79 
80  var doubleValue = [self doubleValue];
81 
82  if (doubleValue < _minValue)
83  [self setDoubleValue:_minValue];
84 
85  // The relative position may have (did) change.
86  [self setNeedsLayout];
87  [self setNeedsDisplay:YES];
88 }
89 
90 - (float)minValue
91 {
92  return _minValue;
93 }
94 
95 - (void)setMaxValue:(float)aMaximumValue
96 {
97  if (_maxValue === aMaximumValue)
98  return;
99 
100  _maxValue = aMaximumValue;
101 
102  var doubleValue = [self doubleValue];
103 
104  if (doubleValue > _maxValue)
105  [self setDoubleValue:_maxValue];
106 
107  // The relative position may have (did) change.
108  [self setNeedsLayout];
109  [self setNeedsDisplay:YES];
110 }
111 
112 - (float)maxValue
113 {
114  return _maxValue;
115 }
116 
117 - (void)setObjectValue:(id)aValue
118 {
119  [super setObjectValue:MIN(MAX(aValue, _minValue), _maxValue)];
120 
121  [self setNeedsLayout];
122  [self setNeedsDisplay:YES];
123 }
124 
125 - (void)setSliderType:(CPSliderType)aSliderType
126 {
127  if (aSliderType === CPCircularSlider)
128  [self setThemeState:CPThemeStateCircular];
129  else
130  [self unsetThemeState:CPThemeStateCircular];
131 }
132 
133 - (CPSliderType)sliderType
134 {
135  return [self hasThemeState:CPThemeStateCircular] ? CPCircularSlider : CPLinearSlider;
136 }
137 
138 - (CGRect)trackRectForBounds:(CGRect)bounds
139 {
140  if ([self hasThemeState:CPThemeStateCircular])
141  {
142  var originalBounds = CGRectCreateCopy(bounds);
143 
144  bounds.size.width = MIN(bounds.size.width, bounds.size.height);
145  bounds.size.height = bounds.size.width;
146 
147  if (bounds.size.width < originalBounds.size.width)
148  bounds.origin.x += (originalBounds.size.width - bounds.size.width) / 2.0;
149  else
150  bounds.origin.y += (originalBounds.size.height - bounds.size.height) / 2.0;
151  }
152  else
153  {
154  var trackWidth = [self currentValueForThemeAttribute:@"track-width"];
155 
156  if (trackWidth <= 0)
157  return _CGRectMakeZero();
158 
159  if ([self isVertical])
160  {
161  bounds.origin.x = (_CGRectGetWidth(bounds) - trackWidth) / 2.0;
162  bounds.size.width = trackWidth;
163  }
164  else
165  {
166  bounds.origin.y = (_CGRectGetHeight(bounds) - trackWidth) / 2.0;
167  bounds.size.height = trackWidth;
168  }
169  }
170 
171  return bounds;
172 }
173 
174 - (CGRect)knobRectForBounds:(CGRect)bounds
175 {
176  var knobSize = [self currentValueForThemeAttribute:@"knob-size"];
177 
178  if (knobSize.width <= 0 || knobSize.height <= 0)
179  return _CGRectMakeZero();
180 
181  var knobRect = _CGRectMake(0.0, 0.0, knobSize.width, knobSize.height),
182  trackRect = [self trackRectForBounds:bounds];
183 
184  // No track, do our best to approximate a place for this thing.
185  if (!trackRect || _CGRectIsEmpty(trackRect))
186  trackRect = bounds;
187 
188  if ([self hasThemeState:CPThemeStateCircular])
189  {
190  var angle = 3 * PI_2 - (1.0 - [self doubleValue] - _minValue) / (_maxValue - _minValue) * PI2,
191  radius = CGRectGetWidth(trackRect) / 2.0 - 8.0;
192 
193  knobRect.origin.x = radius * COS(angle) + CGRectGetMidX(trackRect) - 3.0;
194  knobRect.origin.y = radius * SIN(angle) + CGRectGetMidY(trackRect) - 2.0;
195  }
196  else if ([self isVertical])
197  {
198  knobRect.origin.x = _CGRectGetMidX(trackRect) - knobSize.width / 2.0;
199  knobRect.origin.y = ((_maxValue - [self doubleValue]) / (_maxValue - _minValue)) * (_CGRectGetHeight(trackRect) - knobSize.height);
200  }
201  else
202  {
203  knobRect.origin.x = (([self doubleValue] - _minValue) / (_maxValue - _minValue)) * (_CGRectGetWidth(trackRect) - knobSize.width);
204  knobRect.origin.y = _CGRectGetMidY(trackRect) - knobSize.height / 2.0;
205  }
206 
207  return knobRect;
208 }
209 
210 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
211 {
212  if (aName === "track-view")
213  return [self trackRectForBounds:[self bounds]];
214 
215  else if (aName === "knob-view")
216  return [self knobRectForBounds:[self bounds]];
217 
218  return [super rectForEphemeralSubviewNamed:aName];
219 }
220 
221 - (CPView)createEphemeralSubviewNamed:(CPString)aName
222 {
223  if (aName === "track-view" || aName === "knob-view")
224  {
225  var view = [[CPView alloc] init];
226 
227  [view setHitTests:NO];
228 
229  return view;
230  }
231 
232  return [super createEphemeralSubviewNamed:aName];
233 }
234 
235 - (void)setAltIncrementValue:(float)anAltIncrementValue
236 {
237  _altIncrementValue = anAltIncrementValue;
238 }
239 
240 - (float)altIncrementValue
241 {
242  return _altIncrementValue;
243 }
244 
245 - (void)setFrameSize:(CGSize)aSize
246 {
247  [super setFrameSize:aSize];
248  [self _recalculateIsVertical];
249 }
250 
251 - (void)_recalculateIsVertical
252 {
253  // Recalculate isVertical.
254  var bounds = [self bounds],
255  width = _CGRectGetWidth(bounds),
256  height = _CGRectGetHeight(bounds);
257 
258  _isVertical = width < height ? 1 : (width > height ? 0 : -1);
259 
260  if (_isVertical === 1)
261  [self setThemeState:CPThemeStateVertical];
262  else if (_isVertical === 0)
263  [self unsetThemeState:CPThemeStateVertical];
264 }
265 
266 - (int)isVertical
267 {
268  return _isVertical;
269 }
270 
271 - (void)layoutSubviews
272 {
273  var trackView = [self layoutEphemeralSubviewNamed:@"track-view"
274  positioned:CPWindowBelow
275  relativeToEphemeralSubviewNamed:@"knob-view"];
276 
277  if (trackView)
278  [trackView setBackgroundColor:[self currentValueForThemeAttribute:@"track-color"]];
279 
280  var knobView = [self layoutEphemeralSubviewNamed:@"knob-view"
281  positioned:CPWindowAbove
282  relativeToEphemeralSubviewNamed:@"track-view"];
283 
284  if (knobView)
285  [knobView setBackgroundColor:[self currentValueForThemeAttribute:"knob-color"]];
286 }
287 
288 - (BOOL)tracksMouseOutsideOfFrame
289 {
290  return YES;
291 }
292 
293 - (float)_valueAtPoint:(CGPoint)aPoint
294 {
295  var bounds = [self bounds],
296  knobRect = [self knobRectForBounds:bounds],
297  trackRect = [self trackRectForBounds:bounds];
298 
299  if ([self hasThemeState:CPThemeStateCircular])
300  {
301  var knobWidth = _CGRectGetWidth(knobRect);
302 
303  trackRect.origin.x += knobWidth / 2;
304  trackRect.size.width -= knobWidth;
305 
306  var minValue = [self minValue],
307  dx = aPoint.x - _CGRectGetMidX(trackRect),
308  dy = aPoint.y - _CGRectGetMidY(trackRect);
309 
310  return MAX(0.0, MIN(1.0, 1.0 - (3 * PI_2 - ATAN2(dy, dx)) % PI2 / PI2)) * ([self maxValue] - minValue) + minValue;
311  }
312  else if ([self isVertical])
313  {
314  var knobHeight = _CGRectGetHeight(knobRect);
315 
316  trackRect.origin.y += knobHeight / 2;
317  trackRect.size.height -= knobHeight;
318 
319  var minValue = [self minValue];
320 
321  return MAX(0.0, MIN(1.0, (_CGRectGetMaxY(trackRect) - aPoint.y) / _CGRectGetHeight(trackRect))) * ([self maxValue] - minValue) + minValue;
322  }
323  else
324  {
325  var knobWidth = _CGRectGetWidth(knobRect);
326 
327  trackRect.origin.x += knobWidth / 2;
328  trackRect.size.width -= knobWidth;
329 
330  var minValue = [self minValue];
331 
332  return MAX(0.0, MIN(1.0, (aPoint.x - _CGRectGetMinX(trackRect)) / _CGRectGetWidth(trackRect))) * ([self maxValue] - minValue) + minValue;
333  }
334 }
335 
336 - (BOOL)startTrackingAt:(CGPoint)aPoint
337 {
338  var bounds = [self bounds],
339  knobRect = [self knobRectForBounds:_CGRectMakeCopy(bounds)];
340 
341  if (_CGRectContainsPoint(knobRect, aPoint))
342  _dragOffset = _CGSizeMake(_CGRectGetMidX(knobRect) - aPoint.x, _CGRectGetMidY(knobRect) - aPoint.y);
343  else
344  {
345  var trackRect = [self trackRectForBounds:bounds];
346 
347  if (trackRect && _CGRectContainsPoint(trackRect, aPoint))
348  {
349  _dragOffset = _CGSizeMakeZero();
350 
351  [self setObjectValue:[self _valueAtPoint:aPoint]];
352  }
353 
354  else
355  return NO;
356  }
357 
358  [self setHighlighted:YES];
359 
360  [self setNeedsLayout];
361  [self setNeedsDisplay:YES];
362 
363  return YES;
364 }
365 
366 - (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint
367 {
368  [self setObjectValue:[self _valueAtPoint:_CGPointMake(aPoint.x + _dragOffset.width, aPoint.y + _dragOffset.height)]];
369 
370  return YES;
371 }
372 
373 - (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp
374 {
375  [self setHighlighted:NO];
376 
377  if ([_target respondsToSelector:@selector(sliderDidFinish:)])
378  [_target sliderDidFinish:self];
379 
380  [self setNeedsLayout];
381  [self setNeedsDisplay:YES];
382 }
383 
384 - (BOOL)isContinuous
385 {
386  return (_sendActionOn & CPLeftMouseDraggedMask) !== 0;
387 }
388 
393 - (void)setContinuous:(BOOL)flag
394 {
395  if (flag)
396  _sendActionOn |= CPLeftMouseDraggedMask;
397  else
398  _sendActionOn &= ~CPLeftMouseDraggedMask;
399 }
400 
401 - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
402 {
403  var count = objects.length,
404  value = [objects[0] valueForKeyPath:aKeyPath];
405 
406  [self setObjectValue:value];
407 
408  while (count-- > 1)
409  if (value !== ([objects[count] valueForKeyPath:aKeyPath]))
410  return [self setFloatValue:1.0];
411 }
412 
413 @end
414 
415 var CPSliderMinValueKey = "CPSliderMinValueKey",
416  CPSliderMaxValueKey = "CPSliderMaxValueKey",
417  CPSliderAltIncrValueKey = "CPSliderAltIncrValueKey";
418 
419 @implementation CPSlider (CPCoding)
420 
421 - (id)initWithCoder:(CPCoder)aCoder
422 {
423  _minValue = [aCoder decodeDoubleForKey:CPSliderMinValueKey];
424  _maxValue = [aCoder decodeDoubleForKey:CPSliderMaxValueKey];
425 
426  self = [super initWithCoder:aCoder];
427 
428  if (self)
429  {
430  _altIncrementValue = [aCoder decodeDoubleForKey:CPSliderAltIncrValueKey];
431 
432  [self _recalculateIsVertical];
433 
434  [self setNeedsLayout];
435  [self setNeedsDisplay:YES];
436  }
437 
438  return self;
439 }
440 
441 - (void)encodeWithCoder:(CPCoder)aCoder
442 {
443  [super encodeWithCoder:aCoder];
444 
445  [aCoder encodeDouble:_minValue forKey:CPSliderMinValueKey];
446  [aCoder encodeDouble:_maxValue forKey:CPSliderMaxValueKey];
447  [aCoder encodeDouble:_altIncrementValue forKey:CPSliderAltIncrValueKey];
448 }
449 
450 @end
451 
452 @implementation CPSlider (Deprecated)
453 
454 - (id)value
455 {
456  CPLog.warn("[CPSlider value] is deprecated, use doubleValue or objectValue instead.");
457 
458  return [self doubleValue];
459 }
460 
461 - (void)setValue:(id)aValue
462 {
463  CPLog.warn("[CPSlider setValue:] is deprecated, use setDoubleValue: or setObjectValue: instead.");
464 
465  [self setObjectValue:aValue];
466 }
467 
468 @end