Files
cappuccino/AppKit/CPStepper.j
T
David Richardson f468d483ff AppKit, Foundation: Fix view archiving and add regression tests
Keyed archiving preserves view hierarchies created in code. Decoded views
retain their content, subviews and state, and each view class is verified
against its own description before archiving.

* CPAccordionView: Add CPCoding to CPAccordionView and CPAccordionViewItem.

* CPBox:
  * Encode the content, title and box views unconditionally.
  * Clear the superviews of the box and title views during decoding.

* CPButton: Initialize the value to CPOffState.

* CPMapTable: Add CPCoding.

* CPPopUpButton: Clear the inherited button value.

* CPSearchField: Update the cancel button when the object value changes.

* CPSplitView: Return nil from dividerColor when the theme defines no divider
  image.

* CPStepper: Discard the decoded buttons before recreating them.

* CPTabView:
  * Encode the current tab view items.
  * Exclude the tabs from the encoded subviews.

* CPTokenField:
  * Discard the decoded scroll view before recreating it.
  * Add the decoded tokens to the new document view.

* CPVisualEffectView: Resolve the backdrop filter property only when a DOM is
  available.

* CPBoxTest: Verify preservation of the content view, hierarchy and title, and
  decoding of an untitled box without warnings.

* CPButtonTest: Verify the initial value of a button.

* CPMapTableTest: Verify preservation of entries through archiving.

* DropInArchiveFixtures: Add populated view fixtures, tree comparison, and
  round trip and saved archive helpers.

* DropInArchiveGenerator: Save cib descriptions and view archives, and write no
  archive data if any view fails its round trip.

* DropInArchiveTest: Verify new archives, saved archives and reference cibs, and
  treat decoding warnings as failures.

* DropInSnapshot:
  * Describe the view hierarchy through private views, with state and content.
  * Add DropInWithFixedScrollerStyle.

* DropInArchives, DropInCibs: Add saved view archives, cib descriptions and
  reference cibs.
2026-10-31 20:51:13 -06:00

351 lines
11 KiB
Plaintext

/*
* CPStepper.j
* AppKit
*
* Created by Antoine Mercadal
* Copyright 2009, Antoine Mercadal
*
* 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 "CPButton.j"
@import "CPTextField.j"
/*!
CPStepper is an implementation of Cocoa NSStepper.
This control displays a two part button that can be used to increase or decrease a value with a given interval.
*/
@implementation CPStepper: CPControl
{
BOOL _valueWraps @accessors(property=valueWraps);
BOOL _autorepeat @accessors(getter=autorepeat);
int _increment @accessors(property=increment);
int _maxValue @accessors(property=maxValue);
int _minValue @accessors(property=minValue);
CPButton _buttonDown;
CPButton _buttonUp;
}
// MARK: -
// MARK: Initialization
/*!
Initializes a CPStepper with given values.
@param aValue the initial value of the CPStepper
@param minValue the minimal acceptable value of the stepper
@param maxValue the maximal acceptable value of the stepper
@return Initialized CPStepper
*/
+ (id)stepperWithInitialValue:(float)aValue minValue:(float)aMinValue maxValue:(float)aMaxValue
{
var stepper = [[self alloc] initWithFrame:CGRectMakeZero()];
[stepper setDoubleValue:aValue];
[stepper setMinValue:aMinValue];
[stepper setMaxValue:aMaxValue];
// _sizeToFit will put the good size for the stepper depending of the current theme
[stepper _sizeToFit];
return stepper;
}
/*!
Initializes a CPStepper with default values:
- minValue = 0.0
- maxValue = 59.0
- value = 0.0
@return Initialized CPStepper
*/
+ (id)stepper
{
return [CPStepper stepperWithInitialValue:0.0 minValue:0.0 maxValue:59.0];
}
+ (Class)_binderClassForBinding:(CPString)aBinding
{
if (aBinding === CPValueBinding || aBinding === CPMinValueBinding || aBinding === CPMaxValueBinding)
return [_CPStepperValueBinder class];
return [super _binderClassForBinding:aBinding];
}
- (CPString)_replacementKeyPathForBinding:(CPString)aBinding
{
if (aBinding === CPValueBinding)
return @"doubleValue";
return [super _replacementKeyPathForBinding:aBinding];
}
/*!
Initializes a CPStepper.
@param aFrame the frame of the control
@return initialized CPStepper
*/
- (id)initWithFrame:(CGRect)aFrame
{
if (self = [super initWithFrame:aFrame])
{
_maxValue = 59.0;
_minValue = 0.0;
_increment = 1.0;
_valueWraps = YES;
_autorepeat = YES;
[self setDoubleValue:0.0];
[self _init];
}
return self;
}
/*! @ignore */
- (void)_init
{
_buttonUp = [[CPButton alloc] initWithFrame:CGRectMakeZero()];
[_buttonUp setContinuous:_autorepeat];
[_buttonUp setTarget:self];
[_buttonUp setAction:@selector(_buttonDidClick:)];
[_buttonUp setAutoresizingMask:CPViewNotSizable];
[self addSubview:_buttonUp];
_buttonDown = [[CPButton alloc] initWithFrame:CGRectMakeZero()];
[_buttonDown setContinuous:_autorepeat];
[_buttonDown setTarget:self];
[_buttonDown setAction:@selector(_buttonDidClick:)];
[_buttonDown setAutoresizingMask:CPViewNotSizable];
[self setContinuous:_autorepeat];
[self addSubview:_buttonDown];
[self _sizeToFit];
[self setNeedsLayout];
}
- (void)setControlSize:(CPControlSize)aControlSize
{
[super setControlSize:aControlSize];
[self _sizeToFit];
}
// MARK: -
// MARK: Superclass overrides
/*!
Set if the CPStepper is enabled or not.
@param shouldEnabled BOOL that define if stepper is enabled or not.
*/
- (void)setEnabled:(BOOL)shouldEnabled
{
[super setEnabled:shouldEnabled];
[_buttonUp setEnabled:shouldEnabled];
[_buttonDown setEnabled:shouldEnabled];
}
- (void)setFrame:(CGRect)aFrame
{
var upSize = [self currentValueForThemeAttribute:@"up-button-size"],
downSize = [self currentValueForThemeAttribute:@"down-button-size"],
minSize = CGSizeMake(upSize.width, upSize.height + downSize.height),
frame = CGRectMakeCopy(aFrame);
frame.size.width = MAX(minSize.width, frame.size.width);
frame.size.height = MAX(minSize.height, frame.size.height);
[super setFrame:frame];
}
/*! @ignore */
- (void)layoutSubviews
{
var controlSizeThemeState = [self _controlSizeThemeState],
aFrame = [self frame],
upSize = [self valueForThemeAttribute:@"up-button-size" inState:controlSizeThemeState],
downSize = [self valueForThemeAttribute:@"down-button-size" inState:controlSizeThemeState],
upFrame = CGRectMake(0, 0, upSize.width, upSize.height),
downFrame = CGRectMake(0, upSize.height, downSize.width, downSize.height);
[_buttonUp setFrame:upFrame];
[_buttonDown setFrame:downFrame];
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:[controlSizeThemeState, CPThemeStateBordered]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPButtonStateBezelStyleRoundRect]];
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateDisabled]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateDisabled, CPButtonStateBezelStyleRoundRect]];
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateHighlighted]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateHighlighted, CPButtonStateBezelStyleRoundRect]];
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:[controlSizeThemeState, CPThemeStateBordered]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPButtonStateBezelStyleRoundRect]];
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateDisabled]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateDisabled, CPButtonStateBezelStyleRoundRect]];
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateHighlighted]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateHighlighted, CPButtonStateBezelStyleRoundRect]];
}
- (void)_sizeToFit
{
[self setFrame:CGRectMake([self frameOrigin].x, [self frameOrigin].y, 0, 0)];
}
/*!
Set if CPStepper should autorepeat.
@param shouldAutoRepeat if YES, the first mouse down does one increment (decrement) and, after each delay of 0.5 seconds
*/
- (void)setAutorepeat:(BOOL)shouldAutoRepeat
{
if (shouldAutoRepeat !== _autorepeat)
{
[_buttonUp setContinuous:shouldAutoRepeat];
[_buttonDown setContinuous:shouldAutoRepeat];
}
[self setContinuous:shouldAutoRepeat];
}
/*!
Set the current value of the stepper.
@param aValue a float containing the value
*/
- (void)setDoubleValue:(double)aValue
{
if (aValue > _maxValue)
[super setDoubleValue:_valueWraps ? _minValue : _maxValue];
else if (aValue < _minValue)
[super setDoubleValue:_valueWraps ? _maxValue : _minValue];
else
[super setDoubleValue:aValue];
}
// MARK: -
// MARK: Actions
/*! @ignore */
- (IBAction)_buttonDidClick:(id)aSender
{
if (![self isEnabled])
return;
if (aSender === _buttonUp)
[self setDoubleValue:([self doubleValue] + _increment)];
else
[self setDoubleValue:([self doubleValue] - _increment)];
[self sendAction:[self action] to:[self target]];
}
/*!
Perform a programatic click on up button.
@param aSender sender of the action
*/
- (IBAction)performClickUp:(id)aSender
{
[_buttonUp performClick:aSender];
}
/*!
Perform a programatic click on down button.
@param aSender sender of the action
*/
- (IBAction)performClickDown:(id)aSender
{
[_buttonDown performClick:aSender];
}
// MARK: -
// MARK: Theming
+ (CPString)defaultThemeClass
{
return @"stepper";
}
+ (CPDictionary)themeAttributes
{
return @{
@"bezel-color-up-button": [CPNull null],
@"bezel-color-down-button": [CPNull null],
@"up-button-size": CGSizeMakeZero(),
@"down-button-size": CGSizeMakeZero(),
};
}
@end
@implementation _CPStepperValueBinder : CPBinder
{
}
- (void)_updatePlaceholdersWithOptions:(CPDictionary)options forBinding:(CPString)aBinding
{
var placeholder = (aBinding === CPMaxValueBinding) ? [_source maxValue] : [_source minValue];
[super _updatePlaceholdersWithOptions:options];
[self _setPlaceholder:placeholder forMarker:CPMultipleValuesMarker isDefault:YES];
[self _setPlaceholder:placeholder forMarker:CPNoSelectionMarker isDefault:YES];
[self _setPlaceholder:placeholder forMarker:CPNotApplicableMarker isDefault:YES];
[self _setPlaceholder:placeholder forMarker:CPNullMarker isDefault:YES];
}
@end
var CPStepperMinValue = @"CPStepperMinValue",
CPStepperMaxValue = @"CPStepperMaxValue",
CPStepperValueWraps = @"CPStepperValueWraps",
CPStepperAutorepeat = @"CPStepperAutorepeat",
CPStepperIncrement = @"CPStepperIncrement";
@implementation CPStepper (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
if (self = [super initWithCoder:aCoder])
{
_increment = [aCoder decodeIntForKey:CPStepperIncrement];
_minValue = [aCoder decodeIntForKey:CPStepperMinValue];
_maxValue = [aCoder decodeIntForKey:CPStepperMaxValue];
_valueWraps = [aCoder decodeBoolForKey:CPStepperValueWraps];
_autorepeat = [aCoder decodeBoolForKey:CPStepperAutorepeat];
// The archive holds the buttons as subviews, and _init creates them again.
[self setSubviews:@[]];
[self _init];
[self setEnabled:[self isEnabled]];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeInt:_increment forKey:CPStepperIncrement];
if (_minValue)
[aCoder encodeInt:_minValue forKey:CPStepperMinValue];
if (_maxValue)
[aCoder encodeInt:_maxValue forKey:CPStepperMaxValue];
if (_valueWraps)
[aCoder encodeBool:_valueWraps forKey:CPStepperValueWraps];
if (_autorepeat)
[aCoder encodeBool:_autorepeat forKey:CPStepperAutorepeat];
}
@end