![]() |
API 0.9.5
|
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