Files
cappuccino/AppKit/CPSlider.j
T
David Richardson 1a9a42ff54 Replace #pragma mark with // MARK:
Replace objc directive with native Javascript version // MARK: to facilitate elimination of preproccsing pass.
2026-07-21 15:15:53 -06:00

1135 lines
38 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* CPSlider.j
* AppKit
*
* Created by Francisco Tolmasky.
* Copyright 2009, 280 North, Inc.
*
* Adapted by Didier Korthoudt
* Copyright 2018 <didier.korthoudt@uliege.be>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import "CPControl.j"
@import "CPWindow_Constants.j"
/*! SLIDER STATES */
@typedef CPSliderType
CPLinearSlider = 0;
CPCircularSlider = 1;
/*! SLIDER TICK MARK POSITION */
@typedef CPTickMarkPosition
CPTickMarkPositionBelow = 0;
CPTickMarkPositionAbove = 1;
CPTickMarkPositionLeading = CPTickMarkPositionAbove;
CPTickMarkPositionTrailing = CPTickMarkPositionBelow;
/*! Ticked sliders specific states */
CPThemeStateTickedSlider = CPThemeState("ticked-slider");
CPThemeStateAboveLeftTickedSlider = CPThemeState("above-left-ticked-slider");
CPThemeStateBelowRightTickedSlider = CPThemeState("below-right-ticked-slider");
/*! Tick mark affinity distance */
var AFFINITY = 5;
/*!
@ingroup appkit
*/
@implementation CPSlider : CPControl
{
double _minValue;
double _maxValue;
double _altIncrementValue;
CPInteger _isVertical;
BOOL _isCircular;
BOOL _isTicked;
BOOL _isContinuous;
CGSize _dragOffset;
CPSliderType _sliderType @accessors(property=sliderType);
BOOL _allowsTickMarkValuesOnly @accessors(property=allowsTickMarkValuesOnly);
CPTickMarkPosition _tickMarkPosition @accessors(property=tickMarkPosition);
CPInteger _numberOfTickMarks @accessors(property=numberOfTickMarks);
CPMutableArray _cachedTickMarksRects;
CPMutableArray _cachedTickMarksSegments;
CPMutableArray _cachedTickMarksValues;
CPInteger _currentTickMarkSegment;
CPInteger _closestTickMarkIndex;
CPInteger _affinityPoint;
BOOL _canStickOnTickMarks;
BOOL _stickingOnTickMark;
}
+ (CPString)defaultThemeClass
{
return "slider";
}
+ (CPDictionary)themeAttributes
{
return @{
@"left-track-color": [CPNull null],
@"knob-color": [CPNull null],
@"knob-size": CGSizeMakeZero(),
@"knob-offset": 0.0,
@"track-width": 0.0,
@"track-color": [CPNull null],
@"direct-nib2cib-adjustment": NO,
@"tick-mark-size": CGSizeMakeZero(),
@"tick-mark-color": [CPNull null],
@"tick-mark-margin": 0,
@"top-margin": 0,
@"bottom-margin": 0,
@"ib-size": -1
};
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
if (self)
{
_minValue = 0.0;
_maxValue = 100.0;
[self setObjectValue:50.0];
[self setContinuous:YES];
[self setNumberOfTickMarks:0];
[self setSliderType:CPLinearSlider];
[self _refreshCachesAndStates];
}
return self;
}
- (void)setMinValue:(float)aMinimumValue
{
if (_minValue === aMinimumValue)
return;
_minValue = aMinimumValue;
var doubleValue = [self doubleValue];
if (doubleValue < _minValue)
[self setDoubleValue:_minValue];
[self _refreshCachesAndStates];
// The relative position may have (did) change.
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
- (float)minValue
{
return _minValue;
}
- (void)setMaxValue:(float)aMaximumValue
{
if (_maxValue === aMaximumValue)
return;
_maxValue = aMaximumValue;
var doubleValue = [self doubleValue];
if (doubleValue > _maxValue)
[self setDoubleValue:_maxValue];
[self _refreshCachesAndStates];
// The relative position may have (did) change.
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
- (float)maxValue
{
return _maxValue;
}
- (void)setObjectValue:(id)aValue
{
[self willChangeValueForKey:@"objectValue"];
[super setObjectValue:MIN(MAX(aValue, _minValue), _maxValue)];
[self didChangeValueForKey:@"objectValue"];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
- (void)setSliderType:(CPSliderType)aSliderType
{
if (aSliderType === _sliderType)
return;
_sliderType = aSliderType;
[self _refreshCachesAndStates];
}
- (CPSliderType)sliderType
{
return _sliderType;
}
- (CGRect)trackRectForBounds:(CGRect)bounds
{
var trackRect = CGRectCreateCopy(bounds);
if (_isCircular)
{
var tickMarkMargin = [self currentValueForThemeAttribute:@"tick-mark-margin"];
trackRect.size.width = MIN(trackRect.size.width, trackRect.size.height) - tickMarkMargin * 2;
trackRect.size.height = trackRect.size.width;
trackRect.origin.x = tickMarkMargin;
trackRect.origin.y = tickMarkMargin;
}
else
{
var trackWidth = [self currentValueForThemeAttribute:@"track-width"],
tickMarkSize = [self currentValueForThemeAttribute:@"tick-mark-size"],
tickMarkMargin = [self currentValueForThemeAttribute:@"tick-mark-margin"],
topMargin = [self currentValueForThemeAttribute:@"top-margin"],
bottomMargin = [self currentValueForThemeAttribute:@"bottom-margin"];
if (trackWidth <= 0)
return CGRectMakeZero();
if (_isVertical)
{
var usableWidth = trackRect.size.width - topMargin - bottomMargin - (_isTicked ? (tickMarkSize.width + tickMarkMargin) : 0);
trackRect.origin.x = (usableWidth - trackWidth) / 2.0 + topMargin + ((_isTicked && (_tickMarkPosition === CPTickMarkPositionLeading)) ? (tickMarkSize.width + tickMarkMargin) : 0);
trackRect.size.width = trackWidth;
}
else
{
var usableHeight = trackRect.size.height - topMargin - bottomMargin - (_isTicked ? (tickMarkSize.height + tickMarkMargin) : 0);
trackRect.origin.y = (usableHeight - trackWidth) / 2.0 + topMargin + ((_isTicked && (_tickMarkPosition === CPTickMarkPositionAbove)) ? (tickMarkSize.height + tickMarkMargin) : 0);
trackRect.size.height = trackWidth;
}
}
return trackRect;
}
- (CGRect)leftTrackRectForBounds:(CGRect)bounds
{
// Circular and ticked slider do not have left and right parts, so we don't need to deal with them
if (_isCircular || _isTicked)
return CGRectMakeZero();
var trackRect = CGRectCreateCopy(bounds),
trackWidth = [self currentValueForThemeAttribute:@"track-width"],
currentValue = [self doubleValue];
if (_isVertical)
{
trackRect.origin.x = (trackRect.size.width - trackWidth) / 2.0;
trackRect.origin.y = ((_maxValue - currentValue) / (_maxValue - _minValue)) * trackRect.size.height;
trackRect.size.width = trackWidth;
trackRect.size.height = ((currentValue - _minValue) / (_maxValue - _minValue)) * trackRect.size.height;
}
else
{
trackRect.origin.y = (trackRect.size.height - trackWidth) / 2.0;
trackRect.size.width = ((currentValue - _minValue) / (_maxValue - _minValue)) * trackRect.size.width;
trackRect.size.height = trackWidth;
}
return trackRect;
}
- (CGRect)rightTrackRectForBounds:(CGRect)bounds
{
// Circular and ticked slider do not have left and right parts, so we don't need to deal with them
if (_isCircular || _isTicked)
return CGRectMakeZero();
var trackRect = CGRectCreateCopy(bounds),
trackWidth = [self currentValueForThemeAttribute:@"track-width"],
currentValue = [self doubleValue];
if (_isVertical)
{
trackRect.origin.x = (trackRect.size.width - trackWidth) / 2.0;
trackRect.size.width = trackWidth;
trackRect.size.height = ((_maxValue - currentValue) / (_maxValue - _minValue)) * trackRect.size.height;
}
else
{
trackRect.origin.x = ((currentValue - _minValue) / (_maxValue - _minValue)) * trackRect.size.width;
trackRect.origin.y = (trackRect.size.height - trackWidth) / 2.0;
trackRect.size.width = ((_maxValue - currentValue) / (_maxValue - _minValue)) * trackRect.size.width;
trackRect.size.height = trackWidth;
}
return trackRect;
}
- (CGRect)knobRectForBounds:(CGRect)bounds
{
var knobSize = [self currentValueForThemeAttribute:@"knob-size"];
if (knobSize.width <= 0 || knobSize.height <= 0)
return CGRectMakeZero();
var knobRect = CGRectMake(0.0, 0.0, knobSize.width, knobSize.height),
trackRect = [self trackRectForBounds:bounds],
knobOffset = [self currentValueForThemeAttribute:@"knob-offset"];
// No track, do our best to approximate a place for this thing.
if (!trackRect || CGRectIsEmpty(trackRect))
trackRect = bounds;
if (_allowsTickMarkValuesOnly)
{
[self closestTickMarkValueToValue:[self doubleValue]]; // we only need the side effect …
_currentTickMarkSegment = _closestTickMarkIndex;
}
if (_isCircular)
{
var angle = 3 * PI_2 - (1.0 - [self doubleValue] - _minValue) / (_maxValue - _minValue) * PI2,
radius = trackRect.size.width / 2.0 - (knobOffset ? knobOffset : 8.0);
knobRect.origin.x = radius * COS(angle) + CGRectGetMidX(trackRect) - knobSize.width / 2; // 3.0;
knobRect.origin.y = radius * SIN(angle) + CGRectGetMidY(trackRect) - knobSize.height / 2; // 2.0;
}
else if (_isVertical)
{
knobRect.origin.x = CGRectGetMidX(trackRect) - knobSize.width / 2.0 + knobOffset;
if (_isTicked && _allowsTickMarkValuesOnly && _cachedTickMarksRects)
// This is done to garantee that the knob is perfectly aligned with the tick mark
knobRect.origin.y = _cachedTickMarksRects[_currentTickMarkSegment].origin.y - (knobSize.height - 1) / 2;
else
knobRect.origin.y = ROUND(((_maxValue - [self doubleValue]) / (_maxValue - _minValue)) * (trackRect.size.height - knobSize.height));
}
else
{
if (_isTicked && _allowsTickMarkValuesOnly && _cachedTickMarksRects)
// This is done to garantee that the knob is perfectly aligned with the tick mark
knobRect.origin.x = _cachedTickMarksRects[_currentTickMarkSegment].origin.x - (knobSize.width - 1) / 2;
else
knobRect.origin.x = ROUND((([self doubleValue] - _minValue) / (_maxValue - _minValue)) * (trackRect.size.width - knobSize.width));
knobRect.origin.y = CGRectGetMidY(trackRect) - knobSize.height / 2.0 + knobOffset;
}
return knobRect;
}
- (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
{
switch (aName)
{
case @"track-view":
return [self trackRectForBounds:[self bounds]];
case @"knob-view":
return [self knobRectForBounds:[self bounds]];
case @"left-track-view":
return [self leftTrackRectForBounds:[self bounds]];
case @"right-track-view":
return [self rightTrackRectForBounds:[self bounds]];
default:
return [super rectForEphemeralSubviewNamed:aName];
}
}
- (CPView)createEphemeralSubviewNamed:(CPString)aName
{
if (aName === "track-view" || aName === "knob-view" || aName === "left-track-view" || aName === "right-track-view")
{
var view = [[CPView alloc] init];
[view setHitTests:NO];
return view;
}
return [super createEphemeralSubviewNamed:aName];
}
- (void)setAltIncrementValue:(float)anAltIncrementValue
{
// FIXME: This is not used. BTW, tested in Cocoa and it is totally useless. So leave it like that for now...
_altIncrementValue = anAltIncrementValue;
}
- (float)altIncrementValue
{
return _altIncrementValue;
}
- (CPInteger)isVertical
{
return _isVertical;
}
- (void)layoutSubviews
{
var leftTrackColor = [self currentValueForThemeAttribute:@"left-track-color"];
if (leftTrackColor && !_isCircular && !_isTicked)
{
// Two parts layout (needed by Aristo3)
var leftTrackView = [self layoutEphemeralSubviewNamed:@"left-track-view"
positioned:CPWindowBelow
relativeToEphemeralSubviewNamed:@"knob-view"],
rightTrackView = [self layoutEphemeralSubviewNamed:@"right-track-view"
positioned:CPWindowBelow
relativeToEphemeralSubviewNamed:@"knob-view"];
[leftTrackView setBackgroundColor:leftTrackColor];
[rightTrackView setBackgroundColor:[self currentValueForThemeAttribute:@"track-color"]];
var knobView = [self layoutEphemeralSubviewNamed:@"knob-view"
positioned:CPWindowAbove
relativeToEphemeralSubviewNamed:@"left-track-view"];
[knobView setBackgroundColor:[self currentValueForThemeAttribute:"knob-color"]];
}
else
{
// Normal layout
var trackView = [self layoutEphemeralSubviewNamed:@"track-view"
positioned:CPWindowBelow
relativeToEphemeralSubviewNamed:@"knob-view"];
[trackView setBackgroundColor:[self currentValueForThemeAttribute:@"track-color"]];
var knobView = [self layoutEphemeralSubviewNamed:@"knob-view"
positioned:CPWindowAbove
relativeToEphemeralSubviewNamed:@"track-view"];
[knobView setBackgroundColor:[self currentValueForThemeAttribute:"knob-color"]];
}
}
- (void)drawRect:(CGRect)aRect
{
// We only draw tick marks here
if (!_isTicked)
return;
var context = [[CPGraphicsContext currentContext] graphicsPort];
CGContextSetFillColor(context, [self currentValueForThemeAttribute:@"tick-mark-color"]);
for (var i = 0; i < _numberOfTickMarks; i++)
CGContextFillRect(context, [self rectOfTickMarkAtIndex:i]);
}
- (BOOL)tracksMouseOutsideOfFrame
{
return YES;
}
- (BOOL)startTrackingAt:(CGPoint)aPoint
{
var bounds = [self bounds],
knobRect = [self knobRectForBounds:bounds];
if (CGRectContainsPoint(knobRect, aPoint))
_dragOffset = CGSizeMake(CGRectGetMidX(knobRect) - aPoint.x, CGRectGetMidY(knobRect) - aPoint.y);
else
{
_dragOffset = CGSizeMakeZero();
if (_allowsTickMarkValuesOnly)
{
_currentTickMarkSegment = [self indexOfTickMarkAtPoint:aPoint];
[self setObjectValue:_cachedTickMarksValues[_currentTickMarkSegment]];
}
else
[self setObjectValue:[self _valueAtPoint:aPoint]];
}
_stickingOnTickMark = NO;
[self setHighlighted:YES];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
return YES;
}
- (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint
{
var bounds = [self bounds],
newPoint = _isCircular ? CGPointMake(aPoint.x + _dragOffset.width, aPoint.y + _dragOffset.height)
: CGPointMake(MAX(0, MIN(bounds.size.width-1, aPoint.x + _dragOffset.width)),
MAX(0, MIN(bounds.size.height-1, aPoint.y + _dragOffset.height)));
if (_allowsTickMarkValuesOnly)
{
var tickMarkSegment = [self indexOfTickMarkAtPoint:newPoint];
if (tickMarkSegment !== _currentTickMarkSegment)
{
_currentTickMarkSegment = tickMarkSegment;
[self setObjectValue:_cachedTickMarksValues[_currentTickMarkSegment]];
}
}
else if (_canStickOnTickMarks)
{
// Ticked but not constrained to tick marks
// We're using a tick affinity with a latency of AFFINITY pixels
// Search the nearest tick mark
var tickMarkSegment = [self indexOfTickMarkAtPoint:newPoint],
tickRect = _cachedTickMarksRects[tickMarkSegment];
// Does the knob point to it ?
if ((!_isVertical && (newPoint.x === CGRectGetMidX(tickRect))) ||
( _isVertical && (newPoint.y === CGRectGetMidY(tickRect))))
{
// Start an affinity
[self setObjectValue:_cachedTickMarksValues[tickMarkSegment]];
_stickingOnTickMark = YES;
_affinityPoint = _isVertical ? newPoint.y : newPoint.x;
}
// Are we going too far from the tick mark ?
else if (_stickingOnTickMark && (ABS((_isVertical ? newPoint.y : newPoint.x) - _affinityPoint) > AFFINITY))
{
// Yes. Stop affinity
_stickingOnTickMark = NO;
[self setObjectValue:[self _valueAtPoint:newPoint]];
}
else if (!_stickingOnTickMark) // Normal situation
[self setObjectValue:[self _valueAtPoint:newPoint]];
}
else // Not ticked at all or no possible affinity
[self setObjectValue:[self _valueAtPoint:newPoint]];
return YES;
}
- (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp
{
[self setHighlighted:NO];
if ([_target respondsToSelector:@selector(sliderDidFinish:)])
[_target sliderDidFinish:self];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
- (BOOL)isContinuous
{
return _isContinuous;
}
- (void)setContinuous:(BOOL)flag
{
_isContinuous = flag;
if (_isContinuous)
_sendActionOn |= CPLeftMouseDraggedMask;
else
_sendActionOn &= ~CPLeftMouseDraggedMask;
}
- (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
{
var count = objects.length,
value = [objects[0] valueForKeyPath:aKeyPath];
[self setObjectValue:value];
while (count-- > 1)
if (value !== ([objects[count] valueForKeyPath:aKeyPath]))
return [self setFloatValue:1.0];
}
// MARK: - New methods as in High Sierra (10.13)
/*!
Creates and returns a continuous horizontal slider whose values range from 0.0 to 1.0.
@param aTarget
@param anAction
*/
+ (CPSlider)sliderWithTarget:(id)aTarget action:(SEL)anAction
{
return [self sliderWithValue:0.0 minValue:0.0 maxValue:1.0 target:aTarget action:anAction];
}
// Creates and returns a continuous horizontal slider that represents values over the specified range.
+ (CPSlider)sliderWithValue:(double)value minValue:(double)minValue maxValue:(double)maxValue target:(id)target action:(SEL)action
{
var slider = [[self alloc] initWithFrame:CGRectMake(0, 0, 104, 19)]; // Size used by Cocoa
[slider setMinValue:minValue];
[slider setMaxValue:maxValue];
[slider setDoubleValue:value];
[slider setTarget:target];
[slider setAction:action];
[slider setContinuous:YES];
[slider _refreshCachesAndStates];
return slider;
}
// The knobs thickness, in pixels.
// The thickness is defined to be the extent of the knob along the long dimension of the bar.
// In a vertical slider, a knobs thickness is its height; in a horizontal slider, a knobs thickness is its width.
- (float)knobThickness
{
var knobSize = [self currentValueForThemeAttribute:@"knob-size"];
return _isVertical ? knobSize.height : knobSize.width;
}
// allowsTickMarkValuesOnly : A Boolean value that indicates whether the receiver fixes
// its values to those values represented by its tick marks.
// YES if the slider fixes its values to the values represented by its tick marks; otherwise, NO.
// For example, if a slider has a minimum value of 0, a maximum value of 100, and five markers,
// the allowable values are 0, 25, 50, 75, and 100. When users move the sliders knob,
// it jumps to the tick mark nearest the cursor when the mouse button is released.
// This method has no effect if the slider has no tick marks.
- (void)setAllowsTickMarkValuesOnly:(BOOL)shouldAllowTickMarkValuesOnly
{
if (shouldAllowTickMarkValuesOnly === _allowsTickMarkValuesOnly)
return;
[self willChangeValueForKey:@"allowsTickMarkValuesOnly"];
_allowsTickMarkValuesOnly = shouldAllowTickMarkValuesOnly;
[self didChangeValueForKey:@"allowsTickMarkValuesOnly"];
[self _refreshCachesAndStates];
[self setNeedsDisplay:YES];
}
// Returns the value of the tick mark closest to the specified value.
- (double)closestTickMarkValueToValue:(double)value
{
if (_numberOfTickMarks === 1)
return _cachedTickMarksValues[0];
_closestTickMarkIndex = CPNotFound;
var foundDelta = 2 * _maxValue,
delta;
for (var i = 0; i < _numberOfTickMarks; i++)
{
delta = ABS(value - _cachedTickMarksValues[i]);
if (delta < foundDelta)
{
foundDelta = delta;
_closestTickMarkIndex = i;
}
}
return _closestTickMarkIndex ? _cachedTickMarksValues[_closestTickMarkIndex] : _minValue;
}
// Returns the index of the tick mark closest to the location of the receiver represented by the given point.
- (CPInteger)indexOfTickMarkAtPoint:(CGPoint)point
{
var foundIndex = CPNotFound;
if (_isCircular)
{
// CPCircularSlider
var bounds = [self bounds],
dx = point.x - bounds.size.width / 2,
dy = bounds.size.height / 2 - point.y,
angle = 1 + (((-3 * PI_2 - ATAN2(dy, dx)) % PI2) / PI2);
for (var i = 0; (i < _numberOfTickMarks) && (foundIndex === CPNotFound); i++)
if ((angle < _cachedTickMarksSegments[i]) || ((i === 0) && (angle >= (1 - _cachedTickMarksSegments[i]))))
foundIndex = i;
}
else
{
// CPLinearSlider
if (_isVertical)
{
for (var i = 0; (i < _numberOfTickMarks) && (foundIndex === CPNotFound); i++)
if (point.y <= _cachedTickMarksSegments[i])
foundIndex = i;
}
else
{
for (var i = 0; (i < _numberOfTickMarks) && (foundIndex === CPNotFound); i++)
if (point.x <= _cachedTickMarksSegments[i])
foundIndex = i;
}
}
return foundIndex;
}
// The number of the slider's tick marks. The tick marks assigned to the minimum and maximum values are included.
- (void)setNumberOfTickMarks:(CPInteger)aNumber
{
if (aNumber === _numberOfTickMarks)
return;
// We try to play nicer than Cocoa when passing from a non-ticked slider to a ticked one (and vice versa)
var tickedChanged = (((_numberOfTickMarks == 0) && (aNumber > 0)) || ((_numberOfTickMarks > 0) && (aNumber == 0)));
[self willChangeValueForKey:@"numberOfTickMarks"];
_numberOfTickMarks = aNumber;
[self didChangeValueForKey:@"numberOfTickMarks"];
// If tickedChanged, this refresh is necessary to set new states but we'll have to call it again after for correct geometry
[self _refreshCachesAndStates];
if (tickedChanged)
{
// First, remove ephemeral subviews as they may change with new state (needed ones will be created by layoutSubviews)
[self removeEphemeralSubviewNamed:@"track-view"];
[self removeEphemeralSubviewNamed:@"left-track-view"];
[self removeEphemeralSubviewNamed:@"right-track-view"];
[self removeEphemeralSubviewNamed:@"knob-view"];
// Now adapt the frame size to match what Xcode IB would have provided
var currentSize = [self frameSize],
newSize = [self currentValueForThemeAttribute:@"ib-size"];
if (_isCircular)
[self setFrameSize:CGSizeMake(newSize, newSize)];
else if (_isVertical)
[self setFrameSize:CGSizeMake(newSize, currentSize.height)];
else
[self setFrameSize:CGSizeMake(currentSize.width, newSize)];
// _refreshCachesAndStates, setNeedsDisplay and setNeedsLayout are called in setFrameSize
}
else
[self setNeedsDisplay:YES];
}
// Returns the bounding rectangle of the tick mark at the given index.
- (CGRect)rectOfTickMarkAtIndex:(CPInteger)index
{
// If no tick mark is associated with index, the method raises NSRangeException
if ((index < 0) || (index >= _numberOfTickMarks))
[CPException raise:CPRangeException reason:@"rectOfTickMarkAtIndex: index ("+index+") is outside permitted values (0.."+(_numberOfTickMarks-1)+")"];
return _cachedTickMarksRects[index];
}
// tickMarkPosition : Determines how the receivers tick marks are aligned with it.
// Possible values are CPTickMarkBelow, CPTickMarkAbove, CPTickMarkLeft, and CPTickMarkRight
// (the last two are for vertical sliders). The default alignments are CPTickMarkBelow and CPTickMarkLeft.
// This property has no effect if no tick marks have been assigned (that is, numberOfTickMarks returns 0).
- (void)setTickMarkPosition:(CPTickMarkPosition)aTickMarkPosition
{
if (aTickMarkPosition === _tickMarkPosition)
return;
[self willChangeValueForKey:@"tickMarkPosition"];
_tickMarkPosition = aTickMarkPosition;
[self didChangeValueForKey:@"tickMarkPosition"];
[self _refreshCachesAndStates];
[self setNeedsDisplay:YES];
}
// Returns the receivers value represented by the tick mark at the specified index.
- (double)tickMarkValueAtIndex:(CPInteger)index
{
// If no tick mark is associated with index, the method raises NSRangeException
if ((index < 0) || (index >= _numberOfTickMarks))
[CPException raise:CPRangeException reason:@"tickMarkValueAtIndex: index ("+index+") is outside permitted values (0.."+(_numberOfTickMarks-1)+")"];
return _cachedTickMarksValues[index];
}
// The color of the filled portion of the slider track, in appearances that support it.
- (void)setTrackFillColor:(CPColor)aColor
{
var normalState = _isVertical ? [CPThemeStateVertical, CPThemeStateKeyWindow] : [CPThemeStateKeyWindow],
leftTrackColor = [self valueForThemeAttribute:@"left-track-color" inStates:normalState];
if (leftTrackColor === aColor)
return;
[self setValue:aColor forThemeAttribute:@"left-track-color" inStates:normalState];
}
- (CPColor)trackFillColor
{
var normalState = _isVertical ? [CPThemeStateVertical, CPThemeStateKeyWindow] : [CPThemeStateKeyWindow];
return [self valueForThemeAttribute:@"left-track-color" inStates:normalState];
}
// MARK: - Private methods
- (void)_refreshCachesAndStates
{
// Purge caches
_cachedTickMarksRects = @[];
_cachedTickMarksValues = @[];
_cachedTickMarksSegments = @[];
// Reset affinity
_canStickOnTickMarks = NO;
// Recalculate _isVertical.
var bounds = [self bounds],
boundsWidth = bounds.size.width,
boundsHeight = bounds.size.height;
_isVertical = (boundsWidth < boundsHeight) ? 1 : ((boundsWidth > boundsHeight) ? 0 : -1);
if (_isVertical)
[self setThemeState:CPThemeStateVertical];
else
[self unsetThemeState:CPThemeStateVertical];
// Recalculate _isCircular
_isCircular = (_sliderType === CPCircularSlider);
if (_isCircular)
[self setThemeState:CPThemeStateCircular];
else
[self unsetThemeState:CPThemeStateCircular];
// Recalculate _isTicked
_isTicked = (_numberOfTickMarks > 0);
// If this slider has no tick marks, we're done.
if (!_isTicked)
{
[self unsetThemeState:CPThemeStateTickedSlider];
[self unsetThemeState:CPThemeStateAboveLeftTickedSlider];
[self unsetThemeState:CPThemeStateBelowRightTickedSlider];
return;
}
// Set specific theme states
[self setThemeState:CPThemeStateTickedSlider];
if (!_isCircular)
if (_tickMarkPosition === CPTickMarkPositionAbove)
{
[self setThemeState:CPThemeStateAboveLeftTickedSlider];
[self unsetThemeState:CPThemeStateBelowRightTickedSlider];
}
else
{
[self unsetThemeState:CPThemeStateAboveLeftTickedSlider];
[self setThemeState:CPThemeStateBelowRightTickedSlider];
}
// Calculate cached values : tick marks rects, values and segments
var tickSize = [self currentValueForThemeAttribute:@"tick-mark-size"],
tickMarkMargin = [self currentValueForThemeAttribute:@"tick-mark-margin"],
topMargin = [self currentValueForThemeAttribute:@"top-margin"],
bottomMargin = [self currentValueForThemeAttribute:@"bottom-margin"],
knobSize = [self currentValueForThemeAttribute:@"knob-size"],
width = tickSize.width,
height = tickSize.height,
x,
y,
tickRect,
value;
if (_isCircular)
{
// CPCircularSlider
// Remark : Cocoa doesn't use tick affinity with circular sliders
var delta = 1 / _numberOfTickMarks,
dX = width / 2,
dY = height / 2,
cX = boundsWidth / 2,
cY = boundsHeight / 2,
radius = cX - dX,
deltaValue = (_maxValue - _minValue) / _numberOfTickMarks;
value = _minValue;
for (var i = 0, angle; i < _numberOfTickMarks; i++, value += deltaValue)
{
angle = PI_2 - PI2 * i * delta;
x = radius * COS(angle) + cX;
y = radius * SIN(-angle) + cY; // Vertical coordinates are reversed in Cappuccino
tickRect = CGRectMake(ROUND(x - dX), ROUND(y - dY), width, height);
[_cachedTickMarksRects addObject:tickRect];
[_cachedTickMarksValues addObject:value];
[_cachedTickMarksSegments addObject:(i + 0.5) * delta];
}
}
else
{
// CPLinearSlider
if (_isVertical)
{
// Vertical slider
// If there's only one tick mark, it's centered.
// If there's more than one tick mark, there's at least one at the min value and one at the max value
var usableHeight = boundsHeight - knobSize.height,
dY = (height - 1)/2;
x = (_tickMarkPosition === CPTickMarkPositionLeading) ? topMargin : (boundsWidth - width - bottomMargin);
if (_numberOfTickMarks === 1)
{
y = (boundsHeight - height) / 2;
value = (_maxValue - _minValue) / 2 + _minValue;
tickRect = CGRectMake(x, ROUND(y - dY), width, height);
[_cachedTickMarksRects addObject:tickRect];
[_cachedTickMarksValues addObject:value];
[_cachedTickMarksSegments addObject:bounds];
}
else
{
var delta = usableHeight / (_numberOfTickMarks - 1),
deltaValue = (_maxValue - _minValue) / (_numberOfTickMarks - 1);
y = (knobSize.height - 1) / 2;
value = _maxValue;
_canStickOnTickMarks = !_allowsTickMarkValuesOnly && (delta > AFFINITY+3);
for (var i = 0; i < _numberOfTickMarks; i++, y += delta, value -= deltaValue)
{
tickRect = CGRectMake(x, ROUND(y - dY), width, height);
[_cachedTickMarksRects addObject:tickRect];
[_cachedTickMarksValues addObject:(i < _numberOfTickMarks - 1) ? value : _minValue]; // Avoid rounding problems on last tick mark
[_cachedTickMarksSegments addObject:(i < _numberOfTickMarks - 1) ? ROUND(y + delta/2) : boundsHeight];
}
}
}
else
{
// Horizontal slider
// If there's only one tick mark, it's centered.
// If there's more than one tick mark, there's at least one at the min value and one at the max value
var usableWidth = boundsWidth - knobSize.width,
dX = (width - 1)/2;
y = (_tickMarkPosition === CPTickMarkPositionAbove) ? topMargin : (boundsHeight - height - bottomMargin);
if (_numberOfTickMarks === 1)
{
x = (boundsWidth - width) / 2;
value = (_maxValue - _minValue) / 2 + _minValue;
tickRect = CGRectMake(ROUND(x - dX), y, width, height);
[_cachedTickMarksRects addObject:tickRect];
[_cachedTickMarksValues addObject:value];
[_cachedTickMarksSegments addObject:boundsWidth];
}
else
{
var delta = usableWidth / (_numberOfTickMarks - 1),
deltaValue = (_maxValue - _minValue) / (_numberOfTickMarks - 1);
x = (knobSize.width - 1) / 2;
value = _minValue;
_canStickOnTickMarks = !_allowsTickMarkValuesOnly && (delta > AFFINITY+3);
for (var i = 0; i < _numberOfTickMarks; i++, x += delta, value += deltaValue)
{
tickRect = CGRectMake(ROUND(x - dX), y, width, height);
[_cachedTickMarksRects addObject:tickRect];
[_cachedTickMarksValues addObject:(i < _numberOfTickMarks - 1) ? value : _maxValue]; // Avoid rounding problems on last tick mark
[_cachedTickMarksSegments addObject:(i < _numberOfTickMarks - 1) ? ROUND(x + delta/2) : boundsWidth];
}
}
}
}
#if PLATFORM(DOM)
// Don't do this in nib2cib !..
if (_allowsTickMarkValuesOnly)
{
[self setObjectValue:[self closestTickMarkValueToValue:[self doubleValue]]];
_currentTickMarkSegment = _closestTickMarkIndex;
[self sendAction:[self action] to:[self target]];
}
#endif
}
/*! @ignore */
- (float)_valueAtPoint:(CGPoint)aPoint
{
var bounds = [self bounds],
knobSize = [self currentValueForThemeAttribute:@"knob-size"],
trackRect = [self trackRectForBounds:bounds],
value;
if (_isCircular)
{
var dx = aPoint.x - CGRectGetMidX(trackRect),
dy = CGRectGetMidY(trackRect) - aPoint.y;
value = ((1 + (((-3 * PI_2 - ATAN2(dy, dx)) % PI2) / PI2)) * (_maxValue - _minValue) + _minValue) % _maxValue;
}
else if (_isVertical)
{
var knobHeight = knobSize.height;
trackRect.origin.y += knobHeight / 2;
trackRect.size.height -= knobHeight;
value = MAX(0.0, MIN(1.0, (trackRect.origin.y + trackRect.size.height - aPoint.y) / trackRect.size.height)) * (_maxValue - _minValue) + _minValue;
}
else
{
var knobWidth = knobSize.width;
trackRect.origin.x += knobWidth / 2;
trackRect.size.width -= knobWidth;
value = MAX(0.0, MIN(1.0, (aPoint.x - trackRect.origin.x) / trackRect.size.width)) * (_maxValue - _minValue) + _minValue;
}
return value;
}
// MARK: - Overrides
- (void)setFrameSize:(CGSize)aSize
{
[super setFrameSize:aSize];
[self _refreshCachesAndStates];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
// MARK: -
@end
var CPSliderMinValueKey = "CPSliderMinValueKey",
CPSliderMaxValueKey = "CPSliderMaxValueKey",
CPSliderAltIncrValueKey = "CPSliderAltIncrValueKey",
CPSliderTypeKey = "CPSliderTypeKey",
CPSliderAllowsTickMarkValuesOnlyKey = "CPSliderAllowsTickMarkValuesOnlyKey",
CPSliderTickMarkPositionKey = "CPSliderTickMarkPositionKey",
CPSliderNumberOfTickMarksKey = "CPSliderNumberOfTickMarksKey";
@implementation CPSlider (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
_minValue = [aCoder decodeDoubleForKey:CPSliderMinValueKey];
_maxValue = [aCoder decodeDoubleForKey:CPSliderMaxValueKey];
self = [super initWithCoder:aCoder];
if (self)
{
_altIncrementValue = [aCoder decodeDoubleForKey:CPSliderAltIncrValueKey];
_sliderType = [aCoder decodeIntForKey: CPSliderTypeKey];
_allowsTickMarkValuesOnly = [aCoder decodeBoolForKey: CPSliderAllowsTickMarkValuesOnlyKey];
_tickMarkPosition = [aCoder decodeIntForKey: CPSliderTickMarkPositionKey];
_numberOfTickMarks = [aCoder decodeIntForKey: CPSliderNumberOfTickMarksKey];
_isContinuous = (_sendActionOn & CPLeftMouseDraggedMask) !== 0;
[self _refreshCachesAndStates];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeDouble:_minValue forKey:CPSliderMinValueKey];
[aCoder encodeDouble:_maxValue forKey:CPSliderMaxValueKey];
[aCoder encodeDouble:_altIncrementValue forKey:CPSliderAltIncrValueKey];
[aCoder encodeInt: _sliderType forKey:CPSliderTypeKey];
[aCoder encodeBool: _allowsTickMarkValuesOnly forKey:CPSliderAllowsTickMarkValuesOnlyKey];
[aCoder encodeInt: _tickMarkPosition forKey:CPSliderTickMarkPositionKey];
[aCoder encodeInt: _numberOfTickMarks forKey:CPSliderNumberOfTickMarksKey];
}
@end
@implementation CPSlider (Deprecated)
- (id)value
{
CPLog.warn("[CPSlider value] is deprecated, use doubleValue or objectValue instead.");
return [self doubleValue];
}
- (void)setValue:(id)aValue
{
CPLog.warn("[CPSlider setValue:] is deprecated, use setDoubleValue: or setObjectValue: instead.");
[self setObjectValue:aValue];
}
@end