API 0.9.5
AppKit/CPSlider.j
Go to the documentation of this file.
00001 /*
00002  * CPSlider.j
00003  * AppKit
00004  *
00005  * Created by Francisco Tolmasky.
00006  * Copyright 2009, 280 North, Inc.
00007  *
00008  * This library is free software; you can redistribute it and/or
00009  * modify it under the terms of the GNU Lesser General Public
00010  * License as published by the Free Software Foundation; either
00011  * version 2.1 of the License, or (at your option) any later version.
00012  *
00013  * This library is distributed in the hope that it will be useful,
00014  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00015  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
00016  * Lesser General Public License for more details.
00017  *
00018  * You should have received a copy of the GNU Lesser General Public
00019  * License along with this library; if not, write to the Free Software
00020  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
00021  */
00022 
00023 
00024 
00027 CPLinearSlider      = 0;
00028 CPCircularSlider    = 1;
00029 
00034 @implementation CPSlider : CPControl
00035 {
00036     double          _minValue;
00037     double          _maxValue;
00038     double          _altIncrementValue;
00039 
00040     BOOL            _isVertical;
00041 }
00042 
00043 + (CPString)defaultThemeClass
00044 {
00045     return "slider";
00046 }
00047 
00048 + (id)themeAttributes
00049 {
00050     return [CPDictionary dictionaryWithObjects:[[CPNull null], _CGSizeMakeZero(), 0.0, [CPNull null]]
00051                                        forKeys:[@"knob-color", @"knob-size", @"track-width", @"track-color"]];
00052 }
00053 
00054 - (id)initWithFrame:(CGRect)aFrame
00055 {
00056     self = [super initWithFrame:aFrame];
00057 
00058     if (self)
00059     {
00060         _minValue = 0.0;
00061         _maxValue = 100.0;
00062 
00063         [self setObjectValue:50.0];
00064 
00065         [self setContinuous:YES];
00066 
00067         [self _recalculateIsVertical];
00068     }
00069 
00070     return self;
00071 }
00072 
00073 - (void)setMinValue:(float)aMinimumValue
00074 {
00075     if (_minValue === aMinimumValue)
00076         return;
00077 
00078     _minValue = aMinimumValue;
00079 
00080     var doubleValue = [self doubleValue];
00081 
00082     if (doubleValue < _minValue)
00083         [self setDoubleValue:_minValue];
00084 
00085     // The relative position may have (did) change.
00086     [self setNeedsLayout];
00087     [self setNeedsDisplay:YES];
00088 }
00089 
00090 - (float)minValue
00091 {
00092     return _minValue;
00093 }
00094 
00095 - (void)setMaxValue:(float)aMaximumValue
00096 {
00097     if (_maxValue === aMaximumValue)
00098         return;
00099 
00100     _maxValue = aMaximumValue;
00101 
00102     var doubleValue = [self doubleValue];
00103 
00104     if (doubleValue > _maxValue)
00105         [self setDoubleValue:_maxValue];
00106 
00107     // The relative position may have (did) change.
00108     [self setNeedsLayout];
00109     [self setNeedsDisplay:YES];
00110 }
00111 
00112 - (float)maxValue
00113 {
00114     return _maxValue;
00115 }
00116 
00117 - (void)setObjectValue:(id)aValue
00118 {
00119     [super setObjectValue:MIN(MAX(aValue, _minValue), _maxValue)];
00120 
00121     [self setNeedsLayout];
00122     [self setNeedsDisplay:YES];
00123 }
00124 
00125 - (void)setSliderType:(CPSliderType)aSliderType
00126 {
00127     if (aSliderType === CPCircularSlider)
00128         [self setThemeState:CPThemeStateCircular];
00129     else
00130         [self unsetThemeState:CPThemeStateCircular];
00131 }
00132 
00133 - (CPSliderType)sliderType
00134 {
00135     return [self hasThemeState:CPThemeStateCircular] ? CPCircularSlider : CPLinearSlider;
00136 }
00137 
00138 - (CGRect)trackRectForBounds:(CGRect)bounds
00139 {
00140     if ([self hasThemeState:CPThemeStateCircular])
00141     {
00142         var originalBounds = CGRectCreateCopy(bounds);
00143 
00144         bounds.size.width = MIN(bounds.size.width, bounds.size.height);
00145         bounds.size.height = bounds.size.width;
00146 
00147         if (bounds.size.width < originalBounds.size.width)
00148             bounds.origin.x += (originalBounds.size.width - bounds.size.width) / 2.0;
00149         else
00150             bounds.origin.y += (originalBounds.size.height - bounds.size.height) / 2.0;
00151     }
00152     else
00153     {
00154         var trackWidth = [self currentValueForThemeAttribute:@"track-width"];
00155 
00156         if (trackWidth <= 0)
00157             return _CGRectMakeZero();
00158 
00159         if ([self isVertical])
00160         {
00161             bounds.origin.x = (_CGRectGetWidth(bounds) - trackWidth) / 2.0;
00162             bounds.size.width = trackWidth;
00163         }
00164         else
00165         {
00166             bounds.origin.y = (_CGRectGetHeight(bounds) - trackWidth) / 2.0;
00167             bounds.size.height = trackWidth;
00168         }
00169     }
00170 
00171     return bounds;
00172 }
00173 
00174 - (CGRect)knobRectForBounds:(CGRect)bounds
00175 {
00176     var knobSize = [self currentValueForThemeAttribute:@"knob-size"];
00177 
00178     if (knobSize.width <= 0 || knobSize.height <= 0)
00179         return _CGRectMakeZero();
00180 
00181     var knobRect = _CGRectMake(0.0, 0.0, knobSize.width, knobSize.height),
00182         trackRect = [self trackRectForBounds:bounds];
00183 
00184     // No track, do our best to approximate a place for this thing.
00185     if (!trackRect || _CGRectIsEmpty(trackRect))
00186         trackRect = bounds;
00187 
00188     if ([self hasThemeState:CPThemeStateCircular])
00189     {
00190         var angle = 3 * PI_2 - (1.0 - [self doubleValue] - _minValue) / (_maxValue - _minValue) * PI2,
00191             radius = CGRectGetWidth(trackRect) / 2.0 - 8.0;
00192 
00193         knobRect.origin.x = radius * COS(angle) + CGRectGetMidX(trackRect) - 3.0;
00194         knobRect.origin.y = radius * SIN(angle) + CGRectGetMidY(trackRect) - 2.0;
00195     }
00196     else if ([self isVertical])
00197     {
00198         knobRect.origin.x = _CGRectGetMidX(trackRect) - knobSize.width / 2.0;
00199         knobRect.origin.y = ((_maxValue - [self doubleValue]) / (_maxValue - _minValue)) * (_CGRectGetHeight(trackRect) - knobSize.height);
00200     }
00201     else
00202     {
00203         knobRect.origin.x = (([self doubleValue] - _minValue) / (_maxValue - _minValue)) * (_CGRectGetWidth(trackRect) - knobSize.width);
00204         knobRect.origin.y = _CGRectGetMidY(trackRect) - knobSize.height / 2.0;
00205     }
00206 
00207     return knobRect;
00208 }
00209 
00210 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
00211 {
00212     if (aName === "track-view")
00213         return [self trackRectForBounds:[self bounds]];
00214 
00215     else if (aName === "knob-view")
00216         return [self knobRectForBounds:[self bounds]];
00217 
00218     return [super rectForEphemeralSubviewNamed:aName];
00219 }
00220 
00221 - (CPView)createEphemeralSubviewNamed:(CPString)aName
00222 {
00223     if (aName === "track-view" || aName === "knob-view")
00224     {
00225         var view = [[CPView alloc] init];
00226 
00227         [view setHitTests:NO];
00228 
00229         return view;
00230     }
00231 
00232     return [super createEphemeralSubviewNamed:aName];
00233 }
00234 
00235 - (void)setAltIncrementValue:(float)anAltIncrementValue
00236 {
00237     _altIncrementValue = anAltIncrementValue;
00238 }
00239 
00240 - (float)altIncrementValue
00241 {
00242     return _altIncrementValue;
00243 }
00244 
00245 - (void)setFrameSize:(CGSize)aSize
00246 {
00247     [super setFrameSize:aSize];
00248     [self _recalculateIsVertical];
00249 }
00250 
00251 - (void)_recalculateIsVertical
00252 {
00253     // Recalculate isVertical.
00254     var bounds = [self bounds],
00255         width = _CGRectGetWidth(bounds),
00256         height = _CGRectGetHeight(bounds);
00257 
00258     _isVertical = width < height ? 1 : (width > height ? 0 : -1);
00259 
00260     if (_isVertical === 1)
00261         [self setThemeState:CPThemeStateVertical];
00262     else if (_isVertical === 0)
00263         [self unsetThemeState:CPThemeStateVertical];
00264 }
00265 
00266 - (int)isVertical
00267 {
00268     return _isVertical;
00269 }
00270 
00271 - (void)layoutSubviews
00272 {
00273     var trackView = [self layoutEphemeralSubviewNamed:@"track-view"
00274                                            positioned:CPWindowBelow
00275                       relativeToEphemeralSubviewNamed:@"knob-view"];
00276 
00277     if (trackView)
00278         [trackView setBackgroundColor:[self currentValueForThemeAttribute:@"track-color"]];
00279 
00280     var knobView = [self layoutEphemeralSubviewNamed:@"knob-view"
00281                                           positioned:CPWindowAbove
00282                      relativeToEphemeralSubviewNamed:@"track-view"];
00283 
00284     if (knobView)
00285         [knobView setBackgroundColor:[self currentValueForThemeAttribute:"knob-color"]];
00286 }
00287 
00288 - (BOOL)tracksMouseOutsideOfFrame
00289 {
00290     return YES;
00291 }
00292 
00293 - (float)_valueAtPoint:(CGPoint)aPoint
00294 {
00295     var bounds = [self bounds],
00296         knobRect = [self knobRectForBounds:bounds],
00297         trackRect = [self trackRectForBounds:bounds];
00298 
00299     if ([self hasThemeState:CPThemeStateCircular])
00300     {
00301         var knobWidth = _CGRectGetWidth(knobRect);
00302 
00303         trackRect.origin.x += knobWidth / 2;
00304         trackRect.size.width -= knobWidth;
00305 
00306         var minValue = [self minValue],
00307             dx = aPoint.x - _CGRectGetMidX(trackRect),
00308             dy = aPoint.y - _CGRectGetMidY(trackRect);
00309 
00310         return MAX(0.0, MIN(1.0, 1.0 - (3 * PI_2 - ATAN2(dy, dx)) % PI2 / PI2)) * ([self maxValue] - minValue) + minValue;
00311     }
00312     else if ([self isVertical])
00313     {
00314         var knobHeight = _CGRectGetHeight(knobRect);
00315 
00316         trackRect.origin.y += knobHeight / 2;
00317         trackRect.size.height -= knobHeight;
00318 
00319         var minValue = [self minValue];
00320 
00321         return MAX(0.0, MIN(1.0, (_CGRectGetMaxY(trackRect) - aPoint.y) / _CGRectGetHeight(trackRect))) * ([self maxValue] - minValue) + minValue;
00322     }
00323     else
00324     {
00325         var knobWidth = _CGRectGetWidth(knobRect);
00326 
00327         trackRect.origin.x += knobWidth / 2;
00328         trackRect.size.width -= knobWidth;
00329 
00330         var minValue = [self minValue];
00331 
00332         return MAX(0.0, MIN(1.0, (aPoint.x - _CGRectGetMinX(trackRect)) / _CGRectGetWidth(trackRect))) * ([self maxValue] - minValue) + minValue;
00333     }
00334 }
00335 
00336 - (BOOL)startTrackingAt:(CGPoint)aPoint
00337 {
00338     var bounds = [self bounds],
00339         knobRect = [self knobRectForBounds:_CGRectMakeCopy(bounds)];
00340 
00341     if (_CGRectContainsPoint(knobRect, aPoint))
00342         _dragOffset = _CGSizeMake(_CGRectGetMidX(knobRect) - aPoint.x, _CGRectGetMidY(knobRect) - aPoint.y);
00343     else
00344     {
00345         var trackRect = [self trackRectForBounds:bounds];
00346 
00347         if (trackRect && _CGRectContainsPoint(trackRect, aPoint))
00348         {
00349             _dragOffset = _CGSizeMakeZero();
00350 
00351             [self setObjectValue:[self _valueAtPoint:aPoint]];
00352         }
00353 
00354         else
00355             return NO;
00356     }
00357 
00358     [self setHighlighted:YES];
00359 
00360     [self setNeedsLayout];
00361     [self setNeedsDisplay:YES];
00362 
00363     return YES;
00364 }
00365 
00366 - (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint
00367 {
00368     [self setObjectValue:[self _valueAtPoint:_CGPointMake(aPoint.x + _dragOffset.width, aPoint.y + _dragOffset.height)]];
00369 
00370     return YES;
00371 }
00372 
00373 - (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp
00374 {
00375     [self setHighlighted:NO];
00376 
00377     if ([_target respondsToSelector:@selector(sliderDidFinish:)])
00378         [_target sliderDidFinish:self];
00379 
00380     [self setNeedsLayout];
00381     [self setNeedsDisplay:YES];
00382 }
00383 
00384 - (BOOL)isContinuous
00385 {
00386     return (_sendActionOn & CPLeftMouseDraggedMask) !== 0;
00387 }
00388 
00393 - (void)setContinuous:(BOOL)flag
00394 {
00395     if (flag)
00396         _sendActionOn |= CPLeftMouseDraggedMask;
00397     else
00398         _sendActionOn &= ~CPLeftMouseDraggedMask;
00399 }
00400 
00401 - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
00402 {
00403     var count = objects.length,
00404         value = [objects[0] valueForKeyPath:aKeyPath];
00405 
00406     [self setObjectValue:value];
00407 
00408     while (count-- > 1)
00409         if (value !== ([objects[count] valueForKeyPath:aKeyPath]))
00410             return [self setFloatValue:1.0];
00411 }
00412 
00413 @end
00414 
00415 var CPSliderMinValueKey             = "CPSliderMinValueKey",
00416     CPSliderMaxValueKey             = "CPSliderMaxValueKey",
00417     CPSliderAltIncrValueKey         = "CPSliderAltIncrValueKey";
00418 
00419 @implementation CPSlider (CPCoding)
00420 
00421 - (id)initWithCoder:(CPCoder)aCoder
00422 {
00423     _minValue = [aCoder decodeDoubleForKey:CPSliderMinValueKey];
00424     _maxValue = [aCoder decodeDoubleForKey:CPSliderMaxValueKey];
00425 
00426     self = [super initWithCoder:aCoder];
00427 
00428     if (self)
00429     {
00430         _altIncrementValue = [aCoder decodeDoubleForKey:CPSliderAltIncrValueKey];
00431 
00432         [self _recalculateIsVertical];
00433 
00434         [self setNeedsLayout];
00435         [self setNeedsDisplay:YES];
00436     }
00437 
00438     return self;
00439 }
00440 
00441 - (void)encodeWithCoder:(CPCoder)aCoder
00442 {
00443     [super encodeWithCoder:aCoder];
00444 
00445     [aCoder encodeDouble:_minValue forKey:CPSliderMinValueKey];
00446     [aCoder encodeDouble:_maxValue forKey:CPSliderMaxValueKey];
00447     [aCoder encodeDouble:_altIncrementValue forKey:CPSliderAltIncrValueKey];
00448 }
00449 
00450 @end
00451 
00452 @implementation CPSlider (Deprecated)
00453 
00454 - (id)value
00455 {
00456     CPLog.warn("[CPSlider value] is deprecated, use doubleValue or objectValue instead.");
00457 
00458     return [self doubleValue];
00459 }
00460 
00461 - (void)setValue:(id)aValue
00462 {
00463     CPLog.warn("[CPSlider setValue:] is deprecated, use setDoubleValue: or setObjectValue: instead.");
00464 
00465     [self setObjectValue:aValue];
00466 }
00467 
00468 @end
 All Classes Files Functions Variables Defines