mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-09-05 10:23:39 +00:00
1763 lines
48 KiB
Plaintext
1763 lines
48 KiB
Plaintext
/*
|
|
* CPButtonBar.j
|
|
* AppKit
|
|
*
|
|
* Created by Francisco Tolmasky.
|
|
* Copyright 2009, 280 North, Inc.
|
|
*
|
|
* Adapted by Didier Korthoudt
|
|
* Copyright 2019, Cappuccino Project.
|
|
*
|
|
* 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 "CPView.j"
|
|
@import "CPWindow_Constants.j"
|
|
@import "CPButton.j"
|
|
@import "CPPopUpButton.j"
|
|
@import "CPSearchField.j"
|
|
|
|
@class CPSplitView
|
|
|
|
@global CPPopUpButtonStatePullsDown
|
|
@global CPKeyValueChangeOldKey
|
|
@global CPKeyValueChangeNewKey
|
|
@global CPKeyValueObservingOptionNew
|
|
@global CPKeyValueObservingOptionOld
|
|
|
|
var SharedDummyButtonBar = nil;
|
|
|
|
@typedef CPButtonBarStyle
|
|
CPButtonBarLegacyStyle = 1;
|
|
CPButtonBarModernStyle = 2;
|
|
CPButtonBarSmoothStyle = 3;
|
|
|
|
@typedef CPTemplateImage
|
|
CPAddTemplateImage = @"CPAddTemplate";
|
|
CPRemoveTemplateImage = @"CPRemoveTemplate";
|
|
CPActionTemplateImage = @"CPActionTemplate";
|
|
|
|
var _templateImageMap = @{
|
|
CPAddTemplateImage: @"button-image-plus",
|
|
CPRemoveTemplateImage: @"button-image-minus",
|
|
CPActionTemplateImage: @"button-image-action"
|
|
};
|
|
|
|
var CPButtonBarPopulateButtonBarSelector = 1 << 1;
|
|
|
|
@protocol CPButtonBarDelegate <CPObject>
|
|
|
|
@optional
|
|
- (void)populateButtonBar:(CPButtonBar)buttonBar;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation CPButtonBar : CPView
|
|
{
|
|
CPArray _buttons;
|
|
CPMutableArray _dividers;
|
|
CPMutableArray _flexibles;
|
|
CPButtonBarStyle _style;
|
|
|
|
BOOL _buttonsAreBordered;
|
|
BOOL _drawsSeparator;
|
|
BOOL _isTransparent;
|
|
BOOL _needsToLoadThemeValues;
|
|
BOOL _needsToComputeNeededSpace;
|
|
CGSize _buttonsSize;
|
|
int _spacing;
|
|
CPInteger _buttonVerticalOffset;
|
|
CPColor _dividerColor;
|
|
CPColor _bezelColor;
|
|
int _flexibleSpacesCount;
|
|
int _neededWidth;
|
|
int _flexibleCount;
|
|
int _flexibleDelta;
|
|
CPMutableArray _flexibleDeltas;
|
|
CGSize _resizeControlViewSize;
|
|
CGInset _resizeControlViewInset;
|
|
CPColor _resizeControlViewBackground;
|
|
|
|
BOOL _automaticResizeControl;
|
|
BOOL _hasLeftResizeControl;
|
|
BOOL _hasRightResizeControl;
|
|
|
|
id <CPButtonBarDelegate> _delegate;
|
|
unsigned _delegateSelectors;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Class methods
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return @"button-bar";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"resize-control-inset": CGInsetMake(0.0, 0.0, 0.0, 0.0),
|
|
@"resize-control-size": CGSizeMakeZero(),
|
|
@"resize-control-color": [CPNull null],
|
|
@"bezel-color": [CPNull null],
|
|
@"button-bezel-color": [CPNull null],
|
|
@"button-text-color": [CPNull null],
|
|
@"button-image-plus": [CPNull null],
|
|
@"button-image-minus": [CPNull null],
|
|
@"button-image-action": [CPNull null],
|
|
|
|
// For compatibility with Aristo2
|
|
@"button-vertical-offset": 1,
|
|
@"bordered-buttons": YES,
|
|
@"button-size": CGSizeMake(35.0, 25.0),
|
|
@"spacing-size": CGSizeMake(0.0, 25.0),
|
|
@"divider-color": [CPNull null],
|
|
@"draws-separator": YES,
|
|
@"is-transparent": NO,
|
|
@"min-size": CGSizeMake(0, 0),
|
|
@"max-size": CGSizeMake(-1, -1),
|
|
@"auto-resize-control": YES
|
|
};
|
|
}
|
|
|
|
+ (CPButtonBar)_sharedDummyButtonBar
|
|
{
|
|
if (!SharedDummyButtonBar)
|
|
{
|
|
SharedDummyButtonBar = [[CPButtonBar alloc] initWithFrame:CGRectMakeZero()];
|
|
|
|
[SharedDummyButtonBar setTheme:[CPTheme defaultTheme]];
|
|
}
|
|
|
|
return SharedDummyButtonBar;
|
|
}
|
|
|
|
+ (id)plusButton
|
|
{
|
|
CPLog.warn("[CPButtonBar plusButton] is deprecated, use -(id)plusButton instance method instead.");
|
|
|
|
return [[CPButtonBar _sharedDummyButtonBar] plusButton];
|
|
}
|
|
|
|
+ (id)minusButton
|
|
{
|
|
CPLog.warn("[CPButtonBar minusButton] is deprecated, use -(id)minusButton instance method instead.");
|
|
|
|
return [[CPButtonBar _sharedDummyButtonBar] minusButton];
|
|
}
|
|
|
|
+ (id)actionPopupButton
|
|
{
|
|
CPLog.warn("[CPButtonBar actionPopupButton] is deprecated, use -(id)actionPopupButton instance method instead.");
|
|
|
|
return [[CPButtonBar _sharedDummyButtonBar] actionPopupButton];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Buttons constructors
|
|
|
|
- (id)buttonWithImage:(CPImage)image alternateImage:(CPImage)alternateImage
|
|
{
|
|
return [[_CPButtonBarButton alloc] initWithImage:image alternateImage:alternateImage bordered:_buttonsAreBordered];
|
|
}
|
|
|
|
- (id)buttonWithTemplateImage:(CPTemplateImage)templateImage
|
|
{
|
|
var attributeName = [_templateImageMap valueForKey:templateImage],
|
|
image = [self valueForThemeAttribute:attributeName inState:CPThemeStateNormal],
|
|
alternateImage = [self valueForThemeAttribute:attributeName inState:CPThemeStateHighlighted];
|
|
|
|
return [self buttonWithImage:image alternateImage:alternateImage];
|
|
}
|
|
|
|
- (id)buttonWithMaterialIconNamed:(CPString)iconName
|
|
{
|
|
var image = [CPImage imageWithMaterialIconNamed:iconName size:CGSizeMake(16,16) color:[CPColor colorWithCSSString:@"rgba(0,0,0,0.75)"]],
|
|
alternateImage = [CPImage imageWithMaterialIconNamed:iconName size:CGSizeMake(16,16) color:[CPColor colorWithCSSString:@"rgba(0,0,0,1.00)"]];
|
|
|
|
return [self buttonWithImage:image alternateImage:alternateImage];
|
|
}
|
|
|
|
- (id)pulldownButtonWithImage:(CPImage)image alternateImage:(CPImage)alternateImage
|
|
{
|
|
return [[_CPButtonBarPopUpButton alloc] initWithImage:image alternateImage:alternateImage];
|
|
}
|
|
|
|
- (id)pulldownButtonWithTemplateImage:(CPTemplateImage)templateImage
|
|
{
|
|
var attributeName = [_templateImageMap valueForKey:templateImage],
|
|
image = [self valueForThemeAttribute:attributeName inState:CPThemeStateNormal],
|
|
alternateImage = [self valueForThemeAttribute:attributeName inState:CPThemeStateHighlighted];
|
|
|
|
return [self pulldownButtonWithImage:image alternateImage:alternateImage];
|
|
}
|
|
|
|
- (id)pulldownButtonWithMaterialIconNamed:(CPString)iconName
|
|
{
|
|
var image = [CPImage imageWithMaterialIconNamed:iconName size:CGSizeMake(16,16) color:[CPColor colorWithCSSString:@"rgba(0,0,0,0.75)"]],
|
|
alternateImage = [CPImage imageWithMaterialIconNamed:iconName size:CGSizeMake(16,16) color:[CPColor colorWithCSSString:@"rgba(0,0,0,1.00)"]];
|
|
|
|
return [self pulldownButtonWithImage:image alternateImage:alternateImage];
|
|
}
|
|
|
|
- (id)pulldownButtonWithTitle:(CPString)aTitle
|
|
{
|
|
return [[_CPButtonBarAdaptativePullDownButton alloc] initWithTitle:aTitle];
|
|
}
|
|
|
|
- (id)popUpButton
|
|
{
|
|
return [[_CPButtonBarAdaptativePopUpButton alloc] initWithFrame:CGRectMakeZero()];
|
|
}
|
|
|
|
- (id)searchFieldWithPlaceholder:(CPString)placeholder minWidth:(int)minWidth maxWidth:(int)maxWidth
|
|
{
|
|
return [[_CPButtonBarSearchField alloc] initWithPlaceholder:placeholder minWidth:minWidth maxWidth:maxWidth];
|
|
}
|
|
|
|
- (id)separator
|
|
{
|
|
return [[_CPButtonBarSeparator alloc] initWithBordered:_drawsSeparator];
|
|
}
|
|
|
|
- (id)flexibleSpace
|
|
{
|
|
return [[_CPButtonBarFlexibleSpace alloc] initWithFrame:CGRectMakeZero()];
|
|
}
|
|
|
|
- (id)labelWithTitle:(CPString)aTitle
|
|
{
|
|
return [[_CPButtonBarLabel alloc] initWithTitle:aTitle];
|
|
}
|
|
|
|
- (id)labelWithTitle:(CPString)aTitle width:(int)aWidth
|
|
{
|
|
return [[_CPButtonBarLabel alloc] initWithTitle:aTitle width:aWidth];
|
|
}
|
|
|
|
- (id)adaptativeLabelWithTitle:(CPString)aTitle
|
|
{
|
|
return [[_CPButtonBarAdaptativeLabel alloc] initWithTitle:aTitle];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Backward compatibility
|
|
|
|
- (id)plusButton
|
|
{
|
|
return [self buttonWithTemplateImage:CPAddTemplateImage];
|
|
}
|
|
|
|
- (id)minusButton
|
|
{
|
|
return [self buttonWithTemplateImage:CPRemoveTemplateImage];
|
|
}
|
|
|
|
- (id)actionPopupButton
|
|
{
|
|
return [self pulldownButtonWithTemplateImage:CPActionTemplateImage];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Initialization
|
|
|
|
- (id)initWithFrame:(CGRect)aFrame
|
|
{
|
|
self = [super initWithFrame:aFrame];
|
|
|
|
if (self)
|
|
{
|
|
_buttons = @[];
|
|
_dividers = @[];
|
|
_flexibles = @[];
|
|
_flexibleSpacesCount = 0;
|
|
_neededWidth = 0;
|
|
_flexibleCount = 0;
|
|
_flexibleDelta = 0;
|
|
_flexibleDeltas = @[];
|
|
_hasLeftResizeControl = NO;
|
|
_hasRightResizeControl = NO;
|
|
|
|
// Get default layout attributes
|
|
[self setButtonsAreBordered: [self currentValueForThemeAttribute:@"bordered-buttons"]];
|
|
[self setDrawsSeparator: [self currentValueForThemeAttribute:@"draws-separator"]];
|
|
[self setIsTransparent: [self currentValueForThemeAttribute:@"is-transparent"]];
|
|
[self setAutomaticResizeControl:[self currentValueForThemeAttribute:@"auto-resize-control"]]
|
|
|
|
// Adapt the height of the button bar if needed
|
|
var minSize = [self currentValueForThemeAttribute:@"min-size"],
|
|
maxSize = [self currentValueForThemeAttribute:@"max-size"],
|
|
frameSize = aFrame.size;
|
|
|
|
if (minSize.height > 0)
|
|
{
|
|
frameSize.height = MAX(minSize.height, frameSize.height);
|
|
|
|
if (maxSize.height > 0)
|
|
frameSize.height = MIN(maxSize.height, frameSize.height);
|
|
}
|
|
|
|
[self setFrameSize:frameSize];
|
|
[self setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
|
|
|
|
// This is needed when compiling themes
|
|
[self _loadThemeValues];
|
|
|
|
[self setNeedsLayout:YES];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_loadThemeValues
|
|
{
|
|
_bezelColor = [self currentValueForThemeAttribute:@"bezel-color"];
|
|
_spacing = [self currentValueForThemeAttribute:@"spacing-size"].width;
|
|
_dividerColor = [self currentValueForThemeAttribute:@"divider-color"];
|
|
_buttonVerticalOffset = [self currentValueForThemeAttribute:@"button-vertical-offset"];
|
|
_resizeControlViewBackground = [self currentValueForThemeAttribute:@"resize-control-color"];
|
|
_resizeControlViewSize = [self currentValueForThemeAttribute:@"resize-control-size"];
|
|
_resizeControlViewInset = [self currentValueForThemeAttribute:@"resize-control-inset"];
|
|
|
|
[self setBackgroundColor:_bezelColor];
|
|
|
|
_needsToLoadThemeValues = NO;
|
|
_needsToComputeNeededSpace = YES;
|
|
}
|
|
|
|
- (void)awakeFromCib
|
|
{
|
|
[super awakeFromCib];
|
|
[self _connectToSplitView];
|
|
}
|
|
|
|
- (void)_connectToSplitView
|
|
{
|
|
var view = [self superview],
|
|
subview = self;
|
|
|
|
while (view)
|
|
{
|
|
if ([view isKindOfClass:[CPSplitView class]])
|
|
{
|
|
// // ATTENTION !
|
|
// // If the button bar is created via IB, at this moment, the split view may not be ready : subviews are OK but
|
|
// // not arrangedSubviews. We can then assume that all subviews will be arranged subviews (as subviews declared
|
|
// // in IB are, in fact, arranged subviews).
|
|
// // BUT if the split view is ready, when must work on arranged subviews.
|
|
// //
|
|
// // To determine if the split view is ready, we have to make an hypothesis :
|
|
// // if the number of subviews > 0 and the number of arranged subviews = 0, then the split view is not ready
|
|
// //
|
|
// // This hypothesis could be false if the split view doesn't arrange all subviews and if all subviews are not
|
|
// // arranged subviews. BUT this would mean that the split view has no real panes. And this should never happen
|
|
// // in real life.
|
|
//
|
|
// var arrangedSubviews = [view arrangedSubviews],
|
|
// arrangedSubviewsCount = [arrangedSubviews count],
|
|
// subviews = [view subviews],
|
|
// subviewsCount = [subviews count],
|
|
// splitViewIsNotReady = (subviewsCount > 0) && (arrangedSubviewsCount == 0),
|
|
// viewIndex = [(splitViewIsNotReady ? subviews : arrangedSubviews) indexOfObject:subview],
|
|
// dividerIndex = (viewIndex < (splitViewIsNotReady ? subviewsCount : arrangedSubviewsCount) - 1) ? viewIndex : MAX(viewIndex - 1, 0);
|
|
//
|
|
// [view setButtonBar:self forDividerAtIndex:dividerIndex addResizeControl:_automaticResizeControl];
|
|
|
|
[view attachButtonBar:self];
|
|
break;
|
|
}
|
|
|
|
subview = view;
|
|
view = [view superview];
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Properties
|
|
|
|
- (void)setStyle:(CPButtonBarStyle)style
|
|
{
|
|
if (_style == style)
|
|
return;
|
|
|
|
switch (style) {
|
|
case CPButtonBarLegacyStyle:
|
|
[self setButtonsAreBordered:YES];
|
|
[self setDrawsSeparator:YES];
|
|
[self setIsTransparent:NO];
|
|
break;
|
|
|
|
case CPButtonBarModernStyle:
|
|
[self setButtonsAreBordered:NO];
|
|
[self setDrawsSeparator:YES];
|
|
[self setIsTransparent:NO];
|
|
break;
|
|
|
|
case CPButtonBarSmoothStyle:
|
|
[self setButtonsAreBordered:NO];
|
|
[self setDrawsSeparator:NO];
|
|
[self setIsTransparent:YES];
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
_style = style;
|
|
|
|
// If we already have buttons (surely a live style change), we have to
|
|
// reload them in order to have correct behaviors
|
|
if ([_buttons count] > 0)
|
|
{
|
|
// Clear existing buttons before asking delegate to re-populate,
|
|
// otherwise we get duplicates (A, B -> A, B, A, B).
|
|
[self setButtons:@[]];
|
|
[self _sendDelegatePopulateButtonBar];
|
|
}
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (void)setButtons:(CPArray)buttons
|
|
{
|
|
for (var i = [_buttons count] - 1; i >= 0; i--)
|
|
{
|
|
[_buttons[i] removeFromSuperview];
|
|
[_buttons[i] removeObserver:self forKeyPath:@"hidden"];
|
|
[_dividers[i] removeFromSuperview];
|
|
}
|
|
|
|
_buttons = @[];
|
|
_dividers = @[];
|
|
_flexibles = @[];
|
|
_flexibleSpacesCount = 0;
|
|
|
|
for (var i = 0, count = [buttons count]; i < count; i++)
|
|
[self addButton:buttons[i]];
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (void)addButton:(id)button
|
|
{
|
|
if (![button respondsToSelector:@selector(isButtonBarItem)])
|
|
{
|
|
CPLog.error("CPButtonBar: Only regular CPButtonBar items are allowed (trying to add "+[button class]+")");
|
|
return;
|
|
}
|
|
|
|
[_buttons addObject:button];
|
|
[button addObserver:self forKeyPath:@"hidden" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil];
|
|
[button setBordered:_buttonsAreBordered];
|
|
[self addSubview:button];
|
|
|
|
// Add a corresponding divider
|
|
var newDivider = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
|
|
|
[_dividers addObject:newDivider];
|
|
[newDivider setBackgroundColor:_dividerColor];
|
|
[self addSubview:newDivider];
|
|
|
|
// Is this a flexible space ?
|
|
if ([button isKindOfClass:_CPButtonBarFlexibleSpace])
|
|
_flexibleSpacesCount++;
|
|
|
|
// Is this a flexible item (other than space)
|
|
else if ([button isFlexible])
|
|
[_flexibles addObject:button];
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (CPArray)buttons
|
|
{
|
|
return [CPArray arrayWithArray:_buttons];
|
|
}
|
|
|
|
- (void)setButtonsAreBordered:(BOOL)shouldBeBordered
|
|
{
|
|
if (_buttonsAreBordered === shouldBeBordered)
|
|
return;
|
|
|
|
_buttonsAreBordered = !!shouldBeBordered;
|
|
|
|
if (_buttonsAreBordered)
|
|
[self setThemeState:CPThemeStateBezeled];
|
|
else
|
|
[self unsetThemeState:CPThemeStateBezeled];
|
|
|
|
[self _loadThemeValues];
|
|
|
|
// Update buttons
|
|
for (var i = 0, count = [_buttons count]; i < count; i++)
|
|
{
|
|
[_buttons[i] setBordered:_buttonsAreBordered];
|
|
[_buttons[i] _initThemedValues];
|
|
}
|
|
|
|
var useDividers = _buttonsAreBordered && !!_dividerColor;
|
|
|
|
// Adapt dividers
|
|
for (var i = 0, count = [_dividers count]; i < count; i++)
|
|
{
|
|
[_dividers[i] setBackgroundColor:_dividerColor];
|
|
[_dividers[i] setHidden:!useDividers];
|
|
}
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (BOOL)buttonsAreBordered
|
|
{
|
|
return _buttonsAreBordered;
|
|
}
|
|
|
|
- (void)setButtonsSize:(CGSize)aSize
|
|
{
|
|
if (CGSizeEqualToSize(_buttonsSize, aSize))
|
|
return;
|
|
|
|
CPLog.warn("-setButtonsSize is deprecated, please avoid using it.");
|
|
|
|
_buttonsSize = aSize;
|
|
|
|
// Update existing buttons size
|
|
for (var i = 0, count = [_buttons count]; i < count; i++)
|
|
if ([_buttons[i] isKindOfClass:_CPButtonBarButton])
|
|
[_buttons[i] setFrameSize:_buttonsSize];
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (CGSize)buttonsSize
|
|
{
|
|
return _buttonsSize
|
|
}
|
|
|
|
- (void)setDrawsSeparator:(BOOL)shouldDrawSeparator
|
|
{
|
|
if (_drawsSeparator === shouldDrawSeparator)
|
|
return;
|
|
|
|
_drawsSeparator = !!shouldDrawSeparator;
|
|
|
|
if (_drawsSeparator)
|
|
[self setThemeState:CPThemeStateBordered];
|
|
else
|
|
[self unsetThemeState:CPThemeStateBordered];
|
|
|
|
_needsToLoadThemeValues = YES;
|
|
[self setNeedsLayout:YES];
|
|
}
|
|
|
|
- (BOOL)drawsSeparator
|
|
{
|
|
return _drawsSeparator;
|
|
}
|
|
|
|
- (void)setIsTransparent:(BOOL)shouldBeTransparent
|
|
{
|
|
if (_isTransparent === shouldBeTransparent)
|
|
return;
|
|
|
|
_isTransparent = !!shouldBeTransparent;
|
|
|
|
if (_isTransparent)
|
|
[self setThemeState:CPThemeStateDisabled];
|
|
else
|
|
[self unsetThemeState:CPThemeStateDisabled];
|
|
|
|
_needsToLoadThemeValues = YES;
|
|
[self setNeedsLayout:YES];
|
|
}
|
|
|
|
- (BOOL)isTransparent
|
|
{
|
|
return _isTransparent;
|
|
}
|
|
|
|
- (void)setHasResizeControl:(BOOL)shouldHaveResizeControl
|
|
{
|
|
// For compatibility with previous implementation
|
|
[self setAutomaticResizeControl:shouldHaveResizeControl];
|
|
}
|
|
|
|
- (BOOL)hasResizeControl
|
|
{
|
|
return [self automaticResizeControl];
|
|
}
|
|
|
|
- (void)setResizeControlIsLeftAligned:(BOOL)shouldBeLeftAligned
|
|
{
|
|
// For compatibility with previous implementation
|
|
[self setHasLeftResizeControl:shouldBeLeftAligned];
|
|
[self setHasRightResizeControl:!shouldBeLeftAligned];
|
|
}
|
|
|
|
- (BOOL)resizeControlIsLeftAligned
|
|
{
|
|
return [self hasLeftResizeControl];
|
|
}
|
|
|
|
- (void)setAutomaticResizeControl:(BOOL)shouldAutomaticallyAddResizeControl
|
|
{
|
|
if (_automaticResizeControl === shouldAutomaticallyAddResizeControl)
|
|
return;
|
|
|
|
_automaticResizeControl = !!shouldAutomaticallyAddResizeControl;
|
|
|
|
if (_automaticResizeControl)
|
|
[self _connectToSplitView];
|
|
}
|
|
|
|
- (BOOL)automaticResizeControl
|
|
{
|
|
return _automaticResizeControl;
|
|
}
|
|
|
|
- (void)setHasLeftResizeControl:(BOOL)shouldHaveLeftResizeControl
|
|
{
|
|
if (_hasLeftResizeControl == shouldHaveLeftResizeControl)
|
|
return;
|
|
|
|
_hasLeftResizeControl = shouldHaveLeftResizeControl;
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (BOOL)hasLeftResizeControl
|
|
{
|
|
return _hasLeftResizeControl;
|
|
}
|
|
|
|
- (void)setHasRightResizeControl:(BOOL)shouldHaveRightResizeControl
|
|
{
|
|
if (_hasRightResizeControl == shouldHaveRightResizeControl)
|
|
return;
|
|
|
|
_hasRightResizeControl = shouldHaveRightResizeControl;
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (BOOL)hasRightResizeControl
|
|
{
|
|
return _hasRightResizeControl;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Layout
|
|
|
|
- (CGRect)leftResizeControlFrame
|
|
{
|
|
if (!_hasLeftResizeControl)
|
|
return CGRectMakeZero();
|
|
|
|
return CGRectMake(0, 0, _resizeControlViewSize.width + _resizeControlViewInset.left + _resizeControlViewInset.right, _resizeControlViewSize.height + _resizeControlViewInset.top + _resizeControlViewInset.bottom);
|
|
}
|
|
|
|
- (CGRect)rightResizeControlFrame
|
|
{
|
|
if (!_hasRightResizeControl)
|
|
return CGRectMakeZero();
|
|
|
|
var bounds = [self bounds];
|
|
|
|
return CGRectMake(bounds.size.width - _resizeControlViewSize.width - _resizeControlViewInset.right - _resizeControlViewInset.left, 0, _resizeControlViewSize.width + _resizeControlViewInset.left + _resizeControlViewInset.right, _resizeControlViewSize.height + _resizeControlViewInset.top + _resizeControlViewInset.bottom);
|
|
}
|
|
|
|
- (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
|
|
{
|
|
if (aName === @"left-resize-control-view")
|
|
{
|
|
return CGRectMake(_resizeControlViewInset.left, _resizeControlViewInset.top, _resizeControlViewSize.width, _resizeControlViewSize.height);
|
|
}
|
|
else if (aName === @"right-resize-control-view")
|
|
{
|
|
var bounds = [self bounds];
|
|
|
|
return CGRectMake(bounds.size.width - _resizeControlViewSize.width - _resizeControlViewInset.right, _resizeControlViewInset.top, _resizeControlViewSize.width, _resizeControlViewSize.height);
|
|
}
|
|
|
|
return [super rectForEphemeralSubviewNamed:aName];
|
|
}
|
|
|
|
- (CPView)createEphemeralSubviewNamed:(CPString)aName
|
|
{
|
|
if ((aName === @"left-resize-control-view") || (aName === @"right-resize-control-view"))
|
|
return [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
|
|
|
return [super createEphemeralSubviewNamed:aName];
|
|
}
|
|
|
|
- (void)setNeedsRelayout:(BOOL)needsRelayout
|
|
{
|
|
_needsToComputeNeededSpace = needsRelayout;
|
|
[self setNeedsLayout:needsRelayout];
|
|
}
|
|
|
|
- (void)_computeNeededSpace
|
|
{
|
|
var useDividers = _buttonsAreBordered && !!_dividerColor,
|
|
buttonHorizontalOffset = (useDividers ? 0 : 1),
|
|
bounds = [self bounds],
|
|
frameWidth = bounds.size.width,
|
|
leftResizeRect = _hasLeftResizeControl ? [self leftResizeControlFrame] : CGRectMakeZero(),
|
|
rightResizeRect = _hasRightResizeControl ? [self rightResizeControlFrame] : CGRectMakeZero(),
|
|
leftResizeWidth = leftResizeRect.size.width,
|
|
rightResizeWidth = rightResizeRect.size.width,
|
|
currentButtonOffset = _spacing - buttonHorizontalOffset,
|
|
availableWidth = frameWidth + 1 + buttonHorizontalOffset - leftResizeWidth - rightResizeWidth;
|
|
|
|
_neededWidth = currentButtonOffset;
|
|
_flexibleDelta = 0;
|
|
_flexibleDeltas = @[];
|
|
_flexibleCount = 0;
|
|
|
|
for (var i = 0, count = _buttons.length, button, width, delta; i < count; i++)
|
|
{
|
|
button = _buttons[i];
|
|
|
|
// Skip hidden buttons and separators if buttons are bordered
|
|
if ([button isHidden] || (_buttonsAreBordered && [button isKindOfClass:_CPButtonBarSeparator]))
|
|
continue;
|
|
|
|
if ([button isFlexible])
|
|
{
|
|
// This is a flexible item (space, search field, ...)
|
|
width = [button minWidth];
|
|
|
|
if (width > 0) // not a _CPButtonBarFlexibleSpace
|
|
{
|
|
// If maxWidth == -1, the item wants all the remaining space
|
|
if ([button maxWidth] != -1)
|
|
_flexibleDelta += (delta = [button maxWidth] - width);
|
|
else
|
|
delta = availableWidth;
|
|
|
|
[_flexibleDeltas addObject:delta];
|
|
_flexibleCount++;
|
|
}
|
|
}
|
|
else
|
|
// This is a fixed size item
|
|
width = [button frame].size.width;
|
|
|
|
_neededWidth += width + _spacing - buttonHorizontalOffset + 2 * [button extraSpacing];
|
|
|
|
if (useDividers)
|
|
_neededWidth += _spacing + 1;
|
|
}
|
|
|
|
_needsToComputeNeededSpace = NO;
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
if (_needsToLoadThemeValues)
|
|
[self _loadThemeValues];
|
|
|
|
if (_needsToComputeNeededSpace)
|
|
[self _computeNeededSpace];
|
|
|
|
var useDividers = _buttonsAreBordered && !!_dividerColor,
|
|
buttonHorizontalOffset = (useDividers ? 0 : 1),
|
|
bounds = [self bounds],
|
|
height = bounds.size.height - 1,
|
|
frameWidth = bounds.size.width,
|
|
leftResizeRect = _hasLeftResizeControl ? [self leftResizeControlFrame] : CGRectMakeZero(),
|
|
rightResizeRect = _hasRightResizeControl ? [self rightResizeControlFrame] : CGRectMakeZero(),
|
|
leftResizeWidth = leftResizeRect.size.width,
|
|
rightResizeWidth = rightResizeRect.size.width,
|
|
currentButtonOffset = leftResizeWidth + _spacing - buttonHorizontalOffset,
|
|
availableWidth = frameWidth + 1 + buttonHorizontalOffset - leftResizeWidth - rightResizeWidth,
|
|
flexibleSpaceWidth = 0,
|
|
remainingSpace = availableWidth - _neededWidth,
|
|
nbDividersToRemove = 0;
|
|
|
|
// If we can collapse dividers (avoiding double dividers), get back freed space
|
|
if (useDividers && (remainingSpace < _flexibleDelta))
|
|
{
|
|
nbDividersToRemove = MIN(_flexibleSpacesCount, _flexibleDelta - remainingSpace);
|
|
remainingSpace += nbDividersToRemove;
|
|
}
|
|
|
|
if (remainingSpace > 0)
|
|
{
|
|
// Some space remains.
|
|
|
|
// If we have flexible items (other than spaces), distribute remaining space
|
|
if (_flexibleCount > 0)
|
|
{
|
|
// We first distribute to non "all space" items
|
|
// flexibleDelta is the total needed extra space for non "all space" items
|
|
|
|
var distributionRatio = (_flexibleDelta > remainingSpace) ? remainingSpace / _flexibleDelta : 1.0,
|
|
allSpaceItemsCount = 0;
|
|
|
|
for (var i = 0, count = _flexibles.length, item, itemSize; i < count; i++)
|
|
{
|
|
item = _flexibles[i];
|
|
|
|
if ([item maxWidth] != -1)
|
|
[item setFrameSize:CGSizeMake([item minWidth] + _flexibleDeltas[i] * distributionRatio, [item frameSize].height)];
|
|
else
|
|
allSpaceItemsCount++;
|
|
}
|
|
|
|
remainingSpace -= _flexibleDelta;
|
|
|
|
if ((remainingSpace > 0) && (allSpaceItemsCount > 0))
|
|
{
|
|
// Some space remains.
|
|
// We now distribute it evenly to "all space" items
|
|
|
|
var evenSpace = remainingSpace / allSpaceItemsCount;
|
|
|
|
for (var i = 0, count = _flexibles.length, item; i < count; i++)
|
|
{
|
|
item = _flexibles[i];
|
|
|
|
if ([item maxWidth] == -1)
|
|
[item setFrameSize:CGSizeMake([item minWidth] + evenSpace, [item frameSize].height)];
|
|
}
|
|
|
|
remainingSpace = 0;
|
|
}
|
|
}
|
|
|
|
// If we have flexible space(s), distribute remaining space evenly
|
|
if (_flexibleSpacesCount > 0)
|
|
flexibleSpaceWidth = (remainingSpace > 0) ? remainingSpace / _flexibleSpacesCount : 0;
|
|
}
|
|
|
|
for (var i = 0, count = _buttons.length, button, width, extraSpace, buttonHeight; i < count; i++)
|
|
{
|
|
button = _buttons[i];
|
|
|
|
// Skip separators if buttons are bordered
|
|
if ([button isKindOfClass:_CPButtonBarSeparator])
|
|
[button setHidden:_buttonsAreBordered];
|
|
|
|
// Skip hidden buttons
|
|
if ([button isHidden])
|
|
{
|
|
// We mask the related divider
|
|
[_dividers[i] setHidden:YES];
|
|
continue;
|
|
}
|
|
|
|
width = ([button isKindOfClass:_CPButtonBarFlexibleSpace]) ? flexibleSpaceWidth : [button frame].size.width;
|
|
extraSpace = [button extraSpacing];
|
|
buttonHeight = [button frame].size.height;
|
|
|
|
// Avoid double separators if width = 0
|
|
if ((width == 0) && (nbDividersToRemove > 0))
|
|
{
|
|
[_dividers[i] setHidden:YES];
|
|
|
|
nbDividersToRemove--;
|
|
continue;
|
|
}
|
|
|
|
currentButtonOffset += extraSpace;
|
|
[button setFrame:CGRectMake(currentButtonOffset, _buttonVerticalOffset + (height - buttonHeight)/2, width, buttonHeight)];
|
|
currentButtonOffset += width + _spacing - buttonHorizontalOffset + extraSpace;
|
|
|
|
if (useDividers)
|
|
{
|
|
[_dividers[i] setFrame:CGRectMake(currentButtonOffset, 0, 1, height)];
|
|
currentButtonOffset += _spacing + 1;
|
|
[_dividers[i] setHidden:NO];
|
|
}
|
|
}
|
|
|
|
if (_hasLeftResizeControl)
|
|
{
|
|
var resizeControlView = [self layoutEphemeralSubviewNamed:@"left-resize-control-view"
|
|
positioned:CPWindowAbove
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
|
|
[resizeControlView setAutoresizingMask:CPViewMaxXMargin];
|
|
[resizeControlView setBackgroundColor:_resizeControlViewBackground];
|
|
}
|
|
|
|
if (_hasRightResizeControl)
|
|
{
|
|
var resizeControlView = [self layoutEphemeralSubviewNamed:@"right-resize-control-view"
|
|
positioned:CPWindowAbove
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
|
|
[resizeControlView setAutoresizingMask:CPViewMinXMargin];
|
|
[resizeControlView setBackgroundColor:_resizeControlViewBackground];
|
|
}
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context
|
|
{
|
|
if ([change objectForKey:CPKeyValueChangeOldKey] == [change objectForKey:CPKeyValueChangeNewKey])
|
|
return;
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Delegate
|
|
|
|
- (id)delegate
|
|
{
|
|
return _delegate;
|
|
}
|
|
|
|
- (void)setDelegate:(id <CPButtonBarDelegate>)aDelegate
|
|
{
|
|
if (_delegate == aDelegate)
|
|
return;
|
|
|
|
_delegate = aDelegate;
|
|
|
|
_delegateSelectors = 0;
|
|
|
|
if ([_delegate respondsToSelector:@selector(populateButtonBar:)])
|
|
{
|
|
_delegateSelectors |= CPButtonBarPopulateButtonBarSelector;
|
|
|
|
if ([_buttons count] == 0)
|
|
[_delegate populateButtonBar:self];
|
|
}
|
|
}
|
|
|
|
- (void)_sendDelegatePopulateButtonBar
|
|
{
|
|
if (_delegateSelectors & CPButtonBarPopulateButtonBarSelector)
|
|
[_delegate populateButtonBar:self];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark CPCoding
|
|
|
|
var CPButtonBarHasLeftResizeControlKey = @"CPButtonBarHasLeftResizeControlKey",
|
|
CPButtonBarHasRightResizeControlKey = @"CPButtonBarHasRightResizeControlKey",
|
|
CPButtonBarAutomaticResizeControlKey = @"CPButtonBarAutomaticResizeControlKey",
|
|
CPButtonBarButtonsAreBorderedKey = @"CPButtonBarButtonsAreBorderedKey",
|
|
CPButtonBarDrawsSeparatorKey = @"CPButtonBarDrawsSeparatorKey",
|
|
CPButtonBarIsTransparentKey = @"CPButtonBarIsTransparentKey",
|
|
CPButtonBarButtonsKey = @"CPButtonBarButtonsKey",
|
|
CPButtonBarDividersKey = @"CPButtonBarDividersKey",
|
|
CPButtonBarFlexiblesKey = @"CPButtonBarFlexiblesKey",
|
|
CPButtonBarFlexibleSpacesCountKey = @"CPButtonBarFlexibleSpacesCountKey",
|
|
CPButtonBarDelegateKey = @"CPButtonBarDelegateKey";
|
|
|
|
@implementation CPButtonBar (CPCoding)
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[super encodeWithCoder:aCoder];
|
|
|
|
[aCoder encodeBool: _hasLeftResizeControl forKey:CPButtonBarHasLeftResizeControlKey];
|
|
[aCoder encodeBool: _hasRightResizeControl forKey:CPButtonBarHasRightResizeControlKey];
|
|
[aCoder encodeBool: _automaticResizeControl forKey:CPButtonBarAutomaticResizeControlKey];
|
|
[aCoder encodeBool: _buttonsAreBordered forKey:CPButtonBarButtonsAreBorderedKey];
|
|
[aCoder encodeBool: _drawsSeparator forKey:CPButtonBarDrawsSeparatorKey];
|
|
[aCoder encodeBool: _isTransparent forKey:CPButtonBarIsTransparentKey];
|
|
[aCoder encodeObject:_buttons forKey:CPButtonBarButtonsKey];
|
|
[aCoder encodeObject:_dividers forKey:CPButtonBarDividersKey];
|
|
[aCoder encodeObject:_flexibles forKey:CPButtonBarFlexiblesKey];
|
|
[aCoder encodeInt: _flexibleSpacesCount forKey:CPButtonBarFlexibleSpacesCountKey];
|
|
|
|
[aCoder encodeConditionalObject:_delegate forKey:CPButtonBarDelegateKey];
|
|
}
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
if (self = [super initWithCoder:aCoder])
|
|
{
|
|
_hasLeftResizeControl = [aCoder decodeBoolForKey:CPButtonBarHasLeftResizeControlKey];
|
|
_hasRightResizeControl = [aCoder decodeBoolForKey:CPButtonBarHasRightResizeControlKey];
|
|
_automaticResizeControl = [aCoder decodeBoolForKey:CPButtonBarAutomaticResizeControlKey];
|
|
|
|
[self setButtonsAreBordered:!![aCoder decodeBoolForKey:CPButtonBarButtonsAreBorderedKey]];
|
|
[self setDrawsSeparator: !![aCoder decodeBoolForKey:CPButtonBarDrawsSeparatorKey]];
|
|
[self setIsTransparent: !![aCoder decodeBoolForKey:CPButtonBarIsTransparentKey]];
|
|
|
|
_buttons = [aCoder decodeObjectForKey:CPButtonBarButtonsKey] || @[];
|
|
_dividers = [aCoder decodeObjectForKey:CPButtonBarDividersKey] || @[];
|
|
_flexibles = [aCoder decodeObjectForKey:CPButtonBarFlexiblesKey] || @[];
|
|
_flexibleSpacesCount = [aCoder decodeIntForKey:CPButtonBarFlexibleSpacesCountKey];
|
|
|
|
[self setNeedsRelayout:YES];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Adaptative Popup button
|
|
|
|
@implementation _CPButtonBarAdaptativePopUpButton : CPPopUpButton
|
|
{
|
|
int _extraSpacing @accessors(readonly, property=extraSpacing);
|
|
}
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-adaptative-popup";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"extra-spacing": 0
|
|
};
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (id)initWithFrame:(CGRect)aFrame
|
|
{
|
|
self = [super initWithFrame:aFrame];
|
|
|
|
if (self)
|
|
{
|
|
[self setBezelStyle:CPRegularSquareBezelStyle];
|
|
[self setControlSize:CPSmallControlSize];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
// Get extra-spacing
|
|
_extraSpacing = [self currentValueForThemeAttribute:@"extra-spacing"];
|
|
}
|
|
|
|
- (void)synchronizeTitleAndSelectedItem
|
|
{
|
|
var item = [self selectedItem];
|
|
|
|
[self _setImage:[item image]];
|
|
[self _setTitle:([item isKindOfClass:_CPAdaptativeMenuItem] && [item shortTitle] ? [item shortTitle] : [item title])];
|
|
|
|
// Adapt the width of the popup
|
|
[self sizeToFit];
|
|
[[self superview] setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (void)_setImage:(CPImage)anImage
|
|
{
|
|
var grannyMethod = class_getInstanceMethod([[self superclass] superclass], @selector(setImage:)),
|
|
grannyImplementation = method_getImplementation(grannyMethod);
|
|
|
|
grannyImplementation(self, @selector(setImage:), anImage);
|
|
}
|
|
|
|
- (void)_setTitle:(CPString)aTitle
|
|
{
|
|
var grannyMethod = class_getInstanceMethod([[self superclass] superclass], @selector(setTitle:)),
|
|
grannyImplementation = method_getImplementation(grannyMethod);
|
|
|
|
grannyImplementation(self, @selector(setTitle:), aTitle);
|
|
}
|
|
|
|
- (CPMenuItem)addItemWithTitle:(CPString)aTitle shortTitle:(CPString)aShortTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent
|
|
{
|
|
var item = [[_CPAdaptativeMenuItem alloc] initWithTitle:aTitle shortTitle:aShortTitle action:anAction keyEquivalent:aKeyEquivalent];
|
|
|
|
[self addItem:item];
|
|
|
|
return item;
|
|
}
|
|
|
|
- (void)setBordered:(BOOL)shouldBeBordered
|
|
{
|
|
[super setBordered:shouldBeBordered];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Adaptative PullDown button
|
|
|
|
@implementation _CPButtonBarAdaptativePullDownButton : CPPopUpButton
|
|
{
|
|
int _extraSpacing @accessors(readonly, property=extraSpacing);
|
|
int _minHeight;
|
|
}
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-adaptative-pulldown";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"extra-spacing": 0
|
|
};
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (id)initWithTitle:(CPString)aTitle
|
|
{
|
|
self = [super initWithFrame:CGRectMakeZero()];
|
|
|
|
if (self)
|
|
{
|
|
[self setBezelStyle:CPRegularSquareBezelStyle];
|
|
[self setControlSize:CPSmallControlSize];
|
|
[self setPullsDown:YES];
|
|
|
|
[self _initThemedValues];
|
|
|
|
[self setTitle:aTitle];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
_extraSpacing = [self currentValueForThemeAttribute:@"extra-spacing"];
|
|
var minSize = [self currentValueForThemeAttribute:@"min-size"] || CGSizeMakeZero();
|
|
_minHeight = minSize.height;
|
|
}
|
|
|
|
- (void)synchronizeTitleAndSelectedItem
|
|
{
|
|
var item = nil,
|
|
items = [[self menu] itemArray];
|
|
|
|
if ([items count] > 0)
|
|
item = items[0];
|
|
|
|
[self _setImage:[item image]];
|
|
[self _setTitle:([item isKindOfClass:_CPAdaptativeMenuItem] && [item shortTitle] ? [item shortTitle] : [item title])];
|
|
|
|
// Adapt the width of the popup
|
|
[self sizeToFit];
|
|
[[self superview] setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (void)_setImage:(CPImage)anImage
|
|
{
|
|
var grannyMethod = class_getInstanceMethod([[self superclass] superclass], @selector(setImage:)),
|
|
grannyImplementation = method_getImplementation(grannyMethod);
|
|
|
|
grannyImplementation(self, @selector(setImage:), anImage);
|
|
}
|
|
|
|
- (void)_setTitle:(CPString)aTitle
|
|
{
|
|
var grannyMethod = class_getInstanceMethod([[self superclass] superclass], @selector(setTitle:)),
|
|
grannyImplementation = method_getImplementation(grannyMethod);
|
|
|
|
grannyImplementation(self, @selector(setTitle:), aTitle);
|
|
}
|
|
|
|
- (CPMenuItem)addItemWithTitle:(CPString)aTitle shortTitle:(CPString)aShortTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent
|
|
{
|
|
var item = [[_CPAdaptativeMenuItem alloc] initWithTitle:aTitle shortTitle:aShortTitle action:anAction keyEquivalent:aKeyEquivalent];
|
|
|
|
[self addItem:item];
|
|
|
|
return item;
|
|
}
|
|
|
|
- (void)setBordered:(BOOL)shouldBeBordered
|
|
{
|
|
[super setBordered:shouldBeBordered];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
- (void)sizeToFit
|
|
{
|
|
// We have to adapt our width but keep our height
|
|
[super sizeToFit];
|
|
[self setFrameSize:CGSizeMake([self frameSize].width, _minHeight)];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Menu item
|
|
|
|
@implementation _CPAdaptativeMenuItem : CPMenuItem
|
|
{
|
|
CPString _shortTitle @accessors(property=shortTitle);
|
|
}
|
|
|
|
- (id)initWithTitle:(CPString)aTitle shortTitle:(CPString)aShortTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent
|
|
{
|
|
self = [super initWithTitle:aTitle action:anAction keyEquivalent:aKeyEquivalent];
|
|
|
|
if (self)
|
|
_shortTitle = aShortTitle;
|
|
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Separator
|
|
|
|
@implementation _CPButtonBarSeparator : _CPImageAndTextView
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-separator";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"image": [CPNull null]
|
|
};
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (int)extraSpacing
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (id)initWithBordered:(BOOL)isBordered
|
|
{
|
|
self = [super initWithFrame:CGRectMakeZero()];
|
|
|
|
if (self)
|
|
{
|
|
[self setBordered:isBordered];
|
|
[self setImagePosition:CPImageOnly];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
var image = [self currentValueForThemeAttribute:@"image"],
|
|
size = image ? [image size] : CGSizeMakeZero();
|
|
|
|
[self setFrameSize:size];
|
|
[self setImage:image];
|
|
}
|
|
|
|
- (void)setBordered:(BOOL)shouldBeBordered
|
|
{
|
|
if (shouldBeBordered)
|
|
[self setThemeState:CPThemeStateBordered];
|
|
else
|
|
[self unsetThemeState:CPThemeStateBordered];
|
|
}
|
|
|
|
- (BOOL)isBordered
|
|
{
|
|
return [self hasThemeState:CPThemeStateBordered];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Flexible space
|
|
|
|
@implementation _CPButtonBarFlexibleSpace : CPView
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (int)extraSpacing
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (BOOL)isHidden
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (int)minWidth
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (int)maxWidth
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
- (void)setBordered:(BOOL)shouldBeBordered
|
|
{
|
|
if (shouldBeBordered)
|
|
[self setThemeState:CPThemeStateBordered];
|
|
else
|
|
[self unsetThemeState:CPThemeStateBordered];
|
|
}
|
|
|
|
- (BOOL)isBordered
|
|
{
|
|
return [self hasThemeState:CPThemeStateBordered];
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
// Just for compatibility...
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Search field
|
|
|
|
@implementation _CPButtonBarSearchField : CPSearchField
|
|
{
|
|
int _minWidth @accessors(property=minWidth);
|
|
int _maxWidth @accessors(property=maxWidth);
|
|
int _extraSpacing @accessors(readonly, property=extraSpacing);
|
|
|
|
BOOL _skipInitialFirstResponderCall;
|
|
}
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-searchfield";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"extra-spacing": 0
|
|
};
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return (_minWidth !== _maxWidth);
|
|
}
|
|
|
|
- (id)initWithPlaceholder:(CPString)placeholder minWidth:(int)minWidth maxWidth:(int)maxWidth
|
|
{
|
|
self = [super initWithFrame:CGRectMakeZero()];
|
|
|
|
if (self)
|
|
{
|
|
_minWidth = minWidth;
|
|
_maxWidth = maxWidth;
|
|
|
|
[self _initThemedValues];
|
|
|
|
[self setPlaceholderString:placeholder];
|
|
|
|
_skipInitialFirstResponderCall = YES;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
// Set height to the default height from theme
|
|
[self setFrameSize:CGSizeMake(_minWidth, [self currentValueForThemeAttribute:@"min-size"].height)];
|
|
|
|
// Disable the not editing / editing difference in layouts
|
|
[self setValue:[self valueForThemeAttribute:@"search-button-rect-function" inStates:[CPTextFieldStateRounded, CPThemeStateBezeled, CPThemeStateEditing]] forThemeAttribute:@"search-button-rect-function" inStates:[CPTextFieldStateRounded, CPThemeStateBezeled]];
|
|
|
|
// Get extra-spacing
|
|
_extraSpacing = [self currentValueForThemeAttribute:@"extra-spacing"];
|
|
}
|
|
|
|
- (void)setBordered:(BOOL)shouldBeBordered
|
|
{
|
|
[super setBordered:shouldBeBordered];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
- (BOOL)acceptsFirstResponder
|
|
{
|
|
// When this search field is the first text field inserted in the view hierarchy of the window, it will be asked to be the first responder
|
|
// of the window, and we don't want that. So we skip the first acceptsFirstResponder call.
|
|
return _skipInitialFirstResponderCall ? (_skipInitialFirstResponderCall = NO) : [super acceptsFirstResponder];
|
|
}
|
|
|
|
- (void)mouseDown:(CPEvent)anEvent
|
|
{
|
|
_skipInitialFirstResponderCall = NO;
|
|
|
|
[super mouseDown:anEvent];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Button
|
|
|
|
@implementation _CPButtonBarButton : CPButton
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-button";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"highlights-by": [CPNull null]
|
|
};
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (int)extraSpacing
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (id)initWithImage:(CPImage)image alternateImage:(CPImage)alternateImage bordered:(BOOL)isBordered
|
|
{
|
|
self = [super initWithFrame:CGRectMakeZero()];
|
|
|
|
if (self)
|
|
{
|
|
[self setBordered:isBordered];
|
|
|
|
[self setImage:image];
|
|
[self setAlternateImage:alternateImage];
|
|
[self setImagePosition:CPImageOnly];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
var size = [self currentValueForThemeAttribute:@"min-size"],
|
|
highlightsBy = [self currentValueForThemeAttribute:@"highlights-by"];
|
|
|
|
if (size)
|
|
[self setFrameSize:size];
|
|
|
|
if (highlightsBy)
|
|
[self setHighlightsBy:highlightsBy];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Popup
|
|
|
|
@implementation _CPButtonBarPopUpButton : CPPopUpButton
|
|
{
|
|
CPImage _image;
|
|
}
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-popup";
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (int)extraSpacing
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (id)initWithImage:(CPImage)image alternateImage:(CPImage)alternateImage
|
|
{
|
|
self = [super initWithFrame:CGRectMakeZero() pullsDown:YES];
|
|
|
|
if (self)
|
|
{
|
|
[self addItemWithTitle:@""];
|
|
[self setControlSize:CPSmallControlSize];
|
|
_image = image;
|
|
[self setImagePosition:CPImageOnly];
|
|
[self setAlternateImage:alternateImage];
|
|
[self setBezelStyle:CPRegularSquareBezelStyle];
|
|
[self setShowsStateBy:CPContentsCellMask];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
[self setFrameSize:[self currentValueForThemeAttribute:@"min-size"]];
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
var bezelColor = [self currentValueForThemeAttribute:@"bezel-color"],
|
|
contentView;
|
|
|
|
// 1. Handle Background Color
|
|
if ([bezelColor isCSSBased])
|
|
{
|
|
[self setBackgroundColor:bezelColor];
|
|
contentView = [self layoutEphemeralSubviewNamed:@"content-view"
|
|
positioned:CPWindowAbove
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
}
|
|
else if (bezelColor)
|
|
{
|
|
var bezelView = [self layoutEphemeralSubviewNamed:@"bezel-view"
|
|
positioned:CPWindowBelow
|
|
relativeToEphemeralSubviewNamed:@"content-view"];
|
|
|
|
[bezelView setBackgroundColor:bezelColor];
|
|
|
|
contentView = [self layoutEphemeralSubviewNamed:@"content-view"
|
|
positioned:CPWindowAbove
|
|
relativeToEphemeralSubviewNamed:@"bezel-view"];
|
|
}
|
|
else
|
|
{
|
|
contentView = [self layoutEphemeralSubviewNamed:@"content-view"
|
|
positioned:CPWindowAbove
|
|
relativeToEphemeralSubviewNamed:nil];
|
|
}
|
|
|
|
if (contentView)
|
|
{
|
|
[contentView setImage:_image];
|
|
[contentView setFrameSize:CGSizeMake(16,16)];
|
|
[contentView setImagePosition:CPImageLeft];
|
|
[contentView setDimsImage:[self hasThemeState:CPThemeStateDisabled] && _imageDimsWhenDisabled];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Adaptative Label
|
|
|
|
@implementation _CPButtonBarAdaptativeLabel : CPTextField
|
|
{
|
|
int _extraSpacing @accessors(readonly, property=extraSpacing);
|
|
}
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-adaptative-label";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"extra-spacing": 0
|
|
};
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (id)initWithTitle:(CPString)aTitle
|
|
{
|
|
self = [_CPButtonBarAdaptativeLabel labelWithTitle:aTitle];
|
|
|
|
if (self)
|
|
{
|
|
[self setControlSize:CPSmallControlSize];
|
|
|
|
[self _initThemedValues];
|
|
[self sizeToFit];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
// Get extra-spacing
|
|
_extraSpacing = [self currentValueForThemeAttribute:@"extra-spacing"];
|
|
}
|
|
|
|
- (void)setStringValue:(CPString)aString
|
|
{
|
|
[super setStringValue:aString];
|
|
|
|
// Adapt the width of the label
|
|
[self sizeToFit];
|
|
[[self superview] setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (void)setBordered:(BOOL)shouldBeBordered
|
|
{
|
|
[super setBordered:shouldBeBordered];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Label
|
|
|
|
@implementation _CPButtonBarLabel : CPTextField
|
|
{
|
|
int _extraSpacing @accessors(readonly, property=extraSpacing);
|
|
}
|
|
|
|
+ (CPString)defaultThemeClass
|
|
{
|
|
return "button-bar-label";
|
|
}
|
|
|
|
+ (CPDictionary)themeAttributes
|
|
{
|
|
return @{
|
|
@"extra-spacing": 0
|
|
};
|
|
}
|
|
|
|
- (BOOL)isButtonBarItem
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isFlexible
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (id)initWithTitle:(CPString)aTitle
|
|
{
|
|
self = [_CPButtonBarLabel labelWithTitle:aTitle];
|
|
|
|
if (self)
|
|
{
|
|
[self setControlSize:CPSmallControlSize];
|
|
[self setVerticalAlignment:CPCenterVerticalTextAlignment];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithTitle:(CPString)aTitle width:(int)aWidth
|
|
{
|
|
self = [self labelWithTitle:aTitle];
|
|
|
|
if (self)
|
|
[self setFrameSize:CGSizeMake(aWidth, [self bounds].size.height)];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initThemedValues
|
|
{
|
|
// Get extra-spacing
|
|
_extraSpacing = [self currentValueForThemeAttribute:@"extra-spacing"];
|
|
}
|
|
|
|
- (void)setStringValue:(CPString)aString
|
|
{
|
|
[super setStringValue:aString];
|
|
|
|
[[self superview] setNeedsRelayout:YES];
|
|
}
|
|
|
|
- (void)setBordered:(BOOL)shouldBeBordered
|
|
{
|
|
[super setBordered:shouldBeBordered];
|
|
|
|
[self _initThemedValues];
|
|
}
|
|
|
|
@end
|
|
|