diff --git a/src/feathers/controls/dataRenderers/DrillDownItemRenderer.hx b/src/feathers/controls/dataRenderers/DrillDownItemRenderer.hx new file mode 100644 index 00000000..2b29c65e --- /dev/null +++ b/src/feathers/controls/dataRenderers/DrillDownItemRenderer.hx @@ -0,0 +1,271 @@ +/* + Feathers UI + Copyright 2024 Bowler Hat LLC. All Rights Reserved. + + This program is free software. You can redistribute and/or modify it in + accordance with the terms of the accompanying license agreement. + */ + +package feathers.controls.dataRenderers; + +import feathers.core.IOpenCloseToggle; +import feathers.core.IStateObserver; +import feathers.core.IUIControl; +import feathers.core.IValidating; +import feathers.core.InvalidationFlag; +import feathers.events.FeathersEvent; +import feathers.layout.Measurements; +import feathers.skins.IProgrammaticSkin; +import feathers.utils.AbstractDisplayObjectFactory; +import feathers.utils.DisplayObjectFactory; +import openfl.display.DisplayObject; +import openfl.errors.ArgumentError; +import openfl.events.Event; + +/** + A branch and leaf renderer for data containers that can drill down into + hierarchical data. Displays an additional icon on the right side to indicate + whether it represents a branch or not. + + @since 1.4.0 +**/ +@:styleContext +class DrillDownItemRenderer extends ItemRenderer implements IHierarchicalItemRenderer { + /** + Creates a new `DrillDownItemRenderer` object. + + @since 1.4.0 + **/ + public function new() { + initializeDrillDownItemRendererTheme(); + super(); + } + + private var _branch:Bool = false; + + /** + @see `feathers.controls.dataRenderers.IHierarchicalItemRenderer.branch` + **/ + public var branch(get, set):Bool; + + private function get_branch():Bool { + return this._branch; + } + + private function set_branch(value:Bool):Bool { + if (this._branch == value) { + return this._branch; + } + this._branch = value; + this.setInvalid(DATA); + return this._branch; + } + + private var _currentDrillDownIcon:DisplayObject; + private var _drillDownIconMeasurements:Measurements; + private var _ignoreDrillDownIconResizes = false; + + /** + The display object to use as an icon when the item renderer's data is a + branch. This icon is displayed in addition to the standard `icon` + property. + + @since 1.4.0 + **/ + @:style + public var drillDownIcon:DisplayObject = null; + + private function initializeDrillDownItemRendererTheme():Void { + #if !feathersui_disable_default_theme + feathers.themes.steel.components.SteelDrillDownItemRendererStyles.initialize(); + #end + } + + override private function update():Void { + var dataInvalid = this.isInvalid(DATA); + var stateInvalid = this.isInvalid(STATE); + var stylesInvalid = this.isInvalid(STYLES); + + if (stylesInvalid || stateInvalid || dataInvalid) { + this.refreshDrillDownIcon(); + } + + super.update(); + } + + override private function calculateExplicitWidthForTextMeasurement():Null { + var textFieldExplicitWidth = super.calculateExplicitWidthForTextMeasurement(); + if (textFieldExplicitWidth == null) { + return textFieldExplicitWidth; + } + var adjustedGap = this.gap; + // Math.POSITIVE_INFINITY bug workaround for swf + if (adjustedGap == (1.0 / 0.0)) { + adjustedGap = this.minGap; + } + if (this._currentDrillDownIcon != null) { + var oldgnoreDrillDownIconResizes = this._ignoreDrillDownIconResizes; + this._ignoreDrillDownIconResizes = true; + if ((this._currentDrillDownIcon is IValidating)) { + (cast this._currentDrillDownIcon : IValidating).validateNow(); + } + this._ignoreDrillDownIconResizes = oldgnoreDrillDownIconResizes; + textFieldExplicitWidth -= (this._currentDrillDownIcon.width + adjustedGap); + } + if (textFieldExplicitWidth < 0.0) { + // flash may sometimes render a TextField with negative width + // so make sure it is never smaller than 0.0 + textFieldExplicitWidth = 0.0; + } + return textFieldExplicitWidth; + } + + override private function measureContentWidth():Float { + var contentWidth = super.measureContentWidth(); + var oldgnoreDrillDownIconResizes = this._ignoreDrillDownIconResizes; + this._ignoreDrillDownIconResizes = true; + if ((this._currentDrillDownIcon is IValidating)) { + (cast this._currentDrillDownIcon : IValidating).validateNow(); + } + this._ignoreDrillDownIconResizes = oldgnoreDrillDownIconResizes; + var adjustedGap = this.gap; + // Math.POSITIVE_INFINITY bug workaround for swf + if (adjustedGap == (1.0 / 0.0)) { + adjustedGap = this.minGap; + } + if (this._currentDrillDownIcon != null) { + contentWidth += this._currentDrillDownIcon.width + adjustedGap; + } + return contentWidth; + } + + override private function measureContentMinWidth():Float { + var contentMinWidth = super.measureContentMinWidth(); + var oldgnoreDrillDownIconResizes = this._ignoreDrillDownIconResizes; + this._ignoreDrillDownIconResizes = true; + if ((this._currentDrillDownIcon is IValidating)) { + (cast this._currentDrillDownIcon : IValidating).validateNow(); + } + this._ignoreDrillDownIconResizes = oldgnoreDrillDownIconResizes; + var adjustedGap = this.gap; + // Math.POSITIVE_INFINITY bug workaround for swf + if (adjustedGap == (1.0 / 0.0)) { + adjustedGap = this.minGap; + } + if (this._currentDrillDownIcon != null) { + contentMinWidth += this._currentDrillDownIcon.width + adjustedGap; + } + return contentMinWidth; + } + + override private function layoutChildren():Void { + var oldgnoreDrillDownIconResizes = this._ignoreDrillDownIconResizes; + this._ignoreDrillDownIconResizes = true; + if ((this._currentDrillDownIcon is IValidating)) { + (cast this._currentDrillDownIcon : IValidating).validateNow(); + } + this._ignoreDrillDownIconResizes = oldgnoreDrillDownIconResizes; + var paddingRight = this.paddingRight; + var adjustedGap = this.gap; + // Math.POSITIVE_INFINITY bug workaround for swf + if (adjustedGap == (1.0 / 0.0)) { + adjustedGap = this.minGap; + } + this.runWithoutInvalidation(() -> { + var newPaddingRight = paddingRight; + if (this._currentDrillDownIcon != null) { + newPaddingRight += this._currentDrillDownIcon.width + adjustedGap; + } + this.paddingRight = newPaddingRight; + }); + super.layoutChildren(); + this.runWithoutInvalidation(() -> { + this.paddingRight = paddingRight; + }); + if (this._currentDrillDownIcon != null) { + this._currentDrillDownIcon.x = this.actualWidth - this.paddingRight - this._currentDrillDownIcon.width; + switch (this.verticalAlign) { + case TOP: + this._currentDrillDownIcon.y = this.paddingTop; + case BOTTOM: + this._currentDrillDownIcon.y = Math.max(this.paddingTop, + this.paddingTop + + this.actualHeight + - this.paddingTop + - this.paddingBottom + - this._currentDrillDownIcon.height); + case MIDDLE: + this._currentDrillDownIcon.y = Math.max(this.paddingTop, + this.paddingTop + (this.actualHeight - this.paddingTop - this.paddingBottom - this._currentDrillDownIcon.height) / 2.0); + default: + throw new ArgumentError("Unknown vertical align: " + this.verticalAlign); + } + } + } + + private function refreshDrillDownIcon():Void { + var oldIcon = this._currentDrillDownIcon; + this._currentDrillDownIcon = this.getCurrentDrillDownIcon(); + if (this._currentDrillDownIcon == oldIcon) { + return; + } + this.removeCurrentDrillDownIcon(oldIcon); + this.addCurrentDrillDownIcon(this._currentDrillDownIcon); + } + + private function getCurrentDrillDownIcon():DisplayObject { + if (this._branch) { + return this.drillDownIcon; + } + return null; + } + + private function removeCurrentDrillDownIcon(icon:DisplayObject):Void { + if (icon == null) { + return; + } + icon.removeEventListener(Event.RESIZE, drillDownItemRenderer_drillDownIcon_resizeHandler); + if ((icon is IProgrammaticSkin)) { + (cast icon : IProgrammaticSkin).uiContext = null; + } + if ((icon is IStateObserver)) { + (cast icon : IStateObserver).stateContext = null; + } + // we need to restore these values so that they won't be lost the + // next time that this icon is used for measurement + this._drillDownIconMeasurements.restore(icon); + if (icon.parent == this) { + this.removeChild(icon); + } + } + + private function addCurrentDrillDownIcon(icon:DisplayObject):Void { + if (icon == null) { + this._drillDownIconMeasurements = null; + return; + } + if ((icon is IUIControl)) { + (cast icon : IUIControl).initializeNow(); + } + if (this._drillDownIconMeasurements == null) { + this._drillDownIconMeasurements = new Measurements(icon); + } else { + this._drillDownIconMeasurements.save(icon); + } + if ((icon is IProgrammaticSkin)) { + (cast icon : IProgrammaticSkin).uiContext = this; + } + if ((icon is IStateObserver)) { + (cast icon : IStateObserver).stateContext = this; + } + icon.addEventListener(Event.RESIZE, drillDownItemRenderer_drillDownIcon_resizeHandler, false, 0, true); + this.addChild(icon); + } + + private function drillDownItemRenderer_drillDownIcon_resizeHandler(event:Event):Void { + if (this._ignoreDrillDownIconResizes) { + return; + } + this.setInvalid(STYLES); + } +} diff --git a/src/feathers/themes/steel/SteelTheme.hx b/src/feathers/themes/steel/SteelTheme.hx index 31a5a0b9..75a0e916 100644 --- a/src/feathers/themes/steel/SteelTheme.hx +++ b/src/feathers/themes/steel/SteelTheme.hx @@ -19,6 +19,7 @@ import feathers.themes.steel.components.SteelCollapsibleStyles; import feathers.themes.steel.components.SteelComboBoxStyles; import feathers.themes.steel.components.SteelDatePickerStyles; import feathers.themes.steel.components.SteelDrawerStyles; +import feathers.themes.steel.components.SteelDrillDownItemRendererStyles; import feathers.themes.steel.components.SteelFormItemStyles; import feathers.themes.steel.components.SteelFormStyles; import feathers.themes.steel.components.SteelGridViewStyles; @@ -86,6 +87,7 @@ class SteelTheme extends BaseSteelTheme { SteelComboBoxStyles.initialize(this); SteelDatePickerStyles.initialize(this); SteelDrawerStyles.initialize(this); + SteelDrillDownItemRendererStyles.initialize(this); SteelFormStyles.initialize(this); SteelFormItemStyles.initialize(this); SteelGridViewStyles.initialize(this); diff --git a/src/feathers/themes/steel/components/SteelDrillDownItemRendererStyles.hx b/src/feathers/themes/steel/components/SteelDrillDownItemRendererStyles.hx new file mode 100644 index 00000000..7c9d5af0 --- /dev/null +++ b/src/feathers/themes/steel/components/SteelDrillDownItemRendererStyles.hx @@ -0,0 +1,119 @@ +/* + Feathers UI + Copyright 2024 Bowler Hat LLC. All Rights Reserved. + + This program is free software. You can redistribute and/or modify it in + accordance with the terms of the accompanying license agreement. + */ + +package feathers.themes.steel.components; + +import feathers.skins.MultiSkin; +import feathers.controls.ToggleButtonState; +import feathers.controls.dataRenderers.DrillDownItemRenderer; +import feathers.skins.UnderlineSkin; +import feathers.style.Theme; +import feathers.themes.steel.BaseSteelTheme; +import feathers.utils.DeviceUtil; +import openfl.display.Shape; + +/** + Initialize "steel" styles for the `DrillDownItemRenderer` component. + + @since 1.4.0 +**/ +@:dox(hide) +@:access(feathers.themes.steel.BaseSteelTheme) +class SteelDrillDownItemRendererStyles { + public static function initialize(?theme:BaseSteelTheme):Void { + if (theme == null) { + theme = Std.downcast(Theme.fallbackTheme, BaseSteelTheme); + } + if (theme == null) { + return; + } + + var styleProvider = theme.styleProvider; + if (styleProvider.getStyleFunction(DrillDownItemRenderer, null) == null) { + styleProvider.setStyleFunction(DrillDownItemRenderer, null, function(itemRenderer:DrillDownItemRenderer):Void { + var isDesktop = DeviceUtil.isDesktop(); + + if (itemRenderer.backgroundSkin == null) { + var skin = new UnderlineSkin(); + skin.fill = theme.getContainerFill(); + skin.border = theme.getDividerBorder(); + skin.selectedFill = theme.getActiveThemeFill(); + skin.setFillForState(ToggleButtonState.DOWN(false), theme.getActiveThemeFill()); + if (isDesktop) { + skin.width = 26.0; + skin.height = 26.0; + skin.minWidth = 26.0; + skin.minHeight = 26.0; + } else { + skin.width = 44.0; + skin.height = 44.0; + skin.minWidth = 44.0; + skin.minHeight = 44.0; + } + itemRenderer.backgroundSkin = skin; + } + + if (itemRenderer.drillDownIcon == null) { + var drillDownIcon = new MultiSkin(); + + var defaultIcon = new Shape(); + drawDrillDownIcon(defaultIcon, theme.textColor, isDesktop); + drillDownIcon.defaultView = defaultIcon; + + var disabledIcon = new Shape(); + drawDrillDownIcon(disabledIcon, theme.disabledTextColor, isDesktop); + drillDownIcon.disabledView = disabledIcon; + + itemRenderer.drillDownIcon = drillDownIcon; + } + + if (itemRenderer.textFormat == null) { + itemRenderer.textFormat = theme.getTextFormat(); + } + if (itemRenderer.disabledTextFormat == null) { + itemRenderer.disabledTextFormat = theme.getDisabledTextFormat(); + } + if (itemRenderer.secondaryTextFormat == null) { + itemRenderer.secondaryTextFormat = theme.getSecondaryDetailTextFormat(); + } + if (itemRenderer.disabledSecondaryTextFormat == null) { + itemRenderer.disabledSecondaryTextFormat = theme.getDisabledDetailTextFormat(); + } + + itemRenderer.paddingTop = theme.smallPadding; + itemRenderer.paddingRight = theme.largePadding; + itemRenderer.paddingBottom = theme.smallPadding; + itemRenderer.paddingLeft = theme.largePadding; + itemRenderer.gap = theme.smallPadding; + + itemRenderer.horizontalAlign = LEFT; + }); + } + } + + private static function drawDrillDownIcon(icon:Shape, color:UInt, isDesktop:Bool):Void { + icon.graphics.beginFill(0xff00ff, 0.0); + if (isDesktop) { + icon.graphics.drawRect(0.0, 0.0, 3.0, 6.0); + } else { + icon.graphics.drawRect(0.0, 0.0, 5.0, 8.0); + } + icon.graphics.endFill(); + icon.graphics.lineStyle(1.0, color, 1, false, NORMAL, SQUARE); + if (isDesktop) { + icon.graphics.moveTo(0.5, 0.5); + icon.graphics.lineTo(2.5, 3.0); + icon.graphics.lineTo(0.5, 5.5); + } else { + icon.graphics.moveTo(0.5, 0.5); + icon.graphics.lineTo(4.5, 4.0); + icon.graphics.lineTo(0.5, 7.5); + } + icon.graphics.endFill(); + } +}