mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 04:57:03 +00:00
If a segmented control was initialised with a zero width and then expanded with setSegmentCount, it would not allocate space for any needed dividers. The more tabs were added the more the segmented control would be undersized. The fix is to automatically adjust the width when dividers are added or removed.
960 lines
28 KiB
Plaintext
960 lines
28 KiB
Plaintext
/*
|
|
* CPSegmentedControl.j
|
|
* AppKit
|
|
*
|
|
* Created by Francisco Tolmasky.
|
|
* Copyright 2008, 280 North, Inc.
|
|
*
|
|
* 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 <Foundation/CPArray.j>
|
|
|
|
@import "CPControl.j"
|
|
|
|
|
|
CPSegmentSwitchTrackingSelectOne = 0;
|
|
CPSegmentSwitchTrackingSelectAny = 1;
|
|
CPSegmentSwitchTrackingMomentary = 2;
|
|
|
|
/*!
|
|
@ingroup appkit
|
|
@class CPSegmentedControl
|
|
|
|
This class is a horizontal button with multiple segments.
|
|
*/
|
|
@implementation CPSegmentedControl : CPControl
|
|
{
|
|
CPArray _segments;
|
|
CPArray _themeStates;
|
|
|
|
int _selectedSegment;
|
|
int _segmentStyle;
|
|
CPSegmentSwitchTracking _trackingMode;
|
|
|
|
unsigned _trackingSegment;
|
|
BOOL _trackingHighlighted;
|
|
}
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "segmented-control";
|
|
}
|
|
|
|
+ (id)themeAttributes
|
|
{
|
|
return [CPDictionary dictionaryWithObjects:[CPCenterTextAlignment, CPCenterVerticalTextAlignment, CPImageLeft, CPScaleNone, _CGInsetMakeZero(), _CGInsetMakeZero(), [CPNull null], [CPNull null], [CPNull null], [CPNull null], 1.0, 24.0]
|
|
forKeys:[@"alignment", @"vertical-alignment", @"image-position", @"image-scaling", @"bezel-inset", @"content-inset", @"left-segment-bezel-color", @"right-segment-bezel-color", @"center-segment-bezel-color", @"divider-bezel-color", @"divider-thickness", @"default-height"]];
|
|
}
|
|
|
|
- (id)initWithFrame:(CGRect)aRect
|
|
{
|
|
_segments = [];
|
|
_themeStates = [];
|
|
|
|
self = [super initWithFrame:aRect];
|
|
|
|
if (self)
|
|
{
|
|
_selectedSegment = -1;
|
|
|
|
_trackingMode = CPSegmentSwitchTrackingSelectOne;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
/*!
|
|
Returns the tag of the selected segment.
|
|
*/
|
|
- (int)selectedTag
|
|
{
|
|
return [_segments[_selectedSegment] tag];
|
|
}
|
|
|
|
// Specifying the number of segments
|
|
/*!
|
|
Sets the number of segments in the button.
|
|
@param aCount the number of segments on the button
|
|
*/
|
|
- (void)setSegmentCount:(unsigned)aCount
|
|
{
|
|
if (_segments.length == aCount)
|
|
return;
|
|
|
|
var height = CGRectGetHeight([self bounds]),
|
|
dividersBefore = MAX(0, _segments.length - 1),
|
|
dividersAfter = MAX(0, aCount - 1);
|
|
|
|
if (_segments.length < aCount)
|
|
{
|
|
for (var index = _segments.length; index < aCount; ++index)
|
|
{
|
|
_segments[index] = [[_CPSegmentItem alloc] init];
|
|
_themeStates[index] = CPThemeStateNormal;
|
|
}
|
|
}
|
|
else if (aCount < _segments.length)
|
|
{
|
|
_segments.length = aCount;
|
|
_themeStates.length = aCount;
|
|
}
|
|
|
|
if (_selectedSegment >= _segments.length)
|
|
_selectedSegment = -1;
|
|
|
|
// Make space for/remove space used by dividers.
|
|
var thickness = [self currentValueForThemeAttribute:@"divider-thickness"],
|
|
delta = thickness * (dividersAfter - dividersBefore),
|
|
frame = [self frame];
|
|
|
|
if (delta)
|
|
[self setFrameSize:CGSizeMake(frame.size.width + delta, frame.size.height)];
|
|
|
|
[self tileWithChangedSegment:0];
|
|
}
|
|
|
|
/*!
|
|
Returns the number of segments in the button.
|
|
*/
|
|
- (unsigned)segmentCount
|
|
{
|
|
return _segments.length;
|
|
}
|
|
|
|
// Specifying Selected Segment
|
|
/*!
|
|
Selects a segment.
|
|
@param aSegment the segment to select
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (void)setSelectedSegment:(unsigned)aSegment
|
|
{
|
|
// setSelected:forSegment throws the exception for us (if necessary)
|
|
[self setSelected:YES forSegment:aSegment];
|
|
}
|
|
|
|
/*!
|
|
Returns the selected segment.
|
|
*/
|
|
- (unsigned)selectedSegment
|
|
{
|
|
return _selectedSegment;
|
|
}
|
|
|
|
/*!
|
|
Selects the button segment with the specified tag.
|
|
*/
|
|
- (BOOL)selectSegmentWithTag:(int)aTag
|
|
{
|
|
var index = 0;
|
|
|
|
for (; index < _segments.length; ++index)
|
|
if (_segments[index].tag == aTag)
|
|
{
|
|
[self setSelectedSegment:index];
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
// Specifying Tracking Mode
|
|
|
|
- (BOOL)isTracking
|
|
{
|
|
|
|
}
|
|
|
|
- (void)setTrackingMode:(CPSegmentSwitchTracking)aTrackingMode
|
|
{
|
|
if (_trackingMode == aTrackingMode)
|
|
return;
|
|
|
|
_trackingMode = aTrackingMode;
|
|
|
|
if (_trackingMode == CPSegmentSwitchTrackingSelectOne)
|
|
{
|
|
var index = 0,
|
|
selected = NO;
|
|
|
|
for (; index < _segments.length; ++index)
|
|
if ([_segments[index] selected])
|
|
if (selected)
|
|
[self setSelected:NO forSegment:index];
|
|
else
|
|
selected = YES;
|
|
}
|
|
|
|
else if (_trackingMode == CPSegmentSwitchTrackingMomentary)
|
|
{
|
|
var index = 0;
|
|
|
|
for (; index < _segments.length; ++index)
|
|
if ([_segments[index] selected])
|
|
[self setSelected:NO forSegment:index];
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Returns the control's tracking mode.
|
|
*/
|
|
- (CPSegmentSwitchTracking)trackingMode
|
|
{
|
|
return _trackingMode;
|
|
}
|
|
|
|
// Working with Individual Segments
|
|
/*!
|
|
Sets the width of the specified segment.
|
|
@param aWidth the new width for the segment
|
|
@param aSegment the segment to set the width for
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (void)setWidth:(float)aWidth forSegment:(unsigned)aSegment
|
|
{
|
|
[_segments[aSegment] setWidth:aWidth];
|
|
|
|
[self tileWithChangedSegment:aSegment];
|
|
}
|
|
|
|
/*!
|
|
Returns the width for the specified segment.
|
|
@param aSegment the segment to get the width for
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (float)widthForSegment:(unsigned)aSegment
|
|
{
|
|
return [_segments[aSegment] width];
|
|
}
|
|
|
|
/*!
|
|
Sets the image for the specified segment.
|
|
@param anImage the image for the segment
|
|
@param aSegment the segment to set the image on
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (void)setImage:(CPImage)anImage forSegment:(unsigned)aSegment
|
|
{
|
|
[_segments[aSegment] setImage:anImage];
|
|
|
|
[self tileWithChangedSegment:aSegment];
|
|
}
|
|
|
|
/*!
|
|
Returns the image for the specified segment
|
|
@param aSegment the segment to obtain the image for
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (CPImage)imageForSegment:(unsigned)aSegment
|
|
{
|
|
return [_segments[aSegment] image];
|
|
}
|
|
|
|
/*!
|
|
Sets the label for the specified segment
|
|
@param aLabel the label for the segment
|
|
@param aSegment the segment to label
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (void)setLabel:(CPString)aLabel forSegment:(unsigned)aSegment
|
|
{
|
|
[_segments[aSegment] setLabel:aLabel];
|
|
|
|
[self tileWithChangedSegment:aSegment];
|
|
}
|
|
|
|
/*!
|
|
Returns the label for the specified segment
|
|
@param the segment to obtain the label for
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (CPString)labelForSegment:(unsigned)aSegment
|
|
{
|
|
return [_segments[aSegment] label];
|
|
}
|
|
|
|
/*!
|
|
Sets the menu for the specified segment
|
|
@param aMenu the menu to set
|
|
@param aSegment the segment to set the menu on
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (void)setMenu:(CPMenu)aMenu forSegment:(unsigned)aSegment
|
|
{
|
|
[_segments[aSegment] setMenu:aMenu];
|
|
}
|
|
|
|
/*!
|
|
Returns the menu for the specified segment.
|
|
@param aSegment the segment to obtain the menu for
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (CPMenu)menuForSegment:(unsigned)aSegment
|
|
{
|
|
return [_segments[aSegment] menu];
|
|
}
|
|
|
|
/*!
|
|
Sets the selection for the specified segment. If only one segment
|
|
can be selected at a time, any other segment will be deselected.
|
|
@param isSelected \c YES selects the segment. \c NO deselects it.
|
|
@param aSegment the segment to set the selection for
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (void)setSelected:(BOOL)isSelected forSegment:(unsigned)aSegment
|
|
{
|
|
var segment = _segments[aSegment];
|
|
|
|
// If we're already in this state, bail.
|
|
if ([segment selected] == isSelected)
|
|
return;
|
|
|
|
[segment setSelected:isSelected];
|
|
|
|
_themeStates[aSegment] = isSelected ? CPThemeStateSelected : CPThemeStateNormal;
|
|
|
|
// We need to do some cleanup if we only allow one selection.
|
|
if (isSelected)
|
|
{
|
|
var oldSelectedSegment = _selectedSegment;
|
|
|
|
_selectedSegment = aSegment;
|
|
|
|
if (_trackingMode == CPSegmentSwitchTrackingSelectOne && oldSelectedSegment != aSegment && oldSelectedSegment != -1)
|
|
{
|
|
[_segments[oldSelectedSegment] setSelected:NO];
|
|
_themeStates[oldSelectedSegment] = CPThemeStateNormal;
|
|
|
|
[self drawSegmentBezel:oldSelectedSegment highlight:NO];
|
|
}
|
|
}
|
|
|
|
if (_trackingMode != CPSegmentSwitchTrackingMomentary)
|
|
[self drawSegmentBezel:aSegment highlight:NO];
|
|
|
|
[self setNeedsLayout];
|
|
[self setNeedsDisplay:YES];
|
|
}
|
|
|
|
/*!
|
|
Returns \c YES if the specified segment is selected.
|
|
@param aSegment the segment to check for selection
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (BOOL)isSelectedForSegment:(unsigned)aSegment
|
|
{
|
|
return [_segments[aSegment] selected];
|
|
}
|
|
|
|
/*!
|
|
Enables/diables the specified segment.
|
|
@param isEnabled \c YES enables the segment
|
|
@param aSegment the segment to enable/disble
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (void)setEnabled:(BOOL)isEnabled forSegment:(unsigned)aSegment
|
|
{
|
|
[_segments[aSegment] setEnabled:isEnabled];
|
|
|
|
if (isEnabled)
|
|
_themeStates[aSegment] &= ~CPThemeStateDisabled;
|
|
else
|
|
_themeStates[aSegment] |= CPThemeStateDisabled;
|
|
|
|
[self setNeedsLayout];
|
|
[self setNeedsDisplay:YES];
|
|
}
|
|
|
|
/*!
|
|
Returns \c YES if the specified segment is enabled.
|
|
@param aSegment the segment to check
|
|
@throws CPRangeException if \c aSegment is out of bounds
|
|
*/
|
|
- (BOOL)isEnabledForSegment:(unsigned)aSegment
|
|
{
|
|
return [_segments[aSegment] enabled];
|
|
}
|
|
|
|
/*!
|
|
Sets a tag for the specified segment.
|
|
@param aTag the tag to set
|
|
@param aSegment the segment to set the tag on
|
|
*/
|
|
- (void)setTag:(int)aTag forSegment:(unsigned)aSegment
|
|
{
|
|
[_segments[aSegment] setTag:aTag];
|
|
}
|
|
|
|
/*!
|
|
Returns the tag for the specified segment.
|
|
@param aSegment the segment to obtain the tag for
|
|
*/
|
|
- (int)tagForSegment:(unsigned)aSegment
|
|
{
|
|
return [_segments[aSegment] tag];
|
|
}
|
|
|
|
// Drawings
|
|
/*!
|
|
Draws the specified segment bezel
|
|
@param aSegment the segment to draw the bezel for
|
|
@param shouldHighlight \c YES highlights the bezel
|
|
*/
|
|
- (void)drawSegmentBezel:(int)aSegment highlight:(BOOL)shouldHighlight
|
|
{
|
|
if (shouldHighlight)
|
|
_themeStates[aSegment] |= CPThemeStateHighlighted;
|
|
else
|
|
_themeStates[aSegment] &= ~CPThemeStateHighlighted;
|
|
|
|
[self setNeedsLayout];
|
|
[self setNeedsDisplay:YES];
|
|
}
|
|
|
|
- (float)_leftOffsetForSegment:(unsigned)segment
|
|
{
|
|
var bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"];
|
|
|
|
if (segment == 0)
|
|
return bezelInset.left;
|
|
|
|
var thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
|
|
|
|
return [self _leftOffsetForSegment:segment - 1] + [self widthForSegment:segment - 1] + thickness;
|
|
}
|
|
|
|
- (unsigned)_indexOfLastSegment
|
|
{
|
|
var lastSegmentIndex = [_segments count] - 1;
|
|
|
|
if (lastSegmentIndex < 0)
|
|
lastSegmentIndex = 0;
|
|
|
|
return lastSegmentIndex;
|
|
}
|
|
|
|
- (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
|
|
{
|
|
var height = [self currentValueForThemeAttribute:@"default-height"],
|
|
contentInset = [self currentValueForThemeAttribute:@"content-inset"],
|
|
bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"],
|
|
bounds = [self bounds];
|
|
|
|
if (aName === "left-segment-bezel")
|
|
{
|
|
return CGRectMake(bezelInset.left, bezelInset.top, contentInset.left, height);
|
|
}
|
|
else if (aName === "right-segment-bezel")
|
|
{
|
|
return CPRectMake(CGRectGetWidth([self bounds]) - contentInset.right,
|
|
bezelInset.top,
|
|
contentInset.right,
|
|
height);
|
|
}
|
|
else if (aName.indexOf("segment-bezel") === 0)
|
|
{
|
|
var segment = parseInt(aName.substring("segment-bezel-".length), 10),
|
|
frame = CGRectCreateCopy([_segments[segment] frame]);
|
|
|
|
if (segment === 0)
|
|
{
|
|
frame.origin.x += contentInset.left;
|
|
frame.size.width -= contentInset.left;
|
|
}
|
|
|
|
if (segment === _segments.length - 1)
|
|
frame.size.width = CGRectGetWidth([self bounds]) - contentInset.right - frame.origin.x;
|
|
|
|
return frame;
|
|
}
|
|
else if (aName.indexOf("divider-bezel") === 0)
|
|
{
|
|
var segment = parseInt(aName.substring("divider-bezel-".length), 10),
|
|
width = [self widthForSegment:segment],
|
|
left = [self _leftOffsetForSegment:segment],
|
|
thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
|
|
|
|
return CGRectMake(left + width, bezelInset.top, thickness, height);
|
|
}
|
|
else if (aName.indexOf("segment-content") === 0)
|
|
{
|
|
var segment = parseInt(aName.substring("segment-content-".length), 10);
|
|
|
|
return [self contentFrameForSegment:segment];
|
|
}
|
|
|
|
return [super rectForEphemeralSubviewNamed:aName];
|
|
}
|
|
|
|
- (CPView)createEphemeralSubviewNamed:(CPString)aName
|
|
{
|
|
if ([aName hasPrefix:@"segment-content"])
|
|
return [[_CPImageAndTextView alloc] initWithFrame:_CGRectMakeZero()];
|
|
|
|
return [[CPView alloc] initWithFrame:_CGRectMakeZero()];
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
if (_segments.length <= 0)
|
|
return;
|
|
|
|
var themeState = _themeStates[0];
|
|
|
|
themeState |= _themeState & CPThemeStateDisabled;
|
|
|
|
var leftCapColor = [self valueForThemeAttribute:@"left-segment-bezel-color"
|
|
inState:themeState],
|
|
|
|
leftBezelView = [self layoutEphemeralSubviewNamed:@"left-segment-bezel"
|
|
positioned:CPWindowBelow
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
|
|
[leftBezelView setBackgroundColor:leftCapColor];
|
|
|
|
var themeState = _themeStates[_themeStates.length - 1];
|
|
|
|
themeState |= _themeState & CPThemeStateDisabled;
|
|
|
|
var rightCapColor = [self valueForThemeAttribute:@"right-segment-bezel-color"
|
|
inState:themeState],
|
|
|
|
rightBezelView = [self layoutEphemeralSubviewNamed:@"right-segment-bezel"
|
|
positioned:CPWindowBelow
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
|
|
[rightBezelView setBackgroundColor:rightCapColor];
|
|
|
|
for (var i = 0, count = _themeStates.length; i < count; i++)
|
|
{
|
|
var themeState = _themeStates[i];
|
|
|
|
themeState |= _themeState & CPThemeStateDisabled;
|
|
|
|
var bezelColor = [self valueForThemeAttribute:@"center-segment-bezel-color"
|
|
inState:themeState],
|
|
|
|
bezelView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
|
|
positioned:CPWindowBelow
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
|
|
[bezelView setBackgroundColor:bezelColor];
|
|
|
|
|
|
// layout image/title views
|
|
var segment = _segments[i],
|
|
contentView = [self layoutEphemeralSubviewNamed:@"segment-content-" + i
|
|
positioned:CPWindowAbove
|
|
relativeToEphemeralSubviewNamed:@"segment-bezel-" + i];
|
|
|
|
[contentView setText:[segment label]];
|
|
[contentView setImage:[segment image]];
|
|
|
|
[contentView setFont:[self valueForThemeAttribute:@"font" inState:themeState]];
|
|
[contentView setTextColor:[self valueForThemeAttribute:@"text-color" inState:themeState]];
|
|
[contentView setAlignment:[self valueForThemeAttribute:@"alignment" inState:themeState]];
|
|
[contentView setVerticalAlignment:[self valueForThemeAttribute:@"vertical-alignment" inState:themeState]];
|
|
[contentView setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode" inState:themeState]];
|
|
[contentView setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color" inState:themeState]];
|
|
[contentView setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset" inState:themeState]];
|
|
[contentView setImageScaling:[self valueForThemeAttribute:@"image-scaling" inState:themeState]];
|
|
|
|
if ([segment image] && [segment label])
|
|
[contentView setImagePosition:[self valueForThemeAttribute:@"image-position" inState:themeState]];
|
|
else if ([segment image])
|
|
[contentView setImagePosition:CPImageOnly];
|
|
|
|
if (i == count - 1)
|
|
continue;
|
|
|
|
var borderState = _themeStates[i] | _themeStates[i + 1];
|
|
|
|
borderState = (borderState & CPThemeStateSelected & ~CPThemeStateHighlighted) ? CPThemeStateSelected : CPThemeStateNormal;
|
|
|
|
borderState |= _themeState & CPThemeStateDisabled;
|
|
|
|
var borderColor = [self valueForThemeAttribute:@"divider-bezel-color"
|
|
inState:borderState],
|
|
|
|
borderView = [self layoutEphemeralSubviewNamed:"divider-bezel-" + i
|
|
positioned:CPWindowBelow
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
|
|
[borderView setBackgroundColor:borderColor];
|
|
}
|
|
}
|
|
|
|
|
|
/*!
|
|
Draws the specified segment
|
|
@param aSegment the segment to draw
|
|
@param shouldHighlight \c YES highlights the bezel
|
|
*/
|
|
- (void)drawSegment:(int)aSegment highlight:(BOOL)shouldHighlight
|
|
{
|
|
}
|
|
|
|
- (void)tileWithChangedSegment:(unsigned)aSegment
|
|
{
|
|
if (aSegment >= _segments.length)
|
|
return;
|
|
|
|
var segment = _segments[aSegment],
|
|
segmentWidth = [segment width],
|
|
themeState = _themeStates[aSegment] | (_themeState & CPThemeStateDisabled),
|
|
contentInset = [self valueForThemeAttribute:@"content-inset" inState:themeState],
|
|
font = [self font];
|
|
|
|
if (!segmentWidth)
|
|
{
|
|
if ([segment image] && [segment label])
|
|
segmentWidth = [[segment label] sizeWithFont:font].width + [[segment image] size].width + contentInset.left + contentInset.right;
|
|
else if (segment.image)
|
|
segmentWidth = [[segment image] size].width + contentInset.left + contentInset.right;
|
|
else if (segment.label)
|
|
segmentWidth = [[segment label] sizeWithFont:font].width + contentInset.left + contentInset.right;
|
|
else
|
|
segmentWidth = 0.0;
|
|
}
|
|
|
|
var delta = segmentWidth - CGRectGetWidth([segment frame]);
|
|
|
|
if (!delta)
|
|
{
|
|
[self setNeedsLayout];
|
|
[self setNeedsDisplay:YES];
|
|
|
|
return;
|
|
}
|
|
|
|
// Update control size
|
|
var frame = [self frame];
|
|
|
|
[self setFrameSize:CGSizeMake(CGRectGetWidth(frame) + delta, CGRectGetHeight(frame))];
|
|
|
|
// Update segment width
|
|
[segment setWidth:segmentWidth];
|
|
[segment setFrame:[self frameForSegment:aSegment]];
|
|
|
|
// Update following segments widths
|
|
var index = aSegment + 1;
|
|
|
|
for (; index < _segments.length; ++index)
|
|
{
|
|
[_segments[index] frame].origin.x += delta;
|
|
|
|
[self drawSegmentBezel:index highlight:NO];
|
|
[self drawSegment:index highlight:NO];
|
|
}
|
|
|
|
[self drawSegmentBezel:aSegment highlight:NO];
|
|
[self drawSegment:aSegment highlight:NO];
|
|
|
|
[self setNeedsLayout];
|
|
[self setNeedsDisplay:YES];
|
|
}
|
|
|
|
/*!
|
|
Returns the bounding rectangle for the specified segment.
|
|
@param aSegment the segment to get the rectangle for
|
|
*/
|
|
- (CGRect)frameForSegment:(unsigned)aSegment
|
|
{
|
|
return [self bezelFrameForSegment:aSegment];
|
|
}
|
|
|
|
- (CGRect)bezelFrameForSegment:(unsigned)aSegment
|
|
{
|
|
var height = [self currentValueForThemeAttribute:@"default-height"],
|
|
bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"],
|
|
width = [self widthForSegment:aSegment],
|
|
left = [self _leftOffsetForSegment:aSegment];
|
|
|
|
return CGRectMake(left, bezelInset.top, width, height);
|
|
}
|
|
|
|
- (CGRect)contentFrameForSegment:(unsigned)aSegment
|
|
{
|
|
var height = [self currentValueForThemeAttribute:@"default-height"],
|
|
contentInset = [self currentValueForThemeAttribute:@"content-inset"],
|
|
width = [self widthForSegment:aSegment],
|
|
left = [self _leftOffsetForSegment:aSegment];
|
|
|
|
return CGRectMake(left + contentInset.left, contentInset.top, width - contentInset.left - contentInset.right, height - contentInset.top - contentInset.bottom);
|
|
}
|
|
|
|
/*!
|
|
Returns the segment that is hit by the specified point.
|
|
@param aPoint the point to test for a segment hit
|
|
@return the intersecting segment
|
|
*/
|
|
- (unsigned)testSegment:(CGPoint)aPoint
|
|
{
|
|
var location = [self convertPoint:aPoint fromView:nil],
|
|
count = _segments.length;
|
|
|
|
while (count--)
|
|
if (CGRectContainsPoint([_segments[count] frame], aPoint))
|
|
return count;
|
|
|
|
if (_segments.length)
|
|
{
|
|
var adjustedLastFrame = CGRectCreateCopy([_segments[_segments.length - 1] frame]);
|
|
adjustedLastFrame.size.width = CGRectGetWidth([self bounds]) - adjustedLastFrame.origin.x;
|
|
|
|
if (CGRectContainsPoint(adjustedLastFrame, aPoint))
|
|
return _segments.length - 1;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
- (void)mouseDown:(CPEvent)anEvent
|
|
{
|
|
if (![self isEnabled])
|
|
return;
|
|
|
|
[self trackSegment:anEvent];
|
|
}
|
|
|
|
// FIXME: this should be fixed way up in cpbutton/cpcontrol.
|
|
- (void)mouseUp:(CPEvent)anEvent
|
|
{
|
|
}
|
|
|
|
/*!
|
|
Handles events for the segment
|
|
@param anEvent the event to handle
|
|
*/
|
|
- (void)trackSegment:(CPEvent)anEvent
|
|
{
|
|
var type = [anEvent type],
|
|
location = [self convertPoint:[anEvent locationInWindow] fromView:nil];
|
|
|
|
if (type == CPLeftMouseUp)
|
|
{
|
|
if (_trackingSegment == -1)
|
|
return;
|
|
|
|
if (_trackingSegment === [self testSegment:location])
|
|
{
|
|
if (_trackingMode == CPSegmentSwitchTrackingSelectAny)
|
|
{
|
|
[self setSelected:![self isSelectedForSegment:_trackingSegment] forSegment:_trackingSegment];
|
|
|
|
// With ANY, _selectedSegment means last pressed.
|
|
_selectedSegment = _trackingSegment;
|
|
}
|
|
else
|
|
[self setSelected:YES forSegment:_trackingSegment];
|
|
|
|
[self sendAction:[self action] to:[self target]];
|
|
|
|
if (_trackingMode == CPSegmentSwitchTrackingMomentary)
|
|
{
|
|
[self setSelected:NO forSegment:_trackingSegment];
|
|
|
|
_selectedSegment = -1;
|
|
}
|
|
}
|
|
|
|
[self drawSegmentBezel:_trackingSegment highlight:NO];
|
|
|
|
_trackingSegment = -1;
|
|
|
|
return;
|
|
}
|
|
|
|
if (type == CPLeftMouseDown)
|
|
{
|
|
var trackingSegment = [self testSegment:location];
|
|
if (trackingSegment > -1 && [self isEnabledForSegment:trackingSegment])
|
|
{
|
|
_trackingHighlighted = YES;
|
|
_trackingSegment = trackingSegment;
|
|
[self drawSegmentBezel:_trackingSegment highlight:YES];
|
|
}
|
|
}
|
|
|
|
else if (type == CPLeftMouseDragged)
|
|
{
|
|
if (_trackingSegment == -1)
|
|
return;
|
|
|
|
var highlighted = [self testSegment:location] === _trackingSegment;
|
|
if (highlighted != _trackingHighlighted)
|
|
{
|
|
_trackingHighlighted = highlighted;
|
|
|
|
[self drawSegmentBezel:_trackingSegment highlight:_trackingHighlighted];
|
|
}
|
|
}
|
|
|
|
[CPApp setTarget:self selector:@selector(trackSegment:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
|
|
}
|
|
|
|
- (void)setFont:(CPFont)aFont
|
|
{
|
|
[super setFont:aFont];
|
|
|
|
[self tileWithChangedSegment:0];
|
|
}
|
|
|
|
@end
|
|
|
|
var CPSegmentedControlSegmentsKey = "CPSegmentedControlSegmentsKey",
|
|
CPSegmentedControlSelectedKey = "CPSegmentedControlSelectedKey",
|
|
CPSegmentedControlSegmentStyleKey = "CPSegmentedControlSegmentStyleKey",
|
|
CPSegmentedControlTrackingModeKey = "CPSegmentedControlTrackingModeKey";
|
|
|
|
@implementation CPSegmentedControl (CPCoding)
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
self = [super initWithCoder:aCoder];
|
|
|
|
if (self)
|
|
{
|
|
var frame = [self frame],
|
|
originalWidth = frame.size.width;
|
|
|
|
frame.size.width = 0;
|
|
|
|
[self setFrame:frame];
|
|
|
|
_segments = [aCoder decodeObjectForKey:CPSegmentedControlSegmentsKey];
|
|
_segmentStyle = [aCoder decodeIntForKey:CPSegmentedControlSegmentStyleKey];
|
|
_themeStates = [];
|
|
|
|
if ([aCoder containsValueForKey:CPSegmentedControlSelectedKey])
|
|
_selectedSegment = [aCoder decodeIntForKey:CPSegmentedControlSelectedKey];
|
|
else
|
|
_selectedSegment = -1;
|
|
|
|
if ([aCoder containsValueForKey:CPSegmentedControlTrackingModeKey])
|
|
_trackingMode = [aCoder decodeIntForKey:CPSegmentedControlTrackingModeKey];
|
|
else
|
|
_trackingMode = CPSegmentSwitchTrackingSelectOne;
|
|
|
|
// HACK
|
|
|
|
for (var i = 0; i < _segments.length; i++)
|
|
{
|
|
_themeStates[i] = [_segments[i] selected] ? CPThemeStateSelected : CPThemeStateNormal;
|
|
[self tileWithChangedSegment:i];
|
|
}
|
|
|
|
var difference = MAX(originalWidth - [self frame].size.width, 0.0),
|
|
remainingWidth = FLOOR(difference / _segments.length);
|
|
|
|
for (var i = 0; i < _segments.length; i++)
|
|
[self setWidth:[_segments[i] width] + remainingWidth forSegment:i];
|
|
|
|
[self tileWithChangedSegment:0];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[super encodeWithCoder:aCoder];
|
|
|
|
[aCoder encodeObject:_segments forKey:CPSegmentedControlSegmentsKey];
|
|
[aCoder encodeInt:_selectedSegment forKey:CPSegmentedControlSelectedKey];
|
|
[aCoder encodeInt:_segmentStyle forKey:CPSegmentedControlSegmentStyleKey];
|
|
[aCoder encodeInt:_trackingMode forKey:CPSegmentedControlTrackingModeKey];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation _CPSegmentItem : CPObject
|
|
{
|
|
CPImage image @accessors;
|
|
CPString label @accessors;
|
|
CPMenu menu @accessors;
|
|
BOOL selected @accessors;
|
|
BOOL enabled @accessors;
|
|
int tag @accessors;
|
|
int width @accessors;
|
|
|
|
CGRect frame @accessors;
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
image = nil;
|
|
label = @"";
|
|
menu = nil;
|
|
selected = NO;
|
|
enabled = YES;
|
|
tag = -1;
|
|
width = 0;
|
|
|
|
frame = CGRectMakeZero();
|
|
}
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
var CPSegmentItemImageKey = "CPSegmentItemImageKey",
|
|
CPSegmentItemLabelKey = "CPSegmentItemLabelKey",
|
|
CPSegmentItemMenuKey = "CPSegmentItemMenuKey",
|
|
CPSegmentItemSelectedKey = "CPSegmentItemSelectedKey",
|
|
CPSegmentItemEnabledKey = "CPSegmentItemEnabledKey",
|
|
CPSegmentItemTagKey = "CPSegmentItemTagKey",
|
|
CPSegmentItemWidthKey = "CPSegmentItemWidthKey";
|
|
|
|
@implementation _CPSegmentItem (CPCoding)
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
self = [super init];
|
|
|
|
if (self)
|
|
{
|
|
image = [aCoder decodeObjectForKey:CPSegmentItemImageKey];
|
|
label = [aCoder decodeObjectForKey:CPSegmentItemLabelKey];
|
|
menu = [aCoder decodeObjectForKey:CPSegmentItemMenuKey];
|
|
selected = [aCoder decodeBoolForKey:CPSegmentItemSelectedKey];
|
|
enabled = [aCoder decodeBoolForKey:CPSegmentItemEnabledKey];
|
|
tag = [aCoder decodeIntForKey:CPSegmentItemTagKey];
|
|
width = [aCoder decodeFloatForKey:CPSegmentItemWidthKey];
|
|
|
|
frame = CGRectMakeZero();
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[aCoder encodeObject:image forKey:CPSegmentItemImageKey];
|
|
[aCoder encodeObject:label forKey:CPSegmentItemLabelKey];
|
|
[aCoder encodeObject:menu forKey:CPSegmentItemMenuKey];
|
|
[aCoder encodeBool:selected forKey:CPSegmentItemSelectedKey];
|
|
[aCoder encodeBool:enabled forKey:CPSegmentItemEnabledKey];
|
|
[aCoder encodeInt:tag forKey:CPSegmentItemTagKey];
|
|
[aCoder encodeFloat:width forKey:CPSegmentItemWidthKey];
|
|
}
|
|
|
|
@end
|