/* * CPProgressIndicator.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 "CGGeometry.j" @import "CPImageView.j" @import "CPView.j" @import "CPWindow_Constants.j" @typedef CPProgressIndicatorStyle /* @global @group CPProgressIndicatorStyle */ CPProgressIndicatorBarStyle = 0; /* @global @group CPProgressIndicatorStyle */ CPProgressIndicatorSpinningStyle = 1; /* @global @group CPProgressIndicatorStyle */ CPProgressIndicatorHUDBarStyle = 2; /*! @ingroup appkit @class CPProgressIndicator This class is used in a Cappuccino GUI to display the progress of a function or task. If the duration of the task is unknown, there is also an indeterminate mode for the indicator. */ @implementation CPProgressIndicator : CPView { double _minValue; double _maxValue; double _doubleValue; CPControlSize _controlSize; BOOL _indeterminate; CPProgressIndicatorStyle _style; BOOL _isAnimating; BOOL _isDisplayedWhenStoppedSet; BOOL _isDisplayedWhenStopped; } // Inject CSS Keyframes for spinning animation (Standard + WebKit) + (void)initialize { if (self !== [CPProgressIndicator class]) return; #if PLATFORM(DOM) if (document.getElementById("cp-progress-indicator-style")) return; var style = document.createElement("style"); style.id = "cp-progress-indicator-style"; style.type = "text/css"; // We define two animations: // 1. cp-progress-indicator-spin: Rotates 360 degrees (for spinners) // 2. cp-progress-indicator-bar-slide: Moves background-position (for striped bars) var css = "@keyframes cp-progress-indicator-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } " + "@-webkit-keyframes cp-progress-indicator-spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } " + "@keyframes cp-progress-indicator-bar-slide { 0% { background-position: 0 0; } 100% { background-position: 30px 0; } } " + "@-webkit-keyframes cp-progress-indicator-bar-slide { 0% { background-position: 0 0; } 100% { background-position: 30px 0; } }"; if (style.styleSheet) style.styleSheet.cssText = css; else style.appendChild(document.createTextNode(css)); var head = document.getElementsByTagName("head")[0]; if (head) head.appendChild(style); #endif } + (CPString)defaultThemeClass { return @"progress-indicator"; } + (CPDictionary)themeAttributes { return @{ @"indeterminate-bar-color": [CPNull null], @"bar-color": [CPNull null], @"default-height": 20, @"bezel-color": [CPNull null], // CSS Spinner Attributes @"spinner-color": [CPColor grayColor], @"spinner-track-color": [CPColor colorWithWhite:0.9 alpha:1.0], @"spinner-line-width": 3.0, @"circular-border-color": [CPNull null], @"circular-border-size": 1, @"circular-color": [CPNull null], @"spinning-mini-gif": [CPNull null], @"spinning-small-gif": [CPNull null], @"spinning-regular-gif": [CPNull null] }; } + (Class)_binderClassForBinding:(CPString)aBinding { if (aBinding === CPValueBinding || aBinding === @"isIndeterminate") return [_CPProgressIndicatorBinder class]; return [super _binderClassForBinding:aBinding]; } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self) { _minValue = 0.0; _maxValue = 100.0; _doubleValue = 0.0; _style = CPProgressIndicatorBarStyle; _isDisplayedWhenStoppedSet = NO; _controlSize = CPRegularControlSize; [self setNeedsLayout]; } return self; } /* @ignore */ - (void)setUsesThreadedAnimation:(BOOL)aFlag { } /*! Starts the animation of the progress indicator in indeterminate mode. @param the requesting object */ - (void)startAnimation:(id)aSender { _isAnimating = YES; [self _hideOrDisplay]; [self setNeedsLayout]; // Trigger layout to update CSS animation state } /*! Stops the animation of the progress indicator in indeterminate mode. @param the requesting object */ - (void)stopAnimation:(id)aSender { _isAnimating = NO; [self _hideOrDisplay]; [self setNeedsLayout]; // Trigger layout to remove CSS animation state } /*! Always returns \c NO. Cappuccino does not have multiple threads. */ - (BOOL)usesThreadedAnimation { return NO; } // Advancing the Progress Bar /*! Increases the progress of the bar by the specified value. @param aValue the amount to increase the progress value */ - (void)incrementBy:(double)aValue { [self setDoubleValue:_doubleValue + aValue]; } /*! Sets the progress value of the indicator. */ - (void)setDoubleValue:(double)aValue { _doubleValue = MIN(MAX(aValue, _minValue), _maxValue); [self drawBar]; } /*! Returns the value of the progress indicator. */ - (double)doubleValue { return _doubleValue; } /*! Sets the minimum value of the progress indicator. The default is 0.0. @param aValue the new minimum value */ - (void)setMinValue:(double)aValue { _minValue = aValue; } /*! Returns the minimum value of the progress indicator. */ - (double)minValue { return _minValue; } /*! Sets the maximum value of the progress indicator. The default is 100.0. @param aValue the new maximum value. */ - (void)setMaxValue:(double)aValue { _maxValue = aValue; } /*! Returns the maximum value of the progress indicator. */ - (double)maxValue { return _maxValue; } // Setting the Appearance /*! Sets the progress indicator's size. @param aControlSize the new size */ - (void)setControlSize:(CPControlSize)aControlSize { if (_controlSize == aControlSize) return; _controlSize = aControlSize; [self setNeedsLayout]; } /*! Returns the progress indicator's size */ - (CPControlSize)controlSize { return _controlSize; } /* Not yet implemented */ - (void)setControlTint:(CPControlTint)aControlTint { } /* Not yet implemented. */ - (CPControlTint)controlTint { return 0; } /* Not yet implemented. */ - (void)setBezeled:(BOOL)isBezeled { } /* Not yet implemented. */ - (BOOL)isBezeled { return YES; } /*! Specifies whether this progress indicator should be indeterminate or display progress based on it's max and min. @param isDeterminate \c YES makes the indicator indeterminate */ - (void)setIndeterminate:(BOOL)indeterminate { if (_indeterminate == indeterminate) return; _indeterminate = indeterminate; [self setNeedsLayout]; } /*! Returns \c YES if the progress bar is indeterminate. */ - (BOOL)isIndeterminate { return _indeterminate; } /*! Sets the progress indicator's style @param aStyle the style to set it to */ - (void)setStyle:(CPProgressIndicatorStyle)aStyle { if (_style == aStyle) return; _style = aStyle; [self setNeedsLayout]; } /*! Resizes the indicator based on it's style. */ - (void)sizeToFit { if (_style == CPProgressIndicatorSpinningStyle) { var size = 32.0; if (_controlSize === CPMiniControlSize) size = 16.0; else if (_controlSize === CPSmallControlSize) size = 24.0; else if (_controlSize === CPRegularControlSize) size = 32.0; [self setFrameSize:CGSizeMake(size, size)]; } else [self setFrameSize:CGSizeMake(CGRectGetWidth([self frame]), [self valueForThemeAttribute:@"default-height"])]; } /*! Sets whether the indicator should be displayed when it isn't animating. By default this is \c YES if the style is CPProgressIndicatorBarStyle, and \c NO if it's CPProgressIndicatorSpinningStyle. @param isDisplayedWhenStopped \c YES means the indicator will be displayed when it's not animating. */ - (void)setDisplayedWhenStopped:(BOOL)isDisplayedWhenStopped { if (_isDisplayedWhenStoppedSet && _isDisplayedWhenStopped == isDisplayedWhenStopped) return; _isDisplayedWhenStoppedSet = YES; _isDisplayedWhenStopped = isDisplayedWhenStopped; [self _hideOrDisplay]; } /*! Returns \c YES if the progress bar is displayed when not animating. */ - (BOOL)isDisplayedWhenStopped { if (_isDisplayedWhenStoppedSet) return _isDisplayedWhenStopped; if (_style == CPProgressIndicatorBarStyle || _style == CPProgressIndicatorHUDBarStyle) return YES; return NO; } /* @ignore */ - (void)_hideOrDisplay { [self setHidden:!_isAnimating && ![self isDisplayedWhenStopped]]; } - (void)setFrameSize:(CGSize)aSize { [super setFrameSize:aSize]; [self drawBar]; } /* @ignore */ - (void)drawBar { [self setNeedsLayout]; [self setNeedsDisplay:YES]; } - (CPView)createEphemeralSubviewNamed:(CPString)aName { return [[CPView alloc] initWithFrame:CGRectMakeZero()]; } - (CGRect)rectForEphemeralSubviewNamed:(CPString)aViewName { // Handle the standard Bar View if (aViewName === @"bar-view" && _style !== CPProgressIndicatorSpinningStyle) { var width = CGRectGetWidth([self bounds]), barWidth = width * ((_doubleValue - _minValue) / (_maxValue - _minValue)); if (barWidth > 0.0 && barWidth < 4.0) barWidth = 4.0; if (_indeterminate) barWidth = width; return CGRectMake(0, 0, barWidth, [self valueForThemeAttribute:@"default-height"]); } // Handle the Spinning View (CSS Spinner) if (aViewName === @"spinner-view" && _style == CPProgressIndicatorSpinningStyle && _indeterminate) { return [self bounds]; } // Return nil for views that shouldn't appear in the current style return nil; } - (void)layoutSubviews { if (YES)//_isBezeled) { // === SPINNING STYLE === if (_style == CPProgressIndicatorSpinningStyle) { // If indeterminate, use CSS spinner if (_indeterminate) { var spinnerView = [self layoutEphemeralSubviewNamed:"spinner-view" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:nil]; // Ensure other views are hidden [self layoutEphemeralSubviewNamed:"bar-view" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil]; // Configure CSS on the subview, NOT self._DOMElement, to avoid transform conflicts var domEl = spinnerView._DOMElement, spinnerColor = [self currentValueForThemeAttribute:@"spinner-color"], trackColor = [self currentValueForThemeAttribute:@"spinner-track-color"], lineWidth = [self currentValueForThemeAttribute:@"spinner-line-width"], widthStr = lineWidth + "px"; domEl.style.boxSizing = "border-box"; domEl.style.borderRadius = "50%"; domEl.style.borderStyle = "solid"; domEl.style.borderWidth = widthStr; domEl.style.borderColor = [trackColor cssString]; domEl.style.borderTopColor = [spinnerColor cssString]; // Animation logic if (_isAnimating) { var anim = "cp-progress-indicator-spin 1s linear infinite"; domEl.style.animation = anim; domEl.style.WebkitAnimation = anim; } else { domEl.style.animation = "none"; domEl.style.WebkitAnimation = "none"; } // Ensure main view is transparent [self setBackgroundColor:nil]; } else { // Determinate Spinner (Pie Chart drawn in drawRect) // Hide CSS spinner [self layoutEphemeralSubviewNamed:"spinner-view" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil]; [self setBackgroundColor:nil]; } } // === BAR STYLE === else { // Hide spinner [self layoutEphemeralSubviewNamed:"spinner-view" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil]; [self setBackgroundColor:[self currentValueForThemeAttribute:@"bezel-color"]]; var barView = [self layoutEphemeralSubviewNamed:"bar-view" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil]; if (_indeterminate) [barView setBackgroundColor:[self currentValueForThemeAttribute:@"indeterminate-bar-color"]]; else [barView setBackgroundColor:[self currentValueForThemeAttribute:@"bar-color"]]; } } else [self setBackgroundColor:nil]; } - (void)drawRect:(CGRect)aRect { // Handle determinate state for Spinning style (Pie chart progress) via CoreGraphics. // If indeterminate, the CSS animation (spinner-view) takes over and we draw nothing. if (_style == CPProgressIndicatorSpinningStyle && !_indeterminate) { var context = [[CPGraphicsContext currentContext] graphicsPort], rect = CGRectMakeCopy(aRect), borderSize = [self currentValueForThemeAttribute:@"circular-border-size"]; rect.origin.x += borderSize; rect.origin.y += borderSize; rect.size.width = rect.size.width - borderSize * 2; rect.size.height = rect.size.height - borderSize * 2; if ([self doubleValue] > [self minValue] && [self doubleValue] < [self maxValue]) { var midX = CGRectGetMidX(rect), midY = CGRectGetMidY(rect), endAngle = Math.PI * 2 * (([self doubleValue] - [self minValue]) / ([self maxValue] - [self minValue])) - Math.PI / 2, radius = MIN(rect.size.width / 2, rect.size.height / 2); CGContextBeginPath(context); CGContextSetLineWidth(context, borderSize); CGContextSetFillColor(context, [self currentValueForThemeAttribute:@"circular-color"]); CGContextMoveToPoint(context, midX, midY); CGContextAddArc(context, midX, midY, radius, 3 * Math.PI / 2, endAngle, YES); CGContextAddLineToPoint(context, midX, midY); CGContextClosePath(context); CGContextFillPath(context); CGContextStrokePath(context); } else if ([self doubleValue] == [self maxValue]) { CGContextBeginPath(context); CGContextSetFillColor(context, [self currentValueForThemeAttribute:@"circular-color"]); CGContextAddEllipseInRect(context, rect); CGContextClosePath(context); CGContextFillPath(context); } CGContextBeginPath(context); CGContextSetStrokeColor(context , [self currentValueForThemeAttribute:@"circular-border-color"]); CGContextSetLineWidth(context, borderSize); CGContextAddEllipseInRect(context, rect); CGContextClosePath(context); CGContextStrokePath(context); } } @end @implementation CPProgressIndicator (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { if (self = [super initWithCoder:aCoder]) { _minValue = [aCoder decodeObjectForKey:@"_minValue"]; _maxValue = [aCoder decodeObjectForKey:@"_maxValue"]; _doubleValue = [aCoder decodeObjectForKey:@"_doubleValue"]; _controlSize = [aCoder decodeObjectForKey:@"_controlSize"]; _indeterminate = [aCoder decodeObjectForKey:@"_indeterminate"]; _style = [aCoder decodeIntForKey:@"_style"]; _isAnimating = [aCoder decodeObjectForKey:@"_isAnimating"]; _isDisplayedWhenStoppedSet = [aCoder decodeObjectForKey:@"_isDisplayedWhenStoppedSet"]; _isDisplayedWhenStopped = [aCoder decodeObjectForKey:@"_isDisplayedWhenStopped"]; [self setNeedsLayout]; } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { var backgroundColor = [self backgroundColor]; [self setBackgroundColor:nil]; [super encodeWithCoder:aCoder]; [self setBackgroundColor:backgroundColor]; [aCoder encodeObject:_minValue forKey:@"_minValue"]; [aCoder encodeObject:_maxValue forKey:@"_maxValue"]; [aCoder encodeObject:_doubleValue forKey:@"_doubleValue"]; [aCoder encodeObject:_controlSize forKey:@"_controlSize"]; [aCoder encodeObject:_indeterminate forKey:@"_indeterminate"]; [aCoder encodeInt:_style forKey:@"_style"]; [aCoder encodeObject:_isAnimating forKey:@"_isAnimating"]; [aCoder encodeObject:_isDisplayedWhenStoppedSet forKey:@"_isDisplayedWhenStoppedSet"]; [aCoder encodeObject:_isDisplayedWhenStopped forKey:@"_isDisplayedWhenStopped"]; } @end @implementation _CPProgressIndicatorBinder : CPBinder - (void)_updatePlaceholdersWithOptions:(CPDictionary)options forBinding:(CPString)aBinding { var value = aBinding === CPValueBinding ? 0.0 : YES; [self _setPlaceholder:value forMarker:CPMultipleValuesMarker isDefault:YES]; [self _setPlaceholder:value forMarker:CPNoSelectionMarker isDefault:YES]; [self _setPlaceholder:value forMarker:CPNotApplicableMarker isDefault:YES]; [self _setPlaceholder:value forMarker:CPNullMarker isDefault:YES]; } - (id)valueForBinding:(CPString)aBinding { if (aBinding === CPValueBinding) return [_source doubleValue]; else if (aBinding === @"isIndeterminate") [_source isIndeterminate]; else return [super valueForBinding:aBinding]; } - (BOOL)_setValue:(id)aValue forBinding:(CPString)aBinding { if (aBinding === CPValueBinding) [_source setDoubleValue:aValue]; else if (aBinding === @"isIndeterminate") [_source setIndeterminate:aValue]; else return NO; return YES; } - (void)setValue:(id)aValue forBinding:(CPString)aBinding { if (![self _setValue:aValue forBinding:aBinding]) [super setValue:aValue forBinding:aBinding]; } - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding { if (![self _setValue:aValue forBinding:aBinding]) [super setPlaceholderValue:aValue withMarker:aMarker forBinding:aBinding]; } @end