/* * 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 @import "CPControl.j" #include "CoreGraphics/CGGeometry.h" 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)themeClass { 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]); 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; [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].width = 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 { var segment = _segments[aSegment]; segment.image = 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 { var segment = _segments[aSegment]; _segments[aSegment].label = 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].menu = 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.selected = 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].selected = 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 { 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 !(_themeStates[aSegment] & CPThemeStateDisabled) } /*! 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].tag = 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.substring(0, "segment-content".length) == "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]; var 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]; var rightBezelView = [self layoutEphemeralSubviewNamed:@"right-segment-bezel" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil]; [rightBezelView setBackgroundColor:rightCapColor]; for (var i=0, count = _themeStates.length; i= _segments.length) return; var segment = _segments[aSegment], segmentWidth = segment.width, themeState = _themeStates[aSegment] | (_themeState & CPThemeStateDisabled), contentInset = [self valueForThemeAttribute:@"content-inset" inState:themeState], font = [self valueForThemeAttribute:@"font" inState:themeState]; 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) return; // Update Contorl Size var frame = [self frame]; [self setFrameSize:CGSizeMake(CGRectGetWidth(frame) + delta, CGRectGetHeight(frame))]; // Update Segment Width segment.width = segmentWidth; segment.frame = [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; CPString label; CPMenu menu; BOOL selected; BOOL enabled; int tag; int width; CGRect frame; } - (id)init { if (self = [super init]) { image = nil; label = @""; menu = nil; selected = NO; enabled = NO; tag = 0; 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