From ec2e8d24e1ed83a5c94498a9e9e962ba8ce98440 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Wed, 1 Apr 2026 13:19:12 -0600 Subject: [PATCH 01/11] feat: refactor ProgressBarWidget onto ScaledPVWidget / RTTank Switch ProgressBarWidget from PVWidget to ScaledPVWidget and replace the JFX ProgressBar control with RTTank (border width hardcoded to 0). ProgressBarWidget inherits format, precision, alarm limit lines and alarm colours from ScaledPVWidget. New properties scale_visible and show_minor_ticks (both off/on by default, matching Tank) are added. All existing .bob XML property names are preserved (limits_from_pv, minimum, maximum, fill_color, background_color, horizontal, log_scale) so existing files load without migration. ProgressBarRepresentation wires the same lookChanged / valueChanged / limitsChanged / orientationChanged pattern as TankRepresentation and maps background_color to both setBackground() and setEmptyColor() so the unfilled area matches the declared background. --- .../model/widgets/ProgressBarWidget.java | 106 +++--- .../widgets/ProgressBarRepresentation.java | 336 +++++++++--------- 2 files changed, 232 insertions(+), 210 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java index 193cb01ecf..7c023d0060 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2022 Oak Ridge National Laboratory. + * Copyright (c) 2015-2026 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,38 +7,59 @@ *******************************************************************************/ package org.csstudio.display.builder.model.widgets; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newBooleanPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propBackgroundColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFillColor; +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontal; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propLimitsFromPV; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propMaximum; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propMinimum; import static org.csstudio.display.builder.model.widgets.plots.PlotWidgetProperties.propLogscale; +import static org.csstudio.display.builder.model.widgets.TankWidget.propScaleVisible; +import static org.csstudio.display.builder.model.widgets.TankWidget.propShowMinorTicks; import java.util.Arrays; import java.util.List; +import org.csstudio.display.builder.model.Messages; import org.csstudio.display.builder.model.Version; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.WidgetCategory; import org.csstudio.display.builder.model.WidgetConfigurator; import org.csstudio.display.builder.model.WidgetDescriptor; import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.WidgetPropertyCategory; +import org.csstudio.display.builder.model.WidgetPropertyDescriptor; import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.NamedWidgetFonts; +import org.csstudio.display.builder.model.persist.WidgetFontService; import org.csstudio.display.builder.model.persist.XMLTags; import org.csstudio.display.builder.model.properties.CommonWidgetProperties; import org.csstudio.display.builder.model.properties.HorizontalAlignment; +import org.csstudio.display.builder.model.properties.WidgetFont; import org.phoebus.ui.color.WidgetColor; import org.phoebus.framework.persistence.XMLUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; -/** Widget that displays a progress bar +/** Widget that displays a progress bar with an optional numeric scale. + * + *

Extends {@link ScaledPVWidget} to inherit common scale/limit properties + * (min/max range, format, precision, alarm limit lines). The bar uses the + * same rendering engine as {@link TankWidget} ({@code RTTank}) so the scale + * and alarm-limit features are identical. + * + *

Existing {@code .bob} files load unchanged: {@code fill_color}, + * {@code background_color}, {@code horizontal}, {@code limits_from_pv}, + * {@code minimum}, {@code maximum} and {@code log_scale} keep the same + * XML names. New properties ({@code format}, {@code precision}, + * {@code scale_visible}, {@code show_minor_ticks}, alarm limit properties) + * are silently ignored by older Phoebus versions. + * * @author Kay Kasemir * @author Amanda Carpenter + * @author Heredie Delvalle — CLS, ScaledPVWidget refactoring, scale support */ @SuppressWarnings("nls") -public class ProgressBarWidget extends PVWidget +public class ProgressBarWidget extends ScaledPVWidget { /** Widget descriptor */ public static final WidgetDescriptor WIDGET_DESCRIPTOR = @@ -55,7 +76,7 @@ public Widget createWidget() } }; - /** Widget configurator to read legacy *.opi files*/ + /** Widget configurator to read legacy *.opi files */ private static class ProgressBarConfigurator extends WidgetConfigurator { public ProgressBarConfigurator(final Version xml_version) @@ -72,24 +93,21 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid if (xml_version.getMajor() < 2) { final ProgressBarWidget bar = (ProgressBarWidget) widget; - // BOY progress bar reserved room on top for limit markers, - // and on bottom for scale + // BOY reserved room on top for limit markers and on the bottom for + // a scale. This widget now actually has a scale, so only adjust for + // the marker area that has been removed. if (XMLUtil.getChildBoolean(xml, "show_markers").orElse(true)) { - // This widget has no markers on top, so move widget down and reduce height. - // There is no 'marker font', seems to have constant height final int reduce = 25; bar.propY().setValue(bar.propY().getValue() + reduce); bar.propHeight().setValue(bar.propHeight().getValue() - reduce); } - // Do use space below where BOY placed markers for the bar itself. - // In the future, there could be a scale. final Element el = XMLUtil.getChildElement(xml, "color_fillbackground"); if (el != null) bar.propBackgroundColor().readFromXML(model_reader, el); - // Create text update for the value indicator + // Create a companion TextUpdate widget for the BOY value label. if (XMLUtil.getChildBoolean(xml, "show_label").orElse(true)) { final Document doc = xml.getOwnerDocument(); @@ -125,31 +143,37 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) return new ProgressBarConfigurator(persisted_version); } - private volatile WidgetProperty limits_from_pv; - private volatile WidgetProperty minimum; - private volatile WidgetProperty maximum; - private volatile WidgetProperty log_scale; + private volatile WidgetProperty font; private volatile WidgetProperty fill_color; private volatile WidgetProperty background_color; - private volatile WidgetProperty horizontal; + private volatile WidgetProperty log_scale; + private volatile WidgetProperty horizontal; + private volatile WidgetProperty scale_visible; + private volatile WidgetProperty show_minor_ticks; /** Constructor */ public ProgressBarWidget() { - super(WIDGET_DESCRIPTOR.getType()); + super(WIDGET_DESCRIPTOR.getType(), 300, 30); } @Override protected void defineProperties(final List> properties) { super.defineProperties(properties); - properties.add(fill_color = propFillColor.createProperty(this, new WidgetColor(60, 255, 60))); + properties.add(font = propFont.createProperty(this, WidgetFontService.get(NamedWidgetFonts.DEFAULT))); + properties.add(fill_color = propFillColor.createProperty(this, new WidgetColor(60, 255, 60))); properties.add(background_color = propBackgroundColor.createProperty(this, new WidgetColor(250, 250, 250))); - properties.add(limits_from_pv = propLimitsFromPV.createProperty(this, true)); - properties.add(minimum = propMinimum.createProperty(this, 0.0)); - properties.add(maximum = propMaximum.createProperty(this, 100.0)); - properties.add(log_scale = propLogscale.createProperty(this, false)); - properties.add(horizontal = propHorizontal.createProperty(this, true)); + properties.add(log_scale = propLogscale.createProperty(this, false)); + properties.add(horizontal = propHorizontal.createProperty(this, true)); + properties.add(scale_visible = propScaleVisible.createProperty(this, false)); + properties.add(show_minor_ticks = propShowMinorTicks.createProperty(this, true)); + } + + /** @return 'font' property */ + public WidgetProperty propFont() + { + return font; } /** @return 'fill_color' property */ @@ -164,24 +188,6 @@ public WidgetProperty propBackgroundColor() return background_color; } - /** @return 'limits_from_pv' property */ - public WidgetProperty propLimitsFromPV() - { - return limits_from_pv; - } - - /** @return 'minimum' property */ - public WidgetProperty propMinimum() - { - return minimum; - } - - /** @return 'maximum' property */ - public WidgetProperty propMaximum() - { - return maximum; - } - /** @return 'log_scale' property */ public WidgetProperty propLogScale() { @@ -193,4 +199,16 @@ public WidgetProperty propHorizontal() { return horizontal; } + + /** @return 'scale_visible' property */ + public WidgetProperty propScaleVisible() + { + return scale_visible; + } + + /** @return 'show_minor_ticks' property */ + public WidgetProperty propShowMinorTicks() + { + return show_minor_ticks; + } } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java index 2aa9b529dc..fffffe944a 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2022 Oak Ridge National Laboratory. + * Copyright (c) 2015-2026 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,85 +7,136 @@ *******************************************************************************/ package org.csstudio.display.builder.representation.javafx.widgets; +import java.util.concurrent.TimeUnit; + import org.csstudio.display.builder.model.DirtyFlag; import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; import org.csstudio.display.builder.model.WidgetProperty; import org.csstudio.display.builder.model.WidgetPropertyListener; import org.csstudio.display.builder.model.util.VTypeUtil; import org.csstudio.display.builder.model.widgets.ProgressBarWidget; +import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.representation.javafx.JFXUtil; -import org.csstudio.javafx.rtplot.internal.util.Log10; +import org.csstudio.javafx.rtplot.RTTank; +import org.epics.util.stats.Range; import org.epics.vtype.Display; import org.epics.vtype.VType; -import org.phoebus.ui.javafx.Styles; -import javafx.scene.control.ProgressBar; +import javafx.scene.layout.Pane; import javafx.scene.transform.Rotate; import javafx.scene.transform.Translate; -/** Creates JavaFX item for model widget +/** Creates JavaFX item for model widget. + * + *

Uses {@link RTTank} as the rendering engine so the bar gains + * a numeric scale, configurable tick format/precision, and alarm + * limit lines at no extra maintenance cost. The tank body border + * is always hidden ({@code setBorderWidth(0)}) so the widget looks + * like a plain fill bar rather than a tank. + * * @author Kay Kasemir * @author Amanda Carpenter + * @author Heredie Delvalle — CLS, RTTank-based refactoring, scale support */ @SuppressWarnings("nls") -public class ProgressBarRepresentation extends RegionBaseRepresentation +public class ProgressBarRepresentation extends RegionBaseRepresentation { private final DirtyFlag dirty_look = new DirtyFlag(); - private final DirtyFlag dirty_value = new DirtyFlag(); - private final UntypedWidgetPropertyListener lookChangedListener = this::lookChanged; + private final UntypedWidgetPropertyListener lookListener = this::lookChanged; + private final UntypedWidgetPropertyListener valueListener = this::valueChanged; + private final UntypedWidgetPropertyListener limitsListener = this::limitsChanged; private final WidgetPropertyListener orientationChangedListener = this::orientationChanged; - private final UntypedWidgetPropertyListener valueChangedListener = this::valueChanged; - private final UntypedWidgetPropertyListener propretyChangedListener = this::propertyChanged; - private volatile double percentage = 0.0; + private volatile RTTank tank; @Override - public ProgressBar createJFXNode() throws Exception + public Pane createJFXNode() throws Exception { - final ProgressBar bar = new ProgressBar(); - return bar; + tank = new RTTank(); + // Never show the visual tank-body outline: this is a bar, not a tank. + tank.setBorderWidth(0); + tank.setUpdateThrottle(Preferences.image_update_delay, TimeUnit.MILLISECONDS); + return new Pane(tank); } @Override protected void registerListeners() { super.registerListeners(); - model_widget.propFillColor().addUntypedPropertyListener(lookChangedListener); - model_widget.propBackgroundColor().addUntypedPropertyListener(lookChangedListener); - model_widget.propWidth().addUntypedPropertyListener(lookChangedListener); - model_widget.propHeight().addUntypedPropertyListener(lookChangedListener); - model_widget.propLimitsFromPV().addUntypedPropertyListener(valueChangedListener); - model_widget.propMinimum().addUntypedPropertyListener(propretyChangedListener); - model_widget.propMaximum().addUntypedPropertyListener(propretyChangedListener); - model_widget.propLogScale().addUntypedPropertyListener(valueChangedListener); - model_widget.runtimePropValue().addUntypedPropertyListener(valueChangedListener); + model_widget.propWidth().addUntypedPropertyListener(lookListener); + model_widget.propHeight().addUntypedPropertyListener(lookListener); + model_widget.propFont().addUntypedPropertyListener(lookListener); + model_widget.propFillColor().addUntypedPropertyListener(lookListener); + model_widget.propBackgroundColor().addUntypedPropertyListener(lookListener); + model_widget.propScaleVisible().addUntypedPropertyListener(lookListener); + model_widget.propShowMinorTicks().addUntypedPropertyListener(lookListener); + model_widget.propLogScale().addUntypedPropertyListener(lookListener); + model_widget.propFormat().addUntypedPropertyListener(lookListener); + model_widget.propPrecision().addUntypedPropertyListener(lookListener); + model_widget.propMinorAlarmColor().addUntypedPropertyListener(lookListener); + model_widget.propMajorAlarmColor().addUntypedPropertyListener(lookListener); + + model_widget.propLimitsFromPV().addUntypedPropertyListener(valueListener); + model_widget.propMinimum().addUntypedPropertyListener(valueListener); + model_widget.propMaximum().addUntypedPropertyListener(valueListener); + model_widget.runtimePropValue().addUntypedPropertyListener(valueListener); + + model_widget.propShowAlarmLimits().addUntypedPropertyListener(limitsListener); + model_widget.propAlarmLimitsFromPV().addUntypedPropertyListener(limitsListener); + model_widget.propLevelLoLo().addUntypedPropertyListener(limitsListener); + model_widget.propLevelLow().addUntypedPropertyListener(limitsListener); + model_widget.propLevelHigh().addUntypedPropertyListener(limitsListener); + model_widget.propLevelHiHi().addUntypedPropertyListener(limitsListener); + model_widget.propHorizontal().addPropertyListener(orientationChangedListener); + + // Initial apply — range and fill first, then limits valueChanged(null, null, null); + limitsChanged(null, null, null); } @Override protected void unregisterListeners() { - model_widget.propFillColor().removePropertyListener(lookChangedListener); - model_widget.propBackgroundColor().removePropertyListener(lookChangedListener); - model_widget.propWidth().removePropertyListener(lookChangedListener); - model_widget.propHeight().removePropertyListener(lookChangedListener); - model_widget.propLimitsFromPV().removePropertyListener(valueChangedListener); - model_widget.propMinimum().removePropertyListener(propretyChangedListener); - model_widget.propMaximum().removePropertyListener(propretyChangedListener); - model_widget.propLogScale().removePropertyListener(valueChangedListener); - model_widget.runtimePropValue().removePropertyListener(valueChangedListener); + model_widget.propWidth().removePropertyListener(lookListener); + model_widget.propHeight().removePropertyListener(lookListener); + model_widget.propFont().removePropertyListener(lookListener); + model_widget.propFillColor().removePropertyListener(lookListener); + model_widget.propBackgroundColor().removePropertyListener(lookListener); + model_widget.propScaleVisible().removePropertyListener(lookListener); + model_widget.propShowMinorTicks().removePropertyListener(lookListener); + model_widget.propLogScale().removePropertyListener(lookListener); + model_widget.propFormat().removePropertyListener(lookListener); + model_widget.propPrecision().removePropertyListener(lookListener); + model_widget.propMinorAlarmColor().removePropertyListener(lookListener); + model_widget.propMajorAlarmColor().removePropertyListener(lookListener); + + model_widget.propLimitsFromPV().removePropertyListener(valueListener); + model_widget.propMinimum().removePropertyListener(valueListener); + model_widget.propMaximum().removePropertyListener(valueListener); + model_widget.runtimePropValue().removePropertyListener(valueListener); + + model_widget.propShowAlarmLimits().removePropertyListener(limitsListener); + model_widget.propAlarmLimitsFromPV().removePropertyListener(limitsListener); + model_widget.propLevelLoLo().removePropertyListener(limitsListener); + model_widget.propLevelLow().removePropertyListener(limitsListener); + model_widget.propLevelHigh().removePropertyListener(limitsListener); + model_widget.propLevelHiHi().removePropertyListener(limitsListener); + model_widget.propHorizontal().removePropertyListener(orientationChangedListener); super.unregisterListeners(); } + private void lookChanged(final WidgetProperty property, final Object old_value, final Object new_value) + { + dirty_look.mark(); + toolkit.scheduleUpdate(this); + } + private void orientationChanged(final WidgetProperty prop, final Boolean old, final Boolean horizontal) { - // When interactively changing orientation, swap width <-> height. - // This will only affect interactive changes once the widget is represented on the screen. - // Initially, when the widget is loaded from XML, the representation - // doesn't exist and the original width, height and orientation are applied - // without triggering a swap. + // When the user changes orientation in the editor, swap width ↔ height + // so the widget visually rotates rather than stretching. if (toolkit.isEditMode()) { final int w = model_widget.propWidth().getValue(); @@ -96,82 +147,77 @@ private void orientationChanged(final WidgetProperty prop, final Boolea lookChanged(prop, old, horizontal); } - private void propertyChanged(final WidgetProperty property, final Object old_value, final Object new_value) - { - lookChanged(property, old_value, new_value ); - valueChanged(property, old_value, new_value ); - } - - private void lookChanged(final WidgetProperty property, final Object old_value, final Object new_value) - { - dirty_look.mark(); - toolkit.scheduleUpdate(this); - } - + /** Update the display range and fill level. Called on every PV value change. */ private void valueChanged(final WidgetProperty property, final Object old_value, final Object new_value) { final VType vtype = model_widget.runtimePropValue().getValue(); - final boolean limits_from_pv = model_widget.propLimitsFromPV().getValue(); - - double min_val = 0; - double max_val = 0; - - // Inverted if low limit and higher than high limit - if (model_widget.propMaximum().getValue() > model_widget.propMinimum().getValue()) - { - min_val = model_widget.propMinimum().getValue(); - max_val = model_widget.propMaximum().getValue(); - } - else + double min_val = model_widget.propMinimum().getValue(); + double max_val = model_widget.propMaximum().getValue(); + if (model_widget.propLimitsFromPV().getValue()) { - max_val = model_widget.propMinimum().getValue(); - min_val = model_widget.propMaximum().getValue(); - } - - if (limits_from_pv) - { - // Try display range from PV final Display display_info = Display.displayOf(vtype); - if (display_info != null) + if (display_info != null && display_info.getDisplayRange().isFinite()) { min_val = display_info.getDisplayRange().getMinimum(); max_val = display_info.getDisplayRange().getMaximum(); } } - // Fall back to 0..100 range - if (min_val == max_val) - { - min_val = 0.0; - max_val = 100.0; - } + tank.setRange(min_val, max_val); + + // Re-read alarm limits from PV metadata on every value update. + if (model_widget.propAlarmLimitsFromPV().getValue()) + applyAlarmLimits(vtype); - // Determine percentage of value within the min..max range - final double value = VTypeUtil.getValueNumber(vtype).doubleValue(); - final double percentage; + final double value = toolkit.isEditMode() + ? (min_val + max_val) / 2 + : VTypeUtil.getValueNumber(vtype).doubleValue(); + tank.setValue(value); + } - if (model_widget.propLogScale().getValue()) + /** Re-apply alarm limit lines when a limit property changes. */ + private void limitsChanged(final WidgetProperty property, final Object old_value, final Object new_value) + { + applyAlarmLimits(model_widget.runtimePropValue().getValue()); + } + + /** Push the current alarm limits to the tank from PV metadata or widget properties. */ + private void applyAlarmLimits(final VType vtype) + { + if (!model_widget.propShowAlarmLimits().getValue()) + { + tank.setLimits(Double.NaN, Double.NaN, Double.NaN, Double.NaN); + return; + } + final double lolo, lo, hi, hihi; + if (model_widget.propAlarmLimitsFromPV().getValue()) { - final double d = Log10.log10(max_val) - Log10.log10(min_val); - if (d == 0) - percentage = Double.NaN; + final Display display_info = Display.displayOf(vtype); + if (display_info != null) + { + final Range minor = display_info.getWarningRange(); + final Range major = display_info.getAlarmRange(); + lo = minor.getMinimum(); + hi = minor.getMaximum(); + lolo = major.getMinimum(); + hihi = major.getMaximum(); + } else - percentage = (Log10.log10(value) - Log10.log10(min_val)) / d; + lolo = lo = hi = hihi = Double.NaN; } else - percentage = (value - min_val) / (max_val - min_val); - - // Limit to 0.0 .. 1.0 - if (percentage < 0.0 || !Double.isFinite(percentage)) - this.percentage = 0.0; - else if (percentage > 1.0) - this.percentage = 1.0; - else - this.percentage = percentage; - dirty_value.mark(); - toolkit.scheduleUpdate(this); + { + lolo = model_widget.propLevelLoLo().getValue(); + lo = model_widget.propLevelLow().getValue(); + hi = model_widget.propLevelHigh().getValue(); + hihi = model_widget.propLevelHiHi().getValue(); + } + tank.setLimits(lolo, lo, hi, hihi); + tank.setLimitsFromPV(model_widget.propAlarmLimitsFromPV().getValue()); } + /** Track whether orientation transforms are currently applied. */ + private boolean was_transformed = false; @Override public void updateChanges() @@ -179,86 +225,44 @@ public void updateChanges() super.updateChanges(); if (dirty_look.checkAndClear()) { - boolean horizontal = model_widget.propHorizontal().getValue(); - double width = model_widget.propWidth().getValue(); + double width = model_widget.propWidth().getValue(); double height = model_widget.propHeight().getValue(); - double min_val = model_widget.propMinimum().getValue(); - double max_val = model_widget.propMaximum().getValue(); - if (!horizontal) + // A horizontal bar is rendered by RTTank as if vertical (RTTank is + // always vertical internally) and then rotated 90° clockwise. + if (model_widget.propHorizontal().getValue()) { - jfx_node.getTransforms().setAll( - new Translate(0, height), - new Rotate(-90, 0, 0)); - jfx_node.setPrefSize(height, width); - - if (min_val > max_val) - { - jfx_node.getTransforms().setAll( - new Translate(0, height), - new Rotate(-90, 0, 0, 0), - new Translate(height, 0), - new Rotate(180, 0, 0, 0, Rotate.Y_AXIS)); - jfx_node.setPrefSize(height, width); - } + tank.getTransforms().setAll(new Translate(width, 0), + new Rotate(90, 0, 0)); + was_transformed = true; + tank.setWidth(height); + tank.setHeight(width); } else { - jfx_node.getTransforms().clear(); - jfx_node.setPrefSize(width, height); - - if (min_val > max_val) - { - jfx_node.getTransforms().setAll( - new Translate(width, 0), - new Rotate(180, 0, 0, 0, Rotate.Y_AXIS)); - } + if (was_transformed) + tank.getTransforms().clear(); + was_transformed = false; + tank.setWidth(width); + tank.setHeight(height); } + jfx_node.setPrefSize(width, height); - // Default 'inset' of .bar uses 7 pixels. - // A widget sized 15 has 8 pixels left for the bar. - // Select leaner style where .bar uses full size. - Styles.update(jfx_node, "SmallBar", - Math.min(width, height) <= 15); - - // Could clear style and use setBackground(), - // but result is very plain. - // Tweaking the color used by CSS keeps overall style. - // See also http://stackoverflow.com/questions/13467259/javafx-how-to-change-progressbar-color-dynamically - final StringBuilder style = new StringBuilder(); - - // Color of the progress bar / foreground - style.append("-fx-accent: ").append(JFXUtil.webRGB( - JFXUtil.convert( - model_widget.propFillColor().getValue() - ) - )).append(" !important; "); - - // Color of the background underneath the progress bar - // Note per moderna.css the background is actually three layers of color - // with fx-shadow-highlight-color on the bottom, - // then fx-text-box-border, - // and finally fx-control-inner-background on top, all stacked in place with offsets. - // This gives the illusion of having a bordered box with a shadow instead of actually being a - // bordered box with a shadow... - // Fortunately, the bottom-most color (the 'shadow') is already transparent so we can leave it alone - // Unfortunately, the middle color (the "border" color) is a solid gray color (#ececec), so we must - // override it with its rgba equivalent so that it has transparency matching the picked background color. - style.append("-fx-control-inner-background: ") - .append(JFXUtil.webRGB( - JFXUtil.convert( - model_widget.propBackgroundColor().getValue())) - ) - .append(";"); - style.append("-fx-text-box-border: rgba(236, 236, 236, ") - .append(JFXUtil.webAlpha(model_widget.propBackgroundColor().getValue())) - .append(");"); - style.append("-fx-shadow-highlight-color: rgba(236, 236, 236, ") - .append(JFXUtil.webAlpha(model_widget.propBackgroundColor().getValue())) - .append(");"); - jfx_node.setStyle(style.toString()); + tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); + // Background is the outer canvas margin; emptyColor is the unfilled bar portion. + // Map both to background_color so the whole widget has a uniform background. + final javafx.scene.paint.Color bg = JFXUtil.convert(model_widget.propBackgroundColor().getValue()); + tank.setBackground(bg); + tank.setEmptyColor(bg); + tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); + tank.setScaleVisible(model_widget.propScaleVisible().getValue()); + tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); + tank.setLogScale(model_widget.propLogScale().getValue()); + tank.setLabelFormat(model_widget.propFormat().getValue(), + model_widget.propPrecision().getValue()); + tank.setAlarmColors( + JFXUtil.convert(model_widget.propMinorAlarmColor().getValue()), + JFXUtil.convert(model_widget.propMajorAlarmColor().getValue())); } - if (dirty_value.checkAndClear()) - jfx_node.setProgress(percentage); } } From 9edd355db41de119dada7a84fc374477ca97e0f5 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Wed, 1 Apr 2026 13:40:22 -0600 Subject: [PATCH 02/11] refactor: lift scale-display props to ScaledPVWidget; add dual-scale and perpendicular labels to ProgressBar Move propScaleVisible, propShowMinorTicks, propOppositeScaleVisible, and propPerpendicularTickLabels from TankWidget to their proper home in ScaledPVWidget. This removes the TankWidget import smell in ProgressBarWidget and makes all four properties available to any ScaledPVWidget subclass. ProgressBarWidget gains opposite_scale_visible and perpendicular_tick_labels, wired through ProgressBarRepresentation to RTTank.setRightScaleVisible() and RTTank.setPerpendicularTickLabels() respectively. --- .../model/widgets/ProgressBarWidget.java | 23 +++++++++++++++---- .../builder/model/widgets/ScaledPVWidget.java | 20 ++++++++++++++++ .../builder/model/widgets/TankWidget.java | 21 ----------------- .../widgets/ProgressBarRepresentation.java | 6 +++++ 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java index 7c023d0060..b8e753e9fa 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java @@ -13,8 +13,7 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontal; import static org.csstudio.display.builder.model.widgets.plots.PlotWidgetProperties.propLogscale; -import static org.csstudio.display.builder.model.widgets.TankWidget.propScaleVisible; -import static org.csstudio.display.builder.model.widgets.TankWidget.propShowMinorTicks; + import java.util.Arrays; import java.util.List; @@ -150,6 +149,8 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) private volatile WidgetProperty horizontal; private volatile WidgetProperty scale_visible; private volatile WidgetProperty show_minor_ticks; + private volatile WidgetProperty opposite_scale_visible; + private volatile WidgetProperty perpendicular_tick_labels; /** Constructor */ public ProgressBarWidget() @@ -166,8 +167,10 @@ protected void defineProperties(final List> properties) properties.add(background_color = propBackgroundColor.createProperty(this, new WidgetColor(250, 250, 250))); properties.add(log_scale = propLogscale.createProperty(this, false)); properties.add(horizontal = propHorizontal.createProperty(this, true)); - properties.add(scale_visible = propScaleVisible.createProperty(this, false)); - properties.add(show_minor_ticks = propShowMinorTicks.createProperty(this, true)); + properties.add(scale_visible = propScaleVisible.createProperty(this, false)); + properties.add(show_minor_ticks = propShowMinorTicks.createProperty(this, true)); + properties.add(opposite_scale_visible = propOppositeScaleVisible.createProperty(this, false)); + properties.add(perpendicular_tick_labels = propPerpendicularTickLabels.createProperty(this, false)); } /** @return 'font' property */ @@ -211,4 +214,16 @@ public WidgetProperty propShowMinorTicks() { return show_minor_ticks; } + + /** @return 'opposite_scale_visible' property */ + public WidgetProperty propOppositeScaleVisible() + { + return opposite_scale_visible; + } + + /** @return 'perpendicular_tick_labels' property */ + public WidgetProperty propPerpendicularTickLabels() + { + return perpendicular_tick_labels; + } } diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java index 6f8e5ecd11..3597278292 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java @@ -125,6 +125,26 @@ public EnumWidgetProperty createProperty(final Widget widget, newColorPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "major_alarm_color", Messages.WidgetProperties_MajorAlarmColor); + /** 'scale_visible' — show the numeric scale (tick marks and labels) */ + public static final WidgetPropertyDescriptor propScaleVisible = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "scale_visible", + Messages.WidgetProperties_ScaleVisible); + + /** 'show_minor_ticks' — show minor tick marks on the scale */ + public static final WidgetPropertyDescriptor propShowMinorTicks = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "show_minor_ticks", + Messages.WidgetProperties_ShowMinorTicks); + + /** 'opposite_scale_visible' — show a second scale on the opposite side */ + public static final WidgetPropertyDescriptor propOppositeScaleVisible = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "opposite_scale_visible", + Messages.WidgetProperties_OppositeScaleVisible); + + /** 'perpendicular_tick_labels' — draw scale labels perpendicular to the axis */ + public static final WidgetPropertyDescriptor propPerpendicularTickLabels = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "perpendicular_tick_labels", + Messages.WidgetProperties_PerpendicularTickLabels); + // ---- Instance fields ------------------------------------------------ private volatile WidgetProperty format; diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java index 48666c55da..61b8b7ca30 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java @@ -7,7 +7,6 @@ *******************************************************************************/ package org.csstudio.display.builder.model.widgets; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newBooleanPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newColorPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newIntegerPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propBackgroundColor; @@ -84,26 +83,6 @@ public Widget createWidget() /** 'empty_color' */ public static final WidgetPropertyDescriptor propEmptyColor = newColorPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "empty_color", Messages.WidgetProperties_EmptyColor); - /** 'scale_visible' */ - public static final WidgetPropertyDescriptor propScaleVisible = - newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "scale_visible", Messages.WidgetProperties_ScaleVisible); - - /** 'show_minor_ticks' */ - public static final WidgetPropertyDescriptor propShowMinorTicks = - newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "show_minor_ticks", Messages.WidgetProperties_ShowMinorTicks); - - /** 'perpendicular_tick_labels' — draw scale labels perpendicular - * to the axis direction (horizontal text beside vertical scale) - */ - public static final WidgetPropertyDescriptor propPerpendicularTickLabels = - newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "perpendicular_tick_labels", Messages.WidgetProperties_PerpendicularTickLabels); - - /** 'opposite_scale_visible' — show a second scale on the opposite - * side of the tank (right for vertical, bottom for horizontal). - * Inspired by CS-Studio BOY which could show markers on both sides. - */ - public static final WidgetPropertyDescriptor propOppositeScaleVisible = - newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "opposite_scale_visible", Messages.WidgetProperties_OppositeScaleVisible); /** Widget configurator to read legacy *.opi files*/ private static class CustomConfigurator extends WidgetConfigurator diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java index fffffe944a..026c872bff 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java @@ -70,6 +70,8 @@ protected void registerListeners() model_widget.propBackgroundColor().addUntypedPropertyListener(lookListener); model_widget.propScaleVisible().addUntypedPropertyListener(lookListener); model_widget.propShowMinorTicks().addUntypedPropertyListener(lookListener); + model_widget.propOppositeScaleVisible().addUntypedPropertyListener(lookListener); + model_widget.propPerpendicularTickLabels().addUntypedPropertyListener(lookListener); model_widget.propLogScale().addUntypedPropertyListener(lookListener); model_widget.propFormat().addUntypedPropertyListener(lookListener); model_widget.propPrecision().addUntypedPropertyListener(lookListener); @@ -105,6 +107,8 @@ protected void unregisterListeners() model_widget.propBackgroundColor().removePropertyListener(lookListener); model_widget.propScaleVisible().removePropertyListener(lookListener); model_widget.propShowMinorTicks().removePropertyListener(lookListener); + model_widget.propOppositeScaleVisible().removePropertyListener(lookListener); + model_widget.propPerpendicularTickLabels().removePropertyListener(lookListener); model_widget.propLogScale().removePropertyListener(lookListener); model_widget.propFormat().removePropertyListener(lookListener); model_widget.propPrecision().removePropertyListener(lookListener); @@ -257,6 +261,8 @@ public void updateChanges() tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); tank.setScaleVisible(model_widget.propScaleVisible().getValue()); tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); + tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); + tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); tank.setLogScale(model_widget.propLogScale().getValue()); tank.setLabelFormat(model_widget.propFormat().getValue(), model_widget.propPrecision().getValue()); From b66167e8062dfe841f25dd905863a61ae72542f5 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Wed, 1 Apr 2026 13:49:29 -0600 Subject: [PATCH 03/11] refactor: move propBorderWidth to ScaledPVWidget; add border_width to ProgressBar propTankBorderWidth (XML: 'tank_border_width') removed from TankWidget. Replaced by propBorderWidth (XML: 'border_width') in ScaledPVWidget so all scaled widgets share the same descriptor. TankWidget.CustomConfigurator migrates old 'tank_border_width' XML elements to 'border_width' transparently, so existing .bob files still load. ProgressBarWidget gains border_width_prop (default 0), wired to RTTank.setBorderWidth() in ProgressBarRepresentation. --- .../builder/model/widgets/ProgressBarWidget.java | 8 ++++++++ .../builder/model/widgets/ScaledPVWidget.java | 5 +++++ .../display/builder/model/widgets/TankWidget.java | 15 ++++++--------- .../javafx/widgets/ProgressBarRepresentation.java | 3 +++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java index b8e753e9fa..a4c5088988 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java @@ -151,6 +151,7 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) private volatile WidgetProperty show_minor_ticks; private volatile WidgetProperty opposite_scale_visible; private volatile WidgetProperty perpendicular_tick_labels; + private volatile WidgetProperty border_width_prop; /** Constructor */ public ProgressBarWidget() @@ -171,6 +172,7 @@ protected void defineProperties(final List> properties) properties.add(show_minor_ticks = propShowMinorTicks.createProperty(this, true)); properties.add(opposite_scale_visible = propOppositeScaleVisible.createProperty(this, false)); properties.add(perpendicular_tick_labels = propPerpendicularTickLabels.createProperty(this, false)); + properties.add(border_width_prop = propBorderWidth.createProperty(this, 0)); } /** @return 'font' property */ @@ -226,4 +228,10 @@ public WidgetProperty propPerpendicularTickLabels() { return perpendicular_tick_labels; } + + /** @return 'border_width' property (0 = no border) */ + public WidgetProperty propBorderWidth() + { + return border_width_prop; + } } diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java index 3597278292..2f221f94f4 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java @@ -145,6 +145,11 @@ public EnumWidgetProperty createProperty(final Widget widget, newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "perpendicular_tick_labels", Messages.WidgetProperties_PerpendicularTickLabels); + /** 'border_width' — width in pixels of the border drawn around the widget (0..5) */ + public static final WidgetPropertyDescriptor propBorderWidth = + newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "border_width", + Messages.WidgetProperties_BorderWidth, 0, 5); + // ---- Instance fields ------------------------------------------------ private volatile WidgetProperty format; diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java index 61b8b7ca30..2a653a49e5 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java @@ -8,7 +8,6 @@ package org.csstudio.display.builder.model.widgets; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newColorPropertyDescriptor; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newIntegerPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propBackgroundColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFillColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; @@ -73,13 +72,6 @@ public Widget createWidget() } }; - /** 'tank_border_width' — width in pixels of the border drawn around the - * tank body; 0 (default) means no border, preserving the original look. - */ - public static final WidgetPropertyDescriptor propTankBorderWidth = - newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "tank_border_width", - Messages.WidgetProperties_BorderWidth, 0, 5); - /** 'empty_color' */ public static final WidgetPropertyDescriptor propEmptyColor = newColorPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "empty_color", Messages.WidgetProperties_EmptyColor); @@ -99,6 +91,11 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid if (! super.configureFromXML(model_reader, widget, xml)) return false; + // Migrate old 'tank_border_width' XML key to the renamed common 'border_width'. + final Element bw_compat = XMLUtil.getChildElement(xml, "tank_border_width"); + if (bw_compat != null) + ((TankWidget) widget).propBorderWidth().readFromXML(model_reader, bw_compat); + if (xml_version.getMajor() < 2) { final TankWidget tank = (TankWidget) widget; @@ -175,7 +172,7 @@ protected void defineProperties(final List> properties) properties.add(perpendicular_tick_labels = propPerpendicularTickLabels.createProperty(this, false)); properties.add(log_scale = propLogscale.createProperty(this, false)); properties.add(horizontal = propHorizontal.createProperty(this, false)); - properties.add(border_width_prop = propTankBorderWidth.createProperty(this, 0)); + properties.add(border_width_prop = propBorderWidth.createProperty(this, 0)); } @Override diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java index 026c872bff..52ed394a48 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java @@ -72,6 +72,7 @@ protected void registerListeners() model_widget.propShowMinorTicks().addUntypedPropertyListener(lookListener); model_widget.propOppositeScaleVisible().addUntypedPropertyListener(lookListener); model_widget.propPerpendicularTickLabels().addUntypedPropertyListener(lookListener); + model_widget.propBorderWidth().addUntypedPropertyListener(lookListener); model_widget.propLogScale().addUntypedPropertyListener(lookListener); model_widget.propFormat().addUntypedPropertyListener(lookListener); model_widget.propPrecision().addUntypedPropertyListener(lookListener); @@ -109,6 +110,7 @@ protected void unregisterListeners() model_widget.propShowMinorTicks().removePropertyListener(lookListener); model_widget.propOppositeScaleVisible().removePropertyListener(lookListener); model_widget.propPerpendicularTickLabels().removePropertyListener(lookListener); + model_widget.propBorderWidth().removePropertyListener(lookListener); model_widget.propLogScale().removePropertyListener(lookListener); model_widget.propFormat().removePropertyListener(lookListener); model_widget.propPrecision().removePropertyListener(lookListener); @@ -263,6 +265,7 @@ public void updateChanges() tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); + tank.setBorderWidth(model_widget.propBorderWidth().getValue()); tank.setLogScale(model_widget.propLogScale().getValue()); tank.setLabelFormat(model_widget.propFormat().getValue(), model_widget.propPrecision().getValue()); From a14ef6bf7e8afe308835714df875ae9f54ab719c Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Wed, 1 Apr 2026 15:01:13 -0600 Subject: [PATCH 04/11] fix(rtplot): apply label-format override in LinearTicks.compute() and format() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In compute(), relabelTicks() is now called after the major tick list is built so that the user-specified NumberFormat override (set via setLabelFormat()) takes effect for all ticks, including the forced boundary ticks added by the guard at the end. format() now checks getLabelFormatOverride() directly so that one-off calls (e.g. from axis tooltip code) also respect the override, rather than always falling back to num_fmt. RTTank.significantDigitsFormat() now post-processes String.format('%g') output via normaliseExponent(): uppercase E, no leading zeros on the exponent, no '+' sign — matching the style produced by LinearTicks.createExponentialFormat() and standard scientific notation (MATLAB, NumPy, LabVIEW). --- .../org/csstudio/javafx/rtplot/RTTank.java | 23 +++++++++++++++++-- .../javafx/rtplot/internal/LinearTicks.java | 12 +++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index 670417280f..d7dd14138e 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -335,12 +335,12 @@ private static NumberFormat significantDigitsFormat(final int prec) @Override public StringBuffer format(final double v, final StringBuffer buf, final java.text.FieldPosition pos) { - return buf.append(String.format(java.util.Locale.ROOT, pattern, v)); + return buf.append(normaliseExponent(String.format(java.util.Locale.ROOT, pattern, v))); } @Override public StringBuffer format(final long v, final StringBuffer buf, final java.text.FieldPosition pos) { - return buf.append(String.format(java.util.Locale.ROOT, pattern, (double) v)); + return buf.append(normaliseExponent(String.format(java.util.Locale.ROOT, pattern, (double) v))); } @Override public Number parse(final String s, final java.text.ParsePosition pos) @@ -350,6 +350,25 @@ public Number parse(final String s, final java.text.ParsePosition pos) }; } + /** Normalise a {@code %g}-formatted string to match Phoebus axis convention: + * uppercase {@code E}, no leading zeros on the exponent, no {@code +} sign. + * Examples: {@code "1.0e-01"} → {@code "1.0E-1"}, + * {@code "2.5e+02"} → {@code "2.5E2"}. + */ + private static String normaliseExponent(final String s) + { + final int e = s.indexOf('e'); + if (e < 0) + return s; // decimal notation — no exponent to fix + final String mantissa = s.substring(0, e); + String exp = s.substring(e + 1); // e.g. "-01", "+02" + final boolean neg = exp.startsWith("-"); + exp = exp.replaceFirst("^[+-]?0*", ""); // strip sign & leading zeros + if (exp.isEmpty()) + exp = "0"; + return mantissa + "E" + (neg ? "-" : "") + exp; + } + /** Set alarm and warning limit values to display as horizontal lines on the tank. * Pass {@link Double#NaN} for any limit that should not be shown. * @param lolo LOLO (major alarm) lower limit diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java index db7c6c6c61..9e3e382313 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java @@ -280,6 +280,12 @@ public void compute(Double low, Double high, final Graphics2D gc, final int scre major_ticks.add(0, new MajorTick<>(low, format(low))); major_ticks.add(new MajorTick<>(high, format(high))); } + + // Apply user-specified label format override if set. + final NumberFormat override = getLabelFormatOverride(); + if (override != null) + relabelTicks(major_ticks, override); + this.major_ticks = major_ticks; this.minor_ticks = minor_ticks; } @@ -385,9 +391,9 @@ public String format(final Double num) return "Inf"; // Patch numbers that are "very close to zero" // to avoid "-0.00" or "0.0e-22" - if (Math.abs(num) < zero_threshold) - return num_fmt.format(0.0); - return num_fmt.format(num); + final double val = Math.abs(num) < zero_threshold ? 0.0 : num; + final NumberFormat override = getLabelFormatOverride(); + return (override != null) ? override.format(val) : num_fmt.format(val); } /** {@inheritDoc} */ From bc5f73fda292608678b2bb0cd2dd3924de05e1cf Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Wed, 1 Apr 2026 15:01:56 -0600 Subject: [PATCH 05/11] cleanup: dead imports, stale javadoc, misleading comments; improve EDM/BOY converters ScaledPVWidget: fix javadoc ('show_limits' -> 'show_alarm_limits'); reorder imports to standard Java convention (static, java.*, org.*). TankWidget: remove the tank_border_width XML migration block that was added prematurely before any .bob files using that key existed. ProgressBarWidget: drop three dead imports left over after property descriptors moved to ScaledPVWidget. Add BOY OPI migration for -> scale_visible and -> font, which do not auto-map because their XML element names differ. ProgressBarRepresentation: remove stale setBorderWidth(0) call and its misleading 'always hidden' comment from createJFXNode(); the property is now user-configurable. Update class javadoc to match. Convert_activeBarClass (EDM -> Phoebus): map five previously ignored EDM properties: showScale -> scale_visible; precision (when > 0); min/max when limitsFromDb is false and the range is valid; border boolean -> 1 px border_width. --- .../edm/widgets/Convert_activeBarClass.java | 16 ++++++++++++++++ .../builder/model/widgets/ProgressBarWidget.java | 13 ++++++++++--- .../builder/model/widgets/ScaledPVWidget.java | 6 +++--- .../builder/model/widgets/TankWidget.java | 5 ----- .../widgets/ProgressBarRepresentation.java | 11 ++++------- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeBarClass.java b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeBarClass.java index a7acd31982..44a4bdde0b 100644 --- a/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeBarClass.java +++ b/app/display/convert-edm/src/main/java/org/csstudio/display/converter/edm/widgets/Convert_activeBarClass.java @@ -28,7 +28,23 @@ public Convert_activeBarClass(final EdmConverter converter, final Widget parent, convertColor(r.getBgColor(), widget.propBackgroundColor()); widget.propHorizontal().setValue(!"vertical".equals(r.getOrientation())); widget.propPVName().setValue(convertPVName(r.getIndicatorPv())); + + // Scale and range + widget.propScaleVisible().setValue(r.isShowScale()); widget.propLimitsFromPV().setValue(r.isLimitsFromDb()); + if (!r.isLimitsFromDb() && r.getMax() > r.getMin()) + { + widget.propMinimum().setValue(r.getMin()); + widget.propMaximum().setValue(r.getMax()); + } + + // Precision: 0 in EDM usually means "not set" — only override when positive. + if (r.getPrecision() > 0) + widget.propPrecision().setValue(r.getPrecision()); + + // EDM 'border' is a boolean; map it to a 1 px border when set. + if (r.isBorder()) + widget.propBorderWidth().setValue(1); } @Override diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java index a4c5088988..caba522ef5 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java @@ -7,7 +7,6 @@ *******************************************************************************/ package org.csstudio.display.builder.model.widgets; -import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newBooleanPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propBackgroundColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFillColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; @@ -25,8 +24,6 @@ import org.csstudio.display.builder.model.WidgetConfigurator; import org.csstudio.display.builder.model.WidgetDescriptor; import org.csstudio.display.builder.model.WidgetProperty; -import org.csstudio.display.builder.model.WidgetPropertyCategory; -import org.csstudio.display.builder.model.WidgetPropertyDescriptor; import org.csstudio.display.builder.model.persist.ModelReader; import org.csstudio.display.builder.model.persist.NamedWidgetFonts; import org.csstudio.display.builder.model.persist.WidgetFontService; @@ -106,6 +103,16 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid if (el != null) bar.propBackgroundColor().readFromXML(model_reader, el); + // BOY's 'show_scale' boolean maps to our 'scale_visible' property. + final Element showScaleEl = XMLUtil.getChildElement(xml, "show_scale"); + if (showScaleEl != null) + bar.propScaleVisible().readFromXML(model_reader, showScaleEl); + + // BOY's 'scale_font' maps to our 'font' property. + final Element scaleFontEl = XMLUtil.getChildElement(xml, "scale_font"); + if (scaleFontEl != null) + bar.propFont().readFromXML(model_reader, scaleFontEl); + // Create a companion TextUpdate widget for the BOY value label. if (XMLUtil.getChildBoolean(xml, "show_label").orElse(true)) { diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java index 2f221f94f4..15d9177622 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java @@ -22,10 +22,10 @@ import org.csstudio.display.builder.model.WidgetProperty; import org.csstudio.display.builder.model.WidgetPropertyCategory; import org.csstudio.display.builder.model.WidgetPropertyDescriptor; -import org.phoebus.ui.color.NamedWidgetColors; -import org.phoebus.ui.color.WidgetColorService; import org.csstudio.display.builder.model.properties.EnumWidgetProperty; +import org.phoebus.ui.color.NamedWidgetColors; import org.phoebus.ui.color.WidgetColor; +import org.phoebus.ui.color.WidgetColorService; import org.phoebus.ui.vtype.ScaleFormat; /** Base class for PV widgets that display a numeric value on a scale @@ -46,7 +46,7 @@ * overrides the manual LOLO/LO/HI/HIHI levels. New property; * old Phoebus silently ignores the XML element. *

  • Manual {@code minimum} / {@code maximum} range.
  • - *
  • A {@code show_limits} toggle for alarm-limit visual markers.
  • + *
  • A {@code show_alarm_limits} toggle for alarm-limit visual markers.
  • *
  • Manual LOLO / LO / HI / HIHI thresholds (NaN = inactive).
  • *
  • Configurable minor/major alarm colours defaulting to the named * {@code ALARM_MINOR} / {@code ALARM_MAJOR} palette entries.
  • diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java index 2a653a49e5..918711c5c6 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java @@ -91,11 +91,6 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid if (! super.configureFromXML(model_reader, widget, xml)) return false; - // Migrate old 'tank_border_width' XML key to the renamed common 'border_width'. - final Element bw_compat = XMLUtil.getChildElement(xml, "tank_border_width"); - if (bw_compat != null) - ((TankWidget) widget).propBorderWidth().readFromXML(model_reader, bw_compat); - if (xml_version.getMajor() < 2) { final TankWidget tank = (TankWidget) widget; diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java index 52ed394a48..ef2e583a25 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java @@ -28,11 +28,10 @@ /** Creates JavaFX item for model widget. * - *

    Uses {@link RTTank} as the rendering engine so the bar gains - * a numeric scale, configurable tick format/precision, and alarm - * limit lines at no extra maintenance cost. The tank body border - * is always hidden ({@code setBorderWidth(0)}) so the widget looks - * like a plain fill bar rather than a tank. + *

    Uses {@link RTTank} as the rendering engine so the bar + * gains a numeric scale, configurable tick format and precision, + * optional second scale, and alarm limit lines — all at no extra + * maintenance cost versus the plain JavaFX {@code ProgressBar}. * * @author Kay Kasemir * @author Amanda Carpenter @@ -53,8 +52,6 @@ public class ProgressBarRepresentation extends RegionBaseRepresentation Date: Wed, 1 Apr 2026 17:24:41 -0600 Subject: [PATCH 06/11] perf(rtplot): pre-compile exponent-strip regex in normaliseExponent() String.replaceFirst() recompiles the Pattern on every call. Move the pattern to a static final EXP_LEADING_ZEROS constant so it is compiled once at class-load time. --- .../java/org/csstudio/javafx/rtplot/RTTank.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index d7dd14138e..46552f5f89 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -16,6 +16,7 @@ import java.awt.image.BufferedImage; import java.text.NumberFormat; import java.util.Objects; +import java.util.regex.Pattern; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -350,6 +351,11 @@ public Number parse(final String s, final java.text.ParsePosition pos) }; } + /** Pre-compiled pattern for stripping the sign and leading zeros from a + * {@code %g} exponent string such as {@code "-01"} or {@code "+02"}. + */ + private static final Pattern EXP_LEADING_ZEROS = Pattern.compile("^[+-]?0*"); + /** Normalise a {@code %g}-formatted string to match Phoebus axis convention: * uppercase {@code E}, no leading zeros on the exponent, no {@code +} sign. * Examples: {@code "1.0e-01"} → {@code "1.0E-1"}, @@ -361,12 +367,10 @@ private static String normaliseExponent(final String s) if (e < 0) return s; // decimal notation — no exponent to fix final String mantissa = s.substring(0, e); - String exp = s.substring(e + 1); // e.g. "-01", "+02" - final boolean neg = exp.startsWith("-"); - exp = exp.replaceFirst("^[+-]?0*", ""); // strip sign & leading zeros - if (exp.isEmpty()) - exp = "0"; - return mantissa + "E" + (neg ? "-" : "") + exp; + final String raw = s.substring(e + 1); // e.g. "-01", "+02" + final boolean neg = raw.startsWith("-"); + final String digits = EXP_LEADING_ZEROS.matcher(raw).replaceFirst(""); + return mantissa + "E" + (neg ? "-" : "") + (digits.isEmpty() ? "0" : digits); } /** Set alarm and warning limit values to display as horizontal lines on the tank. From 392f8bc4f3e9bbd50c4264bda939c873cb495a0f Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 3 Apr 2026 10:04:50 -0600 Subject: [PATCH 07/11] feat(display): RTTank-backed ProgressBar with scale + parallel rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related improvements to widget rendering performance and architecture: ## 1. RTTank parallel rendering (rtplot) Root cause: UpdateThrottle.TIMER is a single-thread shared executor. On displays with many Tank/ProgressBar widgets all renders serialise on one thread, capping throughput at ~5 s/sweep for 200 widgets. Fix: RTTank constructor now passes Activator.thread_pool (N-core pool) when parallel_rendering=true, TIMER otherwise. Default is false — no behaviour change for existing installs. New preference: org.csstudio.javafx.rtplot/parallel_rendering (default false) ## 2. ProgressBar scale-mode rendering (display) New preference org.csstudio.display.builder.representation/progressbar_scale_mode: - false (default): stock JFX ProgressBar — original behaviour, no change - true: RTProgressBarRepresentation backed by RTTank, adding a numeric scale, tick format/precision, dual scale, alarm-limit lines, and parallel rendering Architecture: - RTScaledWidgetRepresentation: new abstract base class that eliminates ~90 % duplication between TankRepresentation and RTProgressBarRepresentation. Handles value/range updates, alarm limits, orientation transforms, and throttle wiring in one place. - TankRepresentation: refactored to a thin subclass (~30 lines). - RTProgressBarRepresentation: new thin subclass for the scale-mode bar. - ProgressBarRepresentation: restored to the unmodified upstream JFX bar (closed for modification). - BaseWidgetRepresentations: one-line conditional dispatch on preference. Performance fix in RTScaledWidgetRepresentation.valueChanged(): scale.setValueRange() (tick layout recomputation) is now skipped when a pure PV value arrives and limits_from_pv=false, saving needless work at up to 20 Hz per widget. ## 3. Property panel filtering (editor) When progressbar_scale_mode=false, scale-only properties (scale_visible, format, precision, alarm limits, etc.) are hidden in the property editor via a set-membership check in PropertyPanelSection.fill(). Uses ProgressBarWidget.SCALE_MODE_PROPS — a static constant in the model class — so the editor (which already depends on representation) can read the preference and apply the filter with no new module dependencies. Tested on CLS OPI workstation with 200 RTTank widgets (Tank + ProgressBar mix). With parallel_rendering=true the visible refresh lag drops from ~5 s to <200 ms. --- .../properties/PropertyPanelSection.java | 10 + .../model/widgets/ProgressBarWidget.java | 18 + .../widgets/BaseWidgetRepresentations.java | 5 +- .../widgets/ProgressBarRepresentation.java | 301 +++++++-------- .../widgets/RTProgressBarRepresentation.java | 100 +++++ .../widgets/RTScaledWidgetRepresentation.java | 343 ++++++++++++++++++ .../javafx/widgets/TankRepresentation.java | 238 ++---------- .../builder/representation/Preferences.java | 7 + ...play_representation_preferences.properties | 7 + .../org/csstudio/javafx/rtplot/Activator.java | 7 + .../org/csstudio/javafx/rtplot/RTTank.java | 8 +- .../resources/rt_plot_preferences.properties | 10 + 12 files changed, 665 insertions(+), 389 deletions(-) create mode 100644 app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java create mode 100644 app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java index cd971b0583..639d7e5ac7 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/properties/PropertyPanelSection.java @@ -59,6 +59,8 @@ import org.csstudio.display.builder.model.properties.RulesWidgetProperty; import org.csstudio.display.builder.model.properties.ScriptsWidgetProperty; import org.csstudio.display.builder.model.properties.WidgetClassProperty; +import org.csstudio.display.builder.model.widgets.ProgressBarWidget; +import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.representation.javafx.FilenameSupport; import org.phoebus.ui.color.NamedWidgetColor; import org.phoebus.ui.color.WidgetColor; @@ -163,6 +165,14 @@ void fill(final UndoableActionManager undo, if (property instanceof WidgetClassProperty && class_mode) continue; + // When using the legacy JFX ProgressBar rendering, hide properties that + // only affect the RTTank-based scale rendering — they have no effect and + // would only confuse operators. + if (!Preferences.progressbar_scale_mode + && property.getWidget() instanceof ProgressBarWidget + && ProgressBarWidget.SCALE_MODE_PROPS.contains(property.getName())) + continue; + // Start of new category that needs to be shown? if (property.getCategory() != category) { category = property.getCategory(); diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java index caba522ef5..7fc736bd2e 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Set; import org.csstudio.display.builder.model.Messages; import org.csstudio.display.builder.model.Version; @@ -57,6 +58,23 @@ @SuppressWarnings("nls") public class ProgressBarWidget extends ScaledPVWidget { + /** Property names that only take effect when the RTTank-based rendering engine is + * active ({@code progressbar_scale_mode=true}). The property editor uses this set + * to hide irrelevant entries when the legacy JFX ProgressBar rendering is selected, + * keeping the panel uncluttered for operators who do not need scale features. + * + *

    Properties used by both renderers — {@code minimum}, {@code maximum}, + * {@code limits_from_pv}, {@code horizontal}, {@code log_scale}, + * {@code fill_color}, {@code background_color} — are intentionally absent. */ + public static final Set SCALE_MODE_PROPS = Set.of( + "format", "precision", + "scale_visible", "show_minor_ticks", "opposite_scale_visible", + "perpendicular_tick_labels", "border_width", "font", + "border_alarm_sensitive", + "alarm_limits_from_pv", "show_alarm_limits", + "level_lolo", "level_low", "level_high", "level_hihi", + "minor_alarm_color", "major_alarm_color"); + /** Widget descriptor */ public static final WidgetDescriptor WIDGET_DESCRIPTOR = new WidgetDescriptor("progressbar", WidgetCategory.MONITOR, diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java index b6db502bd7..067f57e0f4 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java @@ -52,6 +52,7 @@ import org.csstudio.display.builder.model.widgets.Viewer3dWidget; import org.csstudio.display.builder.model.widgets.WebBrowserWidget; import org.csstudio.display.builder.model.widgets.plots.DataBrowserWidget; +import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.model.widgets.plots.ImageWidget; import org.csstudio.display.builder.model.widgets.plots.StripchartWidget; import org.csstudio.display.builder.model.widgets.plots.XYPlotWidget; @@ -105,7 +106,9 @@ public Widget createWidget() entry(PictureWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PictureRepresentation()), entry(PolygonWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PolygonRepresentation()), entry(PolylineWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new PolylineRepresentation()), - entry(ProgressBarWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ProgressBarRepresentation()), + entry(ProgressBarWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) (Preferences.progressbar_scale_mode + ? new RTProgressBarRepresentation() + : new ProgressBarRepresentation())), entry(RadioWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new RadioRepresentation()), entry(RectangleWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new RectangleRepresentation()), entry(ScaledSliderWidget.WIDGET_DESCRIPTOR, () -> (WidgetRepresentation) new ScaledSliderRepresentation()), diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java index ef2e583a25..12612cdf5f 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ProgressBarRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2026 Oak Ridge National Laboratory. + * Copyright (c) 2015-2022 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,139 +7,83 @@ *******************************************************************************/ package org.csstudio.display.builder.representation.javafx.widgets; -import java.util.concurrent.TimeUnit; - import org.csstudio.display.builder.model.DirtyFlag; import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; import org.csstudio.display.builder.model.WidgetProperty; import org.csstudio.display.builder.model.WidgetPropertyListener; import org.csstudio.display.builder.model.util.VTypeUtil; import org.csstudio.display.builder.model.widgets.ProgressBarWidget; -import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.representation.javafx.JFXUtil; -import org.csstudio.javafx.rtplot.RTTank; -import org.epics.util.stats.Range; +import org.csstudio.javafx.rtplot.internal.util.Log10; import org.epics.vtype.Display; import org.epics.vtype.VType; +import org.phoebus.ui.javafx.Styles; -import javafx.scene.layout.Pane; +import javafx.scene.control.ProgressBar; import javafx.scene.transform.Rotate; import javafx.scene.transform.Translate; -/** Creates JavaFX item for model widget. - * - *

    Uses {@link RTTank} as the rendering engine so the bar - * gains a numeric scale, configurable tick format and precision, - * optional second scale, and alarm limit lines — all at no extra - * maintenance cost versus the plain JavaFX {@code ProgressBar}. - * +/** Creates JavaFX item for model widget * @author Kay Kasemir * @author Amanda Carpenter - * @author Heredie Delvalle — CLS, RTTank-based refactoring, scale support */ @SuppressWarnings("nls") -public class ProgressBarRepresentation extends RegionBaseRepresentation +public class ProgressBarRepresentation extends RegionBaseRepresentation { private final DirtyFlag dirty_look = new DirtyFlag(); - private final UntypedWidgetPropertyListener lookListener = this::lookChanged; - private final UntypedWidgetPropertyListener valueListener = this::valueChanged; - private final UntypedWidgetPropertyListener limitsListener = this::limitsChanged; + private final DirtyFlag dirty_value = new DirtyFlag(); + private final UntypedWidgetPropertyListener lookChangedListener = this::lookChanged; private final WidgetPropertyListener orientationChangedListener = this::orientationChanged; - - private volatile RTTank tank; + private final UntypedWidgetPropertyListener valueChangedListener = this::valueChanged; + private volatile double percentage = 0.0; @Override - public Pane createJFXNode() throws Exception + public ProgressBar createJFXNode() throws Exception { - tank = new RTTank(); - tank.setUpdateThrottle(Preferences.image_update_delay, TimeUnit.MILLISECONDS); - return new Pane(tank); + final ProgressBar bar = new ProgressBar(); + return bar; } @Override protected void registerListeners() { super.registerListeners(); - model_widget.propWidth().addUntypedPropertyListener(lookListener); - model_widget.propHeight().addUntypedPropertyListener(lookListener); - model_widget.propFont().addUntypedPropertyListener(lookListener); - model_widget.propFillColor().addUntypedPropertyListener(lookListener); - model_widget.propBackgroundColor().addUntypedPropertyListener(lookListener); - model_widget.propScaleVisible().addUntypedPropertyListener(lookListener); - model_widget.propShowMinorTicks().addUntypedPropertyListener(lookListener); - model_widget.propOppositeScaleVisible().addUntypedPropertyListener(lookListener); - model_widget.propPerpendicularTickLabels().addUntypedPropertyListener(lookListener); - model_widget.propBorderWidth().addUntypedPropertyListener(lookListener); - model_widget.propLogScale().addUntypedPropertyListener(lookListener); - model_widget.propFormat().addUntypedPropertyListener(lookListener); - model_widget.propPrecision().addUntypedPropertyListener(lookListener); - model_widget.propMinorAlarmColor().addUntypedPropertyListener(lookListener); - model_widget.propMajorAlarmColor().addUntypedPropertyListener(lookListener); - - model_widget.propLimitsFromPV().addUntypedPropertyListener(valueListener); - model_widget.propMinimum().addUntypedPropertyListener(valueListener); - model_widget.propMaximum().addUntypedPropertyListener(valueListener); - model_widget.runtimePropValue().addUntypedPropertyListener(valueListener); - - model_widget.propShowAlarmLimits().addUntypedPropertyListener(limitsListener); - model_widget.propAlarmLimitsFromPV().addUntypedPropertyListener(limitsListener); - model_widget.propLevelLoLo().addUntypedPropertyListener(limitsListener); - model_widget.propLevelLow().addUntypedPropertyListener(limitsListener); - model_widget.propLevelHigh().addUntypedPropertyListener(limitsListener); - model_widget.propLevelHiHi().addUntypedPropertyListener(limitsListener); - + model_widget.propFillColor().addUntypedPropertyListener(lookChangedListener); + model_widget.propBackgroundColor().addUntypedPropertyListener(lookChangedListener); + model_widget.propWidth().addUntypedPropertyListener(lookChangedListener); + model_widget.propHeight().addUntypedPropertyListener(lookChangedListener); + model_widget.propLimitsFromPV().addUntypedPropertyListener(valueChangedListener); + model_widget.propMinimum().addUntypedPropertyListener(valueChangedListener); + model_widget.propMaximum().addUntypedPropertyListener(valueChangedListener); + model_widget.propLogScale().addUntypedPropertyListener(valueChangedListener); + model_widget.runtimePropValue().addUntypedPropertyListener(valueChangedListener); model_widget.propHorizontal().addPropertyListener(orientationChangedListener); - - // Initial apply — range and fill first, then limits valueChanged(null, null, null); - limitsChanged(null, null, null); } @Override protected void unregisterListeners() { - model_widget.propWidth().removePropertyListener(lookListener); - model_widget.propHeight().removePropertyListener(lookListener); - model_widget.propFont().removePropertyListener(lookListener); - model_widget.propFillColor().removePropertyListener(lookListener); - model_widget.propBackgroundColor().removePropertyListener(lookListener); - model_widget.propScaleVisible().removePropertyListener(lookListener); - model_widget.propShowMinorTicks().removePropertyListener(lookListener); - model_widget.propOppositeScaleVisible().removePropertyListener(lookListener); - model_widget.propPerpendicularTickLabels().removePropertyListener(lookListener); - model_widget.propBorderWidth().removePropertyListener(lookListener); - model_widget.propLogScale().removePropertyListener(lookListener); - model_widget.propFormat().removePropertyListener(lookListener); - model_widget.propPrecision().removePropertyListener(lookListener); - model_widget.propMinorAlarmColor().removePropertyListener(lookListener); - model_widget.propMajorAlarmColor().removePropertyListener(lookListener); - - model_widget.propLimitsFromPV().removePropertyListener(valueListener); - model_widget.propMinimum().removePropertyListener(valueListener); - model_widget.propMaximum().removePropertyListener(valueListener); - model_widget.runtimePropValue().removePropertyListener(valueListener); - - model_widget.propShowAlarmLimits().removePropertyListener(limitsListener); - model_widget.propAlarmLimitsFromPV().removePropertyListener(limitsListener); - model_widget.propLevelLoLo().removePropertyListener(limitsListener); - model_widget.propLevelLow().removePropertyListener(limitsListener); - model_widget.propLevelHigh().removePropertyListener(limitsListener); - model_widget.propLevelHiHi().removePropertyListener(limitsListener); - + model_widget.propFillColor().removePropertyListener(lookChangedListener); + model_widget.propBackgroundColor().removePropertyListener(lookChangedListener); + model_widget.propWidth().removePropertyListener(lookChangedListener); + model_widget.propHeight().removePropertyListener(lookChangedListener); + model_widget.propLimitsFromPV().removePropertyListener(valueChangedListener); + model_widget.propMinimum().removePropertyListener(valueChangedListener); + model_widget.propMaximum().removePropertyListener(valueChangedListener); + model_widget.propLogScale().removePropertyListener(valueChangedListener); + model_widget.runtimePropValue().removePropertyListener(valueChangedListener); model_widget.propHorizontal().removePropertyListener(orientationChangedListener); super.unregisterListeners(); } - private void lookChanged(final WidgetProperty property, final Object old_value, final Object new_value) - { - dirty_look.mark(); - toolkit.scheduleUpdate(this); - } - private void orientationChanged(final WidgetProperty prop, final Boolean old, final Boolean horizontal) { - // When the user changes orientation in the editor, swap width ↔ height - // so the widget visually rotates rather than stretching. + // When interactively changing orientation, swap width <-> height. + // This will only affect interactive changes once the widget is represented on the screen. + // Initially, when the widget is loaded from XML, the representation + // doesn't exist and the original width, height and orientation are applied + // without triggering a swap. if (toolkit.isEditMode()) { final int w = model_widget.propWidth().getValue(); @@ -150,77 +94,61 @@ private void orientationChanged(final WidgetProperty prop, final Boolea lookChanged(prop, old, horizontal); } - /** Update the display range and fill level. Called on every PV value change. */ + private void lookChanged(final WidgetProperty property, final Object old_value, final Object new_value) + { + dirty_look.mark(); + toolkit.scheduleUpdate(this); + } + private void valueChanged(final WidgetProperty property, final Object old_value, final Object new_value) { final VType vtype = model_widget.runtimePropValue().getValue(); + final boolean limits_from_pv = model_widget.propLimitsFromPV().getValue(); double min_val = model_widget.propMinimum().getValue(); double max_val = model_widget.propMaximum().getValue(); - if (model_widget.propLimitsFromPV().getValue()) + if (limits_from_pv) { + // Try display range from PV final Display display_info = Display.displayOf(vtype); - if (display_info != null && display_info.getDisplayRange().isFinite()) + if (display_info != null) { min_val = display_info.getDisplayRange().getMinimum(); max_val = display_info.getDisplayRange().getMaximum(); } } - tank.setRange(min_val, max_val); - - // Re-read alarm limits from PV metadata on every value update. - if (model_widget.propAlarmLimitsFromPV().getValue()) - applyAlarmLimits(vtype); - - final double value = toolkit.isEditMode() - ? (min_val + max_val) / 2 - : VTypeUtil.getValueNumber(vtype).doubleValue(); - tank.setValue(value); - } - - /** Re-apply alarm limit lines when a limit property changes. */ - private void limitsChanged(final WidgetProperty property, final Object old_value, final Object new_value) - { - applyAlarmLimits(model_widget.runtimePropValue().getValue()); - } - - /** Push the current alarm limits to the tank from PV metadata or widget properties. */ - private void applyAlarmLimits(final VType vtype) - { - if (!model_widget.propShowAlarmLimits().getValue()) + // Fall back to 0..100 range + if (min_val >= max_val) { - tank.setLimits(Double.NaN, Double.NaN, Double.NaN, Double.NaN); - return; + min_val = 0.0; + max_val = 100.0; } - final double lolo, lo, hi, hihi; - if (model_widget.propAlarmLimitsFromPV().getValue()) + + // Determine percentage of value within the min..max range + final double value = VTypeUtil.getValueNumber(vtype).doubleValue(); + final double percentage; + + if (model_widget.propLogScale().getValue()) { - final Display display_info = Display.displayOf(vtype); - if (display_info != null) - { - final Range minor = display_info.getWarningRange(); - final Range major = display_info.getAlarmRange(); - lo = minor.getMinimum(); - hi = minor.getMaximum(); - lolo = major.getMinimum(); - hihi = major.getMaximum(); - } + final double d = Log10.log10(max_val) - Log10.log10(min_val); + if (d == 0) + percentage = Double.NaN; else - lolo = lo = hi = hihi = Double.NaN; + percentage = (Log10.log10(value) - Log10.log10(min_val)) / d; } else - { - lolo = model_widget.propLevelLoLo().getValue(); - lo = model_widget.propLevelLow().getValue(); - hi = model_widget.propLevelHigh().getValue(); - hihi = model_widget.propLevelHiHi().getValue(); - } - tank.setLimits(lolo, lo, hi, hihi); - tank.setLimitsFromPV(model_widget.propAlarmLimitsFromPV().getValue()); - } + percentage = (value - min_val) / (max_val - min_val); - /** Track whether orientation transforms are currently applied. */ - private boolean was_transformed = false; + // Limit to 0.0 .. 1.0 + if (percentage < 0.0 || !Double.isFinite(percentage)) + this.percentage = 0.0; + else if (percentage > 1.0) + this.percentage = 1.0; + else + this.percentage = percentage; + dirty_value.mark(); + toolkit.scheduleUpdate(this); + } @Override public void updateChanges() @@ -228,47 +156,66 @@ public void updateChanges() super.updateChanges(); if (dirty_look.checkAndClear()) { - double width = model_widget.propWidth().getValue(); + boolean horizontal = model_widget.propHorizontal().getValue(); + double width = model_widget.propWidth().getValue(); double height = model_widget.propHeight().getValue(); - - // A horizontal bar is rendered by RTTank as if vertical (RTTank is - // always vertical internally) and then rotated 90° clockwise. - if (model_widget.propHorizontal().getValue()) + if (!horizontal) { - tank.getTransforms().setAll(new Translate(width, 0), - new Rotate(90, 0, 0)); - was_transformed = true; - tank.setWidth(height); - tank.setHeight(width); + jfx_node.getTransforms().setAll( + new Translate(0, height), + new Rotate(-90, 0, 0)); + jfx_node.setPrefSize(height, width); } else { - if (was_transformed) - tank.getTransforms().clear(); - was_transformed = false; - tank.setWidth(width); - tank.setHeight(height); + jfx_node.getTransforms().clear(); + jfx_node.setPrefSize(width, height); } - jfx_node.setPrefSize(width, height); - tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); - // Background is the outer canvas margin; emptyColor is the unfilled bar portion. - // Map both to background_color so the whole widget has a uniform background. - final javafx.scene.paint.Color bg = JFXUtil.convert(model_widget.propBackgroundColor().getValue()); - tank.setBackground(bg); - tank.setEmptyColor(bg); - tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); - tank.setScaleVisible(model_widget.propScaleVisible().getValue()); - tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); - tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); - tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); - tank.setBorderWidth(model_widget.propBorderWidth().getValue()); - tank.setLogScale(model_widget.propLogScale().getValue()); - tank.setLabelFormat(model_widget.propFormat().getValue(), - model_widget.propPrecision().getValue()); - tank.setAlarmColors( - JFXUtil.convert(model_widget.propMinorAlarmColor().getValue()), - JFXUtil.convert(model_widget.propMajorAlarmColor().getValue())); + // Default 'inset' of .bar uses 7 pixels. + // A widget sized 15 has 8 pixels left for the bar. + // Select leaner style where .bar uses full size. + Styles.update(jfx_node, "SmallBar", + Math.min(width, height) <= 15); + + // Could clear style and use setBackground(), + // but result is very plain. + // Tweaking the color used by CSS keeps overall style. + // See also http://stackoverflow.com/questions/13467259/javafx-how-to-change-progressbar-color-dynamically + final StringBuilder style = new StringBuilder(); + + // Color of the progress bar / foreground + style.append("-fx-accent: ").append(JFXUtil.webRGB( + JFXUtil.convert( + model_widget.propFillColor().getValue() + ) + )).append(" !important; "); + + // Color of the background underneath the progress bar + // Note per moderna.css the background is actually three layers of color + // with fx-shadow-highlight-color on the bottom, + // then fx-text-box-border, + // and finally fx-control-inner-background on top, all stacked in place with offsets. + // This gives the illusion of having a bordered box with a shadow instead of actually being a + // bordered box with a shadow... + // Fortunately, the bottom-most color (the 'shadow') is already transparent so we can leave it alone + // Unfortunately, the middle color (the "border" color) is a solid gray color (#ececec), so we must + // override it with its rgba equivalent so that it has transparency matching the picked background color. + style.append("-fx-control-inner-background: ") + .append(JFXUtil.webRGB( + JFXUtil.convert( + model_widget.propBackgroundColor().getValue())) + ) + .append(";"); + style.append("-fx-text-box-border: rgba(236, 236, 236, ") + .append(JFXUtil.webAlpha(model_widget.propBackgroundColor().getValue())) + .append(");"); + style.append("-fx-shadow-highlight-color: rgba(236, 236, 236, ") + .append(JFXUtil.webAlpha(model_widget.propBackgroundColor().getValue())) + .append(");"); + jfx_node.setStyle(style.toString()); } + if (dirty_value.checkAndClear()) + jfx_node.setProgress(percentage); } } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java new file mode 100644 index 0000000000..b1ba86b5e9 --- /dev/null +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2015-2026 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.csstudio.display.builder.representation.javafx.widgets; + +import org.csstudio.display.builder.model.widgets.ProgressBarWidget; +import org.csstudio.display.builder.representation.javafx.JFXUtil; + +import javafx.scene.paint.Color; + +/** Creates JavaFX item for the Progress Bar widget using {@link org.csstudio.javafx.rtplot.RTTank} + * as the rendering engine. + * + *

    This representation adds a numeric scale, tick format/precision, + * an optional second scale, and alarm-limit lines to the progress bar. + * It is selected when the {@code progressbar_scale_mode} preference is + * {@code true}; the stock representation ({@link ProgressBarRepresentation}) + * is used otherwise, preserving the default JFX look. + * + *

    Shared RTTank lifecycle (value / range updates, alarm limits, + * orientation handling) lives in {@link RTScaledWidgetRepresentation}. + * This class contributes only the ProgressBar-specific appearance mapping: + * fill colour and a uniform background for the unfilled bar track. + * + * @author Kay Kasemir + * @author Amanda Carpenter + * @author Heredie Delvalle — CLS, RTTank-based refactoring, scale support; + * refactored onto RTScaledWidgetRepresentation + */ +@SuppressWarnings("nls") +public class RTProgressBarRepresentation extends RTScaledWidgetRepresentation +{ + @Override + protected boolean isHorizontal() + { + return model_widget.propHorizontal().getValue(); + } + + @Override + protected void registerLookListeners() + { + model_widget.propWidth().addUntypedPropertyListener(lookListener); + model_widget.propHeight().addUntypedPropertyListener(lookListener); + model_widget.propFont().addUntypedPropertyListener(lookListener); + model_widget.propFillColor().addUntypedPropertyListener(lookListener); + model_widget.propBackgroundColor().addUntypedPropertyListener(lookListener); + model_widget.propScaleVisible().addUntypedPropertyListener(lookListener); + model_widget.propShowMinorTicks().addUntypedPropertyListener(lookListener); + model_widget.propOppositeScaleVisible().addUntypedPropertyListener(lookListener); + model_widget.propPerpendicularTickLabels().addUntypedPropertyListener(lookListener); + model_widget.propBorderWidth().addUntypedPropertyListener(lookListener); + model_widget.propLogScale().addUntypedPropertyListener(lookListener); + model_widget.propFormat().addUntypedPropertyListener(lookListener); + model_widget.propPrecision().addUntypedPropertyListener(lookListener); + model_widget.propHorizontal().addPropertyListener(orientationChangedListener); + } + + @Override + protected void unregisterLookListeners() + { + model_widget.propWidth().removePropertyListener(lookListener); + model_widget.propHeight().removePropertyListener(lookListener); + model_widget.propFont().removePropertyListener(lookListener); + model_widget.propFillColor().removePropertyListener(lookListener); + model_widget.propBackgroundColor().removePropertyListener(lookListener); + model_widget.propScaleVisible().removePropertyListener(lookListener); + model_widget.propShowMinorTicks().removePropertyListener(lookListener); + model_widget.propOppositeScaleVisible().removePropertyListener(lookListener); + model_widget.propPerpendicularTickLabels().removePropertyListener(lookListener); + model_widget.propBorderWidth().removePropertyListener(lookListener); + model_widget.propLogScale().removePropertyListener(lookListener); + model_widget.propFormat().removePropertyListener(lookListener); + model_widget.propPrecision().removePropertyListener(lookListener); + model_widget.propHorizontal().removePropertyListener(orientationChangedListener); + } + + @Override + protected void applyLookToTank(final double width, final double height) + { + final Color bg = JFXUtil.convert(model_widget.propBackgroundColor().getValue()); + tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); + tank.setBackground(bg); + // Map background to empty colour so the unfilled portion blends with + // the outer margin, giving a uniform "bar-track" appearance. + tank.setEmptyColor(bg); + tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); + tank.setScaleVisible(model_widget.propScaleVisible().getValue()); + tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); + tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); + tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); + tank.setBorderWidth(model_widget.propBorderWidth().getValue()); + tank.setLogScale(model_widget.propLogScale().getValue()); + tank.setLabelFormat(model_widget.propFormat().getValue(), + model_widget.propPrecision().getValue()); + } +} diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java new file mode 100644 index 0000000000..a86007a6e9 --- /dev/null +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java @@ -0,0 +1,343 @@ +/******************************************************************************* + * Copyright (c) 2015-2026 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.csstudio.display.builder.representation.javafx.widgets; + +import java.util.concurrent.TimeUnit; + +import org.csstudio.display.builder.model.DirtyFlag; +import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; +import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.WidgetPropertyListener; +import org.csstudio.display.builder.model.util.VTypeUtil; +import org.csstudio.display.builder.model.widgets.ScaledPVWidget; +import org.csstudio.display.builder.representation.Preferences; +import org.csstudio.display.builder.representation.javafx.JFXUtil; +import org.csstudio.javafx.rtplot.RTTank; +import org.epics.util.stats.Range; +import org.epics.vtype.Display; +import org.epics.vtype.VType; + +import javafx.scene.layout.Pane; +import javafx.scene.transform.Rotate; +import javafx.scene.transform.Translate; + +/** Abstract base for widget representations whose JFX node is an {@link RTTank}. + * + *

    Handles all logic that depends only on the {@link ScaledPVWidget} contract: + *

    + * + *

    Subclasses provide: + *

    + * + *

    Neither this class nor its subclasses have any knowledge of each other's + * widget type: {@code TankRepresentation} and {@code ProgressBarRepresentation} + * are fully independent. + * + * @param concrete {@link ScaledPVWidget} subtype + * @author Heredie Delvalle — CLS, extracted from TankRepresentation / + * ProgressBarRepresentation to eliminate duplication + */ +@SuppressWarnings("nls") +public abstract class RTScaledWidgetRepresentation + extends RegionBaseRepresentation +{ + // ── shared state ────────────────────────────────────────────────────────── + + /** The rendering canvas shared by all RTTank-based widgets. */ + protected volatile RTTank tank; + + /** Dirty flag for appearance (color, scale, size). Value updates do not + * set this — they bypass the JFX representation update cycle entirely by + * calling {@link RTTank#setValue} directly. */ + protected final DirtyFlag dirty_look = new DirtyFlag(); + + // ── listeners ───────────────────────────────────────────────────────────── + + /** Marks appearance dirty and schedules an update. Shared by subclass + * listeners on color / scale / font properties. */ + protected final UntypedWidgetPropertyListener lookListener = + (p, o, n) -> { dirty_look.mark(); toolkit.scheduleUpdate(this); }; + + /** Forwards PV value or display-range changes to the tank immediately. */ + private final UntypedWidgetPropertyListener valueListener = this::valueChanged; + + /** Re-evaluates and pushes alarm limit lines whenever a limit property changes. */ + private final UntypedWidgetPropertyListener limitsListener = this::limitsChanged; + + /** Swaps width ↔ height in the editor and triggers a look update. */ + protected final WidgetPropertyListener orientationChangedListener = + this::orientationChanged; + + /** Whether orientation transforms are currently applied to the tank node. */ + private boolean was_transformed = false; + + // ── JFX node creation ───────────────────────────────────────────────────── + + @Override + public Pane createJFXNode() throws Exception + { + tank = new RTTank(); + tank.setUpdateThrottle(Preferences.image_update_delay, TimeUnit.MILLISECONDS); + configureTank(); + return new Pane(tank); + } + + /** Called once after {@link #tank} is created. + * Override to apply one-time tank settings (e.g. a rendering style). */ + protected void configureTank() + { + // no-op by default + } + + // ── listener lifecycle ──────────────────────────────────────────────────── + + /** Register listeners on the {@link ScaledPVWidget} value and limit + * properties, then call {@link #registerLookListeners()} for the + * subclass to add its widget-specific appearance listeners. + *

    Initial value and limit state is applied at the end so the tank + * shows the correct fill level as soon as the PV connects. */ + @Override + protected void registerListeners() + { + super.registerListeners(); + + // Value / range + model_widget.propLimitsFromPV().addUntypedPropertyListener(valueListener); + model_widget.propMinimum().addUntypedPropertyListener(valueListener); + model_widget.propMaximum().addUntypedPropertyListener(valueListener); + model_widget.runtimePropValue().addUntypedPropertyListener(valueListener); + + // Alarm limit lines + model_widget.propShowAlarmLimits().addUntypedPropertyListener(limitsListener); + model_widget.propAlarmLimitsFromPV().addUntypedPropertyListener(limitsListener); + model_widget.propLevelLoLo().addUntypedPropertyListener(limitsListener); + model_widget.propLevelLow().addUntypedPropertyListener(limitsListener); + model_widget.propLevelHigh().addUntypedPropertyListener(limitsListener); + model_widget.propLevelHiHi().addUntypedPropertyListener(limitsListener); + + // Alarm color changes only affect appearance, not limits + model_widget.propMinorAlarmColor().addUntypedPropertyListener(lookListener); + model_widget.propMajorAlarmColor().addUntypedPropertyListener(lookListener); + + // Widget-specific look properties (colours, scale, font, …) + registerLookListeners(); + + // Seed initial state — range first, then limits + valueChanged(null, null, null); + limitsChanged(null, null, null); + } + + /** Register listeners on widget-specific appearance properties. + * The implementation should add listeners using {@link #lookListener} + * (or a dedicated listener) and call nothing on the tank directly — + * that happens in {@link #applyLookToTank(double, double)}. */ + protected abstract void registerLookListeners(); + + @Override + protected void unregisterListeners() + { + model_widget.propLimitsFromPV().removePropertyListener(valueListener); + model_widget.propMinimum().removePropertyListener(valueListener); + model_widget.propMaximum().removePropertyListener(valueListener); + model_widget.runtimePropValue().removePropertyListener(valueListener); + + model_widget.propShowAlarmLimits().removePropertyListener(limitsListener); + model_widget.propAlarmLimitsFromPV().removePropertyListener(limitsListener); + model_widget.propLevelLoLo().removePropertyListener(limitsListener); + model_widget.propLevelLow().removePropertyListener(limitsListener); + model_widget.propLevelHigh().removePropertyListener(limitsListener); + model_widget.propLevelHiHi().removePropertyListener(limitsListener); + + model_widget.propMinorAlarmColor().removePropertyListener(lookListener); + model_widget.propMajorAlarmColor().removePropertyListener(lookListener); + + unregisterLookListeners(); + super.unregisterListeners(); + } + + /** Unregister the listeners added by {@link #registerLookListeners()}. */ + protected abstract void unregisterLookListeners(); + + // ── value / limits handling ─────────────────────────────────────────────── + + /** Called on every PV value update and on range-related property changes. + * Updates the tank's fill level. Also updates the display range when the + * range may have changed — i.e. when a range property fired, or when + * limits come from the PV (range is embedded in every VType). + * Alarm limits from PV metadata are also re-evaluated here. */ + private void valueChanged(final WidgetProperty prop, + final Object old_value, final Object new_value) + { + final VType vtype = model_widget.runtimePropValue().getValue(); + final boolean limits_from_pv = model_widget.propLimitsFromPV().getValue(); + + // Skip the scale-range update when a pure PV value arrived and the + // range is fixed by widget properties: scale.setValueRange() recomputes + // tick layout on every call, so skipping it at 20 Hz saves real work. + if (prop != model_widget.runtimePropValue() || limits_from_pv) + updateRange(vtype, limits_from_pv); + + // Alarm metadata is embedded in each VType — re-evaluate on every update. + if (model_widget.propAlarmLimitsFromPV().getValue()) + applyAlarmLimits(vtype); + + final double min = model_widget.propMinimum().getValue(); + final double max = model_widget.propMaximum().getValue(); + final double value = toolkit.isEditMode() + ? (min + max) / 2.0 + : VTypeUtil.getValueNumber(vtype).doubleValue(); + tank.setValue(value); + } + + /** Push the display range to the tank. + * When {@code limits_from_pv} is {@code true}, reads the range from PV + * display metadata and falls back to widget properties when metadata is + * unavailable. When {@code false}, uses the widget properties directly. + * + * @param vtype current PV value (may be {@code null} before connect) + * @param limits_from_pv whether the range should come from the PV */ + private void updateRange(final VType vtype, final boolean limits_from_pv) + { + double min = model_widget.propMinimum().getValue(); + double max = model_widget.propMaximum().getValue(); + if (limits_from_pv) + { + final Display display_info = Display.displayOf(vtype); + if (display_info != null && display_info.getDisplayRange().isFinite()) + { + min = display_info.getDisplayRange().getMinimum(); + max = display_info.getDisplayRange().getMaximum(); + } + } + tank.setRange(min, max); + } + + /** Triggered when any alarm limit property changes; delegates to + * {@link #applyAlarmLimits(VType)} with the current PV value. */ + private void limitsChanged(final WidgetProperty property, + final Object old_value, final Object new_value) + { + applyAlarmLimits(model_widget.runtimePropValue().getValue()); + } + + /** Resolves alarm limits from PV metadata or widget properties (depending on + * {@code alarm_limits_from_pv}) and pushes them to the tank. + * Clears all limit lines when {@code show_alarm_limits} is {@code false}. */ + private void applyAlarmLimits(final VType vtype) + { + if (!model_widget.propShowAlarmLimits().getValue()) + { + tank.setLimits(Double.NaN, Double.NaN, Double.NaN, Double.NaN); + return; + } + final double lolo, lo, hi, hihi; + if (model_widget.propAlarmLimitsFromPV().getValue()) + { + final Display display_info = Display.displayOf(vtype); + if (display_info != null) + { + final Range minor = display_info.getWarningRange(); + final Range major = display_info.getAlarmRange(); + lo = minor.getMinimum(); + hi = minor.getMaximum(); + lolo = major.getMinimum(); + hihi = major.getMaximum(); + } + else + lolo = lo = hi = hihi = Double.NaN; + } + else + { + lolo = model_widget.propLevelLoLo().getValue(); + lo = model_widget.propLevelLow().getValue(); + hi = model_widget.propLevelHigh().getValue(); + hihi = model_widget.propLevelHiHi().getValue(); + } + tank.setLimits(lolo, lo, hi, hihi); + tank.setLimitsFromPV(model_widget.propAlarmLimitsFromPV().getValue()); + } + + // ── orientation & appearance ────────────────────────────────────────────── + + /** @return whether this widget is currently in horizontal orientation */ + protected abstract boolean isHorizontal(); + + /** Swaps width ↔ height in the editor (so the widget visually rotates + * rather than stretching) and triggers a look update. */ + protected void orientationChanged(final WidgetProperty prop, + final Boolean old, final Boolean horizontal) + { + if (toolkit.isEditMode()) + { + final int w = model_widget.propWidth().getValue(); + final int h = model_widget.propHeight().getValue(); + model_widget.propWidth().setValue(h); + model_widget.propHeight().setValue(w); + } + dirty_look.mark(); + toolkit.scheduleUpdate(this); + } + + /** Push current widget-specific appearance properties to the tank. + * Called from {@link #updateChanges()} after size and orientation + * transforms have already been applied. + * + * @param width logical widget width in pixels (pre-rotation) + * @param height logical widget height in pixels (pre-rotation) */ + protected abstract void applyLookToTank(double width, double height); + + @Override + public void updateChanges() + { + super.updateChanges(); + if (dirty_look.checkAndClear()) + { + final double width = model_widget.propWidth().getValue(); + final double height = model_widget.propHeight().getValue(); + + // RTTank renders vertically; rotate 90° clockwise for horizontal bars. + if (isHorizontal()) + { + tank.getTransforms().setAll(new Translate(width, 0), + new Rotate(90, 0, 0)); + was_transformed = true; + tank.setWidth(height); + tank.setHeight(width); + } + else + { + if (was_transformed) + tank.getTransforms().clear(); + was_transformed = false; + tank.setWidth(width); + tank.setHeight(height); + } + jfx_node.setPrefSize(width, height); + + applyLookToTank(width, height); + tank.setAlarmColors( + JFXUtil.convert(model_widget.propMinorAlarmColor().getValue()), + JFXUtil.convert(model_widget.propMajorAlarmColor().getValue())); + } + } +} diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java index 8e3d373914..5937894431 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015-2023 Oak Ridge National Laboratory. + * Copyright (c) 2015-2026 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,52 +7,32 @@ *******************************************************************************/ package org.csstudio.display.builder.representation.javafx.widgets; -import java.util.concurrent.TimeUnit; - -import org.csstudio.display.builder.model.DirtyFlag; -import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; -import org.csstudio.display.builder.model.WidgetProperty; -import org.csstudio.display.builder.model.WidgetPropertyListener; -import org.csstudio.display.builder.model.util.VTypeUtil; import org.csstudio.display.builder.model.widgets.TankWidget; -import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.representation.javafx.JFXUtil; -import org.csstudio.javafx.rtplot.RTTank; -import org.epics.util.stats.Range; -import org.epics.vtype.Display; -import org.epics.vtype.VType; - -import javafx.scene.layout.Pane; -import javafx.scene.transform.Rotate; -import javafx.scene.transform.Translate; -/** Creates JavaFX item for model widget +/** Creates JavaFX item for the Tank widget. + * + *

    All shared RTTank wiring (value updates, alarm limits, + * orientation handling) lives in {@link RTScaledWidgetRepresentation}. + * This class contributes only the Tank-specific appearance properties: + * background, foreground, fill and empty colours. + * * @author Kay Kasemir * @author Heredie Delvalle — CLS, alarm limits, dual scale, - * format/precision wiring + * format/precision wiring; refactored onto RTScaledWidgetRepresentation */ -public class TankRepresentation extends RegionBaseRepresentation +@SuppressWarnings("nls") +public class TankRepresentation extends RTScaledWidgetRepresentation { - private final DirtyFlag dirty_look = new DirtyFlag(); - private final UntypedWidgetPropertyListener lookListener = this::lookChanged; - private final UntypedWidgetPropertyListener valueListener = this::valueChanged; - private final UntypedWidgetPropertyListener limitsListener = this::limitsChanged; - private final WidgetPropertyListener orientationChangedListener = this::orientationChanged; - - private volatile RTTank tank; - @Override - public Pane createJFXNode() throws Exception + protected boolean isHorizontal() { - tank = new RTTank(); - tank.setUpdateThrottle(Preferences.image_update_delay, TimeUnit.MILLISECONDS); - return new Pane(tank); + return model_widget.propHorizontal().getValue(); } @Override - protected void registerListeners() + protected void registerLookListeners() { - super.registerListeners(); model_widget.propWidth().addUntypedPropertyListener(lookListener); model_widget.propHeight().addUntypedPropertyListener(lookListener); model_widget.propFont().addUntypedPropertyListener(lookListener); @@ -65,33 +45,14 @@ protected void registerListeners() model_widget.propPerpendicularTickLabels().addUntypedPropertyListener(lookListener); model_widget.propFormat().addUntypedPropertyListener(lookListener); model_widget.propPrecision().addUntypedPropertyListener(lookListener); - model_widget.propMinorAlarmColor().addUntypedPropertyListener(lookListener); - model_widget.propMajorAlarmColor().addUntypedPropertyListener(lookListener); model_widget.propOppositeScaleVisible().addUntypedPropertyListener(lookListener); model_widget.propBorderWidth().addUntypedPropertyListener(lookListener); model_widget.propLogScale().addUntypedPropertyListener(lookListener); - - // Range and fill-level; need re-evaluation on every PV sample - model_widget.propLimitsFromPV().addUntypedPropertyListener(valueListener); - model_widget.propMinimum().addUntypedPropertyListener(valueListener); - model_widget.propMaximum().addUntypedPropertyListener(valueListener); - model_widget.runtimePropValue().addUntypedPropertyListener(valueListener); - // Alarm limits; only need re-evaluation when limit properties change. - // When alarm_limits_from_pv=true, valueChanged() calls applyAlarmLimits() too. - model_widget.propShowAlarmLimits().addUntypedPropertyListener(limitsListener); - model_widget.propAlarmLimitsFromPV().addUntypedPropertyListener(limitsListener); - model_widget.propLevelLoLo().addUntypedPropertyListener(limitsListener); - model_widget.propLevelLow().addUntypedPropertyListener(limitsListener); - model_widget.propLevelHigh().addUntypedPropertyListener(limitsListener); - model_widget.propLevelHiHi().addUntypedPropertyListener(limitsListener); model_widget.propHorizontal().addPropertyListener(orientationChangedListener); - // Initial apply — order matters: range first, then limits, then value - valueChanged(null, null, null); - limitsChanged(null, null, null); } @Override - protected void unregisterListeners() + protected void unregisterLookListeners() { model_widget.propWidth().removePropertyListener(lookListener); model_widget.propHeight().removePropertyListener(lookListener); @@ -105,168 +66,27 @@ protected void unregisterListeners() model_widget.propPerpendicularTickLabels().removePropertyListener(lookListener); model_widget.propFormat().removePropertyListener(lookListener); model_widget.propPrecision().removePropertyListener(lookListener); - model_widget.propMinorAlarmColor().removePropertyListener(lookListener); - model_widget.propMajorAlarmColor().removePropertyListener(lookListener); model_widget.propOppositeScaleVisible().removePropertyListener(lookListener); model_widget.propBorderWidth().removePropertyListener(lookListener); model_widget.propLogScale().removePropertyListener(lookListener); - - model_widget.propLimitsFromPV().removePropertyListener(valueListener); - model_widget.propMinimum().removePropertyListener(valueListener); - model_widget.propMaximum().removePropertyListener(valueListener); - model_widget.runtimePropValue().removePropertyListener(valueListener); - model_widget.propShowAlarmLimits().removePropertyListener(limitsListener); - model_widget.propAlarmLimitsFromPV().removePropertyListener(limitsListener); - model_widget.propLevelLoLo().removePropertyListener(limitsListener); - model_widget.propLevelLow().removePropertyListener(limitsListener); - model_widget.propLevelHigh().removePropertyListener(limitsListener); - model_widget.propLevelHiHi().removePropertyListener(limitsListener); model_widget.propHorizontal().removePropertyListener(orientationChangedListener); - super.unregisterListeners(); } - private void lookChanged(final WidgetProperty property, final Object old_value, final Object new_value) - { - dirty_look.mark(); - toolkit.scheduleUpdate(this); - } - - /** Update the display range and fill level. Called on every PV value change. - * Alarm limits from PV metadata are also refreshed here (the metadata is - * carried inside the VType on every update). Manually-configured limits - * are managed exclusively by {@link #limitsChanged}. - */ - private void valueChanged(final WidgetProperty property, final Object old_value, final Object new_value) - { - final VType vtype = model_widget.runtimePropValue().getValue(); - - double min_val = model_widget.propMinimum().getValue(); - double max_val = model_widget.propMaximum().getValue(); - if (model_widget.propLimitsFromPV().getValue()) - { - final Display display_info = Display.displayOf(vtype); - if (display_info != null && display_info.getDisplayRange().isFinite()) - { - min_val = display_info.getDisplayRange().getMinimum(); - max_val = display_info.getDisplayRange().getMaximum(); - } - } - tank.setRange(min_val, max_val); - - // Alarm metadata is embedded in the VType, so re-check it on every update. - // When using widget-configured limits, limitsChanged() handles updates instead. - if (model_widget.propAlarmLimitsFromPV().getValue()) - applyAlarmLimits(vtype); - - final double value = toolkit.isEditMode() - ? (min_val + max_val) / 2 - : VTypeUtil.getValueNumber(vtype).doubleValue(); - tank.setValue(value); - } - - /** Re-apply alarm limit lines. Called when any limit property changes. - * Also invoked from {@link #valueChanged} when limits come from the PV. - */ - private void limitsChanged(final WidgetProperty property, final Object old_value, final Object new_value) - { - applyAlarmLimits(model_widget.runtimePropValue().getValue()); - } - - /** Push the current alarm limits to the tank, reading from PV metadata or - * widget properties depending on {@code alarm_limits_from_pv}. - * Clears all limit lines when {@code show_alarm_limits} is {@code false}. - */ - private void applyAlarmLimits(final VType vtype) - { - if (!model_widget.propShowAlarmLimits().getValue()) - { - tank.setLimits(Double.NaN, Double.NaN, Double.NaN, Double.NaN); - return; - } - final double lolo, lo, hi, hihi; - if (model_widget.propAlarmLimitsFromPV().getValue()) - { - final Display display_info = Display.displayOf(vtype); - if (display_info != null) - { - final Range minor = display_info.getWarningRange(); - final Range major = display_info.getAlarmRange(); - lo = minor.getMinimum(); - hi = minor.getMaximum(); - lolo = major.getMinimum(); - hihi = major.getMaximum(); - } - else - { // PV connected but no metadata yet — show nothing - lolo = lo = hi = hihi = Double.NaN; - } - } - else - { - lolo = model_widget.propLevelLoLo().getValue(); - lo = model_widget.propLevelLow().getValue(); - hi = model_widget.propLevelHigh().getValue(); - hihi = model_widget.propLevelHiHi().getValue(); - } - tank.setLimits(lolo, lo, hi, hihi); - tank.setLimitsFromPV(model_widget.propAlarmLimitsFromPV().getValue()); - } - - private void orientationChanged(final WidgetProperty prop, final Boolean old, final Boolean horizontal) - { - if (toolkit.isEditMode()) - { // Swap width <-> height so widget basically rotates - final int w = model_widget.propWidth().getValue(); - final int h = model_widget.propHeight().getValue(); - model_widget.propWidth().setValue(h); - model_widget.propHeight().setValue(w); - } - lookChanged(prop, old, horizontal); - } - - /** Track if we ever set transformations because just 'clearing' would otherwise allocate them */ - private boolean was_transformed = false; - @Override - public void updateChanges() + protected void applyLookToTank(final double width, final double height) { - super.updateChanges(); - if (dirty_look.checkAndClear()) - { - double width = model_widget.propWidth().getValue(); - double height = model_widget.propHeight().getValue(); - if (model_widget.propHorizontal().getValue()) - { - tank.getTransforms().setAll(new Translate(width, 0), - new Rotate(90, 0, 0)); - was_transformed = true; - tank.setWidth(height); - tank.setHeight(width); - } - else - { - if (was_transformed) - tank.getTransforms().clear(); - tank.setWidth(width); - tank.setHeight(height); - } - jfx_node.setPrefSize(width, height); - tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); - tank.setBackground(JFXUtil.convert(model_widget.propBackground().getValue())); - tank.setForeground(JFXUtil.convert(model_widget.propForeground().getValue())); - tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); - tank.setEmptyColor(JFXUtil.convert(model_widget.propEmptyColor().getValue())); - tank.setScaleVisible(model_widget.propScaleVisible().getValue()); - tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); - tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); - tank.setLogScale(model_widget.propLogScale().getValue()); - tank.setLabelFormat(model_widget.propFormat().getValue(), - model_widget.propPrecision().getValue()); - tank.setAlarmColors( - JFXUtil.convert(model_widget.propMinorAlarmColor().getValue()), - JFXUtil.convert(model_widget.propMajorAlarmColor().getValue())); - tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); - tank.setBorderWidth(model_widget.propBorderWidth().getValue()); - } + tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); + tank.setBackground(JFXUtil.convert(model_widget.propBackground().getValue())); + tank.setForeground(JFXUtil.convert(model_widget.propForeground().getValue())); + tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); + tank.setEmptyColor(JFXUtil.convert(model_widget.propEmptyColor().getValue())); + tank.setScaleVisible(model_widget.propScaleVisible().getValue()); + tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); + tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); + tank.setLogScale(model_widget.propLogScale().getValue()); + tank.setLabelFormat(model_widget.propFormat().getValue(), + model_widget.propPrecision().getValue()); + tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); + tank.setBorderWidth(model_widget.propBorderWidth().getValue()); } } diff --git a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/Preferences.java b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/Preferences.java index d25853b120..dad8d6b34d 100644 --- a/app/display/representation/src/main/java/org/csstudio/display/builder/representation/Preferences.java +++ b/app/display/representation/src/main/java/org/csstudio/display/builder/representation/Preferences.java @@ -22,6 +22,13 @@ public class Preferences update_accumulation_time, update_delay, plot_update_delay, image_update_delay, tooltip_length, embedded_timeout; + /** When {@code true}, the Progress Bar widget uses {@link org.csstudio.javafx.rtplot.RTTank} + * as its rendering engine, which adds a numeric scale, tick format/precision, + * an optional second scale, and alarm-limit lines. + * When {@code false} (default), the stock JFX {@code ProgressBar} look is preserved. + * Requires restart to take effect. */ + @Preference public static boolean progressbar_scale_mode; + static { AnnotatedPreferences.initialize(Preferences.class, "/display_representation_preferences.properties"); diff --git a/app/display/representation/src/main/resources/display_representation_preferences.properties b/app/display/representation/src/main/resources/display_representation_preferences.properties index 32724cbba7..e3f27e73ba 100644 --- a/app/display/representation/src/main/resources/display_representation_preferences.properties +++ b/app/display/representation/src/main/resources/display_representation_preferences.properties @@ -44,3 +44,10 @@ tooltip_length=200 # Timeout for load / unload of Embedded Widget content, in milliseconds. embedded_timeout=5000 + +# When true, the Progress Bar widget uses the RTTank rendering engine, +# which adds a numeric scale, tick marks, format/precision control, +# an optional second scale, and alarm-limit lines. +# When false (default), the standard JFX ProgressBar look is preserved. +# Requires restart to take effect. +progressbar_scale_mode = false diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/Activator.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/Activator.java index 8444d75931..60a5ce5ef0 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/Activator.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/Activator.java @@ -30,6 +30,13 @@ public class Activator @Preference(name="shady_future") private static int[] rgba; public static final Color shady_future; + /** When true, RTTank renders on the shared thread pool (one thread per CPU + * core) so many simultaneous Tank / ProgressBar instances update in + * parallel. When false, all renders serialise on a single global thread + * (the pre-fix behaviour). Controlled by the {@code parallel_rendering} + * preference. */ + @Preference(name="parallel_rendering") public static boolean parallel_rendering; + /** Thread pool for scrolling, throttling updates * *

    One per CPU core allows that many plots to run updateImageBuffer in parallel. diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index 46552f5f89..8c294c7b05 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -167,7 +167,11 @@ public RTTank() widthProperty().addListener(resize_listener); heightProperty().addListener(resize_listener); - // 20Hz default throttle + // 20Hz default throttle. + // When parallel_rendering is enabled, each tank renders on the shared thread pool + // so that many tanks on a display update concurrently. The default (false) serialises + // all renders on the single global UpdateThrottle.TIMER thread — safe but slow for + // displays with many Tank / ProgressBar widgets. update_throttle = new UpdateThrottle(50, TimeUnit.MILLISECONDS, () -> { if (needUpdate.getAndSet(false)){ @@ -179,7 +183,7 @@ public RTTank() requestUpdate(); } } - }); + }, Activator.parallel_rendering ? Activator.thread_pool : UpdateThrottle.TIMER); // Configure right-side scale — must happen after update_throttle is // initialised because setOnRight() triggers requestUpdate() via the diff --git a/app/rtplot/src/main/resources/rt_plot_preferences.properties b/app/rtplot/src/main/resources/rt_plot_preferences.properties index 3e9e4587e1..4303a625db 100644 --- a/app/rtplot/src/main/resources/rt_plot_preferences.properties +++ b/app/rtplot/src/main/resources/rt_plot_preferences.properties @@ -20,3 +20,13 @@ # # shady_future=128, 128, 128, 0 shady_future=128, 128, 128, 128 + +# Use a thread pool for RTTank rendering so that many simultaneous RTTank +# instances (Tank widget, ProgressBar with scale) render in parallel rather +# than serialising on a single global thread. +# +# Set to false only on severely resource-constrained systems where you want +# to limit background CPU usage at the cost of slower widget refresh. +# +# :default: false +parallel_rendering=false From 171630f57b5d977dabfbefc691766ed0f4ff6c61 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 3 Apr 2026 18:36:07 -0600 Subject: [PATCH 08/11] fix(progressbar): remove universal props from SCALE_MODE_PROPS filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit border_alarm_sensitive and border_width are handled by RegionBaseRepresentation for all RegionBaseRepresentation subclasses, regardless of rendering mode. They were incorrectly included in SCALE_MODE_PROPS, which caused the property editor to hide them when progressbar_scale_mode=false — preventing users from setting alarm-sensitive borders or custom widget borders on the classic JFX progress bar. Also add 'requires restart' note to parallel_rendering preference comment for consistency with progressbar_scale_mode documentation. --- .../builder/model/widgets/ProgressBarWidget.java | 13 ++++++++----- .../main/resources/rt_plot_preferences.properties | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java index 7fc736bd2e..b1ca1fed0f 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java @@ -63,14 +63,17 @@ public class ProgressBarWidget extends ScaledPVWidget * to hide irrelevant entries when the legacy JFX ProgressBar rendering is selected, * keeping the panel uncluttered for operators who do not need scale features. * - *

    Properties used by both renderers — {@code minimum}, {@code maximum}, - * {@code limits_from_pv}, {@code horizontal}, {@code log_scale}, - * {@code fill_color}, {@code background_color} — are intentionally absent. */ + *

    Properties used by both renderers are intentionally absent, including + * {@code minimum}, {@code maximum}, {@code limits_from_pv}, {@code horizontal}, + * {@code log_scale}, {@code fill_color}, {@code background_color}, + * {@code border_width} (drawn as a CSS outer border by {@code RegionBaseRepresentation} + * in the legacy renderer; as an inner canvas border in RTTank mode), and + * {@code border_alarm_sensitive} (alarm-sensitive outer border, also handled by + * {@code RegionBaseRepresentation} for all widget types). */ public static final Set SCALE_MODE_PROPS = Set.of( "format", "precision", "scale_visible", "show_minor_ticks", "opposite_scale_visible", - "perpendicular_tick_labels", "border_width", "font", - "border_alarm_sensitive", + "perpendicular_tick_labels", "font", "alarm_limits_from_pv", "show_alarm_limits", "level_lolo", "level_low", "level_high", "level_hihi", "minor_alarm_color", "major_alarm_color"); diff --git a/app/rtplot/src/main/resources/rt_plot_preferences.properties b/app/rtplot/src/main/resources/rt_plot_preferences.properties index 4303a625db..21e9d9de42 100644 --- a/app/rtplot/src/main/resources/rt_plot_preferences.properties +++ b/app/rtplot/src/main/resources/rt_plot_preferences.properties @@ -28,5 +28,7 @@ shady_future=128, 128, 128, 128 # Set to false only on severely resource-constrained systems where you want # to limit background CPU usage at the cost of slower widget refresh. # +# Requires restart to take effect. +# # :default: false parallel_rendering=false From a243bfe2e4c19448a0307bea7755a5c8d54e6ff9 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 3 Apr 2026 20:27:11 -0600 Subject: [PATCH 09/11] feat(progressbar): match stock JFX ProgressBar appearance in scale mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two RTTank rendering modes used only by RTProgressBarRepresentation: flat_track: paints the unfilled track with a solid colour instead of the default left→center gradient. The stock JFX ProgressBar has a flat background; the gradient was appearing as a dark band in the middle of the track background, opposite to the intended visual. inner_padding: extra inset from all four canvas edges to the plot body. When scale_visible=false the canvas fills edge-to-edge; 3 px of padding recreates the inset margin of the default JFX ProgressBar CSS (~7 px total, ~3 px per side visible), so .bob files from older Phoebus versions render consistently after enabling progressbar_scale_mode. Set to 0 automatically when scale_visible=true (the scale label area provides the visual framing). Both features default to off so the Tank widget is completely unaffected. RTProgressBarRepresentation activates them in configureTank() / applyLookToTank(). --- .../widgets/BaseWidgetRepresentations.java | 2 +- .../widgets/RTProgressBarRepresentation.java | 20 ++++++- .../org/csstudio/javafx/rtplot/RTTank.java | 52 ++++++++++++++++--- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java index 067f57e0f4..8c9927f195 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/BaseWidgetRepresentations.java @@ -52,10 +52,10 @@ import org.csstudio.display.builder.model.widgets.Viewer3dWidget; import org.csstudio.display.builder.model.widgets.WebBrowserWidget; import org.csstudio.display.builder.model.widgets.plots.DataBrowserWidget; -import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.model.widgets.plots.ImageWidget; import org.csstudio.display.builder.model.widgets.plots.StripchartWidget; import org.csstudio.display.builder.model.widgets.plots.XYPlotWidget; +import org.csstudio.display.builder.representation.Preferences; import org.csstudio.display.builder.representation.WidgetRepresentation; import org.csstudio.display.builder.representation.WidgetRepresentationFactory; import org.csstudio.display.builder.representation.javafx.widgets.plots.DataBrowserRepresentation; diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java index b1ba86b5e9..9f52b72261 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java @@ -34,6 +34,18 @@ @SuppressWarnings("nls") public class RTProgressBarRepresentation extends RTScaledWidgetRepresentation { + /** Called once after {@link #tank} is created; switch on flat-track and + * progress-bar inner padding (set dynamically per {@code scale_visible} + * in {@link #applyLookToTank}). */ + @Override + protected void configureTank() + { + // The original JFX ProgressBar has a flat (solid) track background — + // no gradient. Enabling flat_track removes the darker-center gradient + // that RTTank normally uses for the Tank widget's empty region. + tank.setFlatTrack(true); + } + @Override protected boolean isHorizontal() { @@ -88,7 +100,8 @@ protected void applyLookToTank(final double width, final double height) // the outer margin, giving a uniform "bar-track" appearance. tank.setEmptyColor(bg); tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); - tank.setScaleVisible(model_widget.propScaleVisible().getValue()); + final boolean scale_visible = model_widget.propScaleVisible().getValue(); + tank.setScaleVisible(scale_visible); tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); @@ -96,5 +109,10 @@ protected void applyLookToTank(final double width, final double height) tank.setLogScale(model_widget.propLogScale().getValue()); tank.setLabelFormat(model_widget.propFormat().getValue(), model_widget.propPrecision().getValue()); + // When there is no scale the canvas fills the widget boundary edge-to-edge. + // Apply inner padding to match the inset margin of the stock JFX ProgressBar + // (~7 px total / ~3 px per side in the default CSS), so displays from older + // Phoebus versions look consistent after switching to scale mode. + tank.setInnerPadding(scale_visible ? 0 : 3); } } diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index 8c294c7b05..f59b192d5e 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -94,6 +94,17 @@ public class RTTank extends Canvas /** Border width in pixels around the tank body; 0 = no border (default) */ private volatile int border_width = 0; + /** Extra inset from canvas edge to plot body on all four sides. + * 0 = default tank look; positive values give a recessed bar-track + * appearance suitable for progress-bar use without a visible scale. */ + private volatile int inner_padding = 0; + + /** When {@code true}, the empty (unfilled) portion of the tank is painted + * with a solid colour instead of the default left-to-center gradient. + * Use for progress-bar style tracks where the JFX original has a flat + * background and only the filled bar carries a visual gradient. */ + private volatile boolean flat_track = false; + /** Current value, i.e. fill level */ private volatile double value = 5.0; @@ -219,6 +230,30 @@ public void setBorderWidth(final int width) requestUpdate(); } + /** Extra inset from all four canvas edges to the plot body. + * Set to 0 (default) for the standard tank look. Set to a positive + * value (e.g. 4) when using RTTank as a progress-bar track so that + * the filled area is visually inset from the widget boundary, + * matching the margin of the JFX {@code ProgressBar} CSS style. + * @param pixels padding in pixels; clamped to [0, 20] */ + public void setInnerPadding(final int pixels) + { + inner_padding = Math.max(0, Math.min(20, pixels)); + need_layout.set(true); + requestUpdate(); + } + + /** Control whether the unfilled track region is painted with a solid colour + * ({@code true}) or the default left-to-center gradient ({@code false}). + * Enable for progress-bar use to match the flat track background of the + * JFX {@code ProgressBar}; disable (default) to keep the Tank look. + * @param flat {@code true} = solid track, {@code false} = gradient track */ + public void setFlatTrack(final boolean flat) + { + flat_track = flat; + requestUpdate(); + } + /** @param color Background color */ public void setBackground(final javafx.scene.paint.Color color) { @@ -549,11 +584,14 @@ private void computeLayout(final Graphics2D gc, final Rectangle bounds) // Inset = ceil(border_width/2) keeps the outer stroke edge inside the canvas. // On sides with a scale the label area provides ample margin so inset=0. // When there is no border, inset=1 is the original clip guard. + // inner_padding is added on top of all four sides regardless of scale presence; + // it is 0 for the standard tank and > 0 for the progress-bar track mode. final int half_bw_ceil = (border_width + 1) / 2; - final int inset_left = (left_width == 0) ? Math.max(1, half_bw_ceil) : 0; - final int inset_right = (right_width == 0) ? Math.max(1, half_bw_ceil) : 0; - final int inset_top = (ends[1] == 0) ? Math.max(1, half_bw_ceil) : 0; - final int inset_bottom = (ends[0] == 0) ? Math.max(1, half_bw_ceil) : 0; + final int ip = inner_padding; + final int inset_left = (left_width == 0) ? Math.max(1, half_bw_ceil) + ip : ip; + final int inset_right = (right_width == 0) ? Math.max(1, half_bw_ceil) + ip : ip; + final int inset_top = (ends[1] == 0) ? Math.max(1, half_bw_ceil) + ip : ip; + final int inset_bottom = (ends[0] == 0) ? Math.max(1, half_bw_ceil) + ip : ip; final int top = bounds.y + ends[1] + inset_top; final int height = bounds.height - ends[0] - ends[1] - inset_top - inset_bottom; @@ -609,8 +647,10 @@ protected Image updateImageBuffer() final int level = computeFillLevel(plot_bounds.height, min, max, current, scale.isLogarithmic()); final int arc = Math.min(plot_bounds.width, plot_bounds.height) / 10; - gc.setPaint(new GradientPaint(plot_bounds.x, 0, empty, plot_bounds.x+plot_bounds.width/2, 0, empty_shadow, true)); - + if (flat_track) + gc.setColor(empty); + else + gc.setPaint(new GradientPaint(plot_bounds.x, 0, empty, plot_bounds.x+plot_bounds.width/2, 0, empty_shadow, true)); gc.fillRoundRect(plot_bounds.x, plot_bounds.y, plot_bounds.width, plot_bounds.height, arc, arc); gc.setPaint(new GradientPaint(plot_bounds.x, 0, fill, plot_bounds.x+plot_bounds.width/2, 0, fill_highlight, true)); From 25b7b3da8dc201a5e3159afa2be32eb0eced62ff Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Sat, 4 Apr 2026 09:34:35 -0600 Subject: [PATCH 10/11] feat(display/rtplot): show_scale_labels + inner_padding for Tank/ProgressBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new widget properties extending the RTTank rendering path: ## show_scale_labels (Tank + ProgressBar, default: true) When false, the axis scale draws tick marks but suppresses all label text. This enables tight stacked layouts where one labelled widget leads and the rest show aligned tick marks only, saving horizontal (or vertical) space without losing the alignment grid. Changes: YAxisImpl - volatile boolean show_labels (render thread / JFX thread) - getDesiredPixelSize: short-circuits to TICK_LENGTH when false - getPixelGaps: returns {0,0} — no label overhang at endpoints - paint: skips drawTickLabel + paintLabels when false RTTank - setScaleLabelsVisible() delegates to both YAxisImpl axes; axes fire requestLayout/requestRefresh via plot_part_listener, so RTTank needs no extra need_layout or requestUpdate calls ScaledPVWidget - propShowScaleLabels descriptor (shared base) TankWidget - show_scale_labels property (default true) TankRepresentation - listener + applyLookToTank call ## inner_padding (ProgressBar only, default: 3, range: 0..20) Configurable inset between the widget edge and the fill bar. Replaces the previous hardcoded 'scale_visible ? 0 : 3' logic. Set to 0 for an edge-to-edge bar; keep at 3 to match the stock JFX ProgressBar CSS margin. Changes: ProgressBarWidget - propInnerPadding descriptor + property + accessor - inner_padding added to SCALE_MODE_PROPS filter - show_scale_labels added to SCALE_MODE_PROPS filter RTProgressBarRepresentation - listener + applyLookToTank call ## Review fixes (same batch) - volatile on YAxisImpl.show_labels (thread-safety: render vs JFX thread) - removed redundant need_layout+requestUpdate in RTTank.setScaleLabelsVisible - corrected stale configureTank() javadoc in RTProgressBarRepresentation - updated ProgressBarWidget class javadoc to mention new properties - fixed double blank line in ProgressBarWidget imports - Messages.java + messages.properties: ShowScaleLabels, InnerPadding - docs/source/changelog.rst: ShowScaleLabels + InnerPadding entries added --- .../display/builder/model/Messages.java | 2 + .../model/widgets/ProgressBarWidget.java | 35 +++++++++++++++-- .../builder/model/widgets/ScaledPVWidget.java | 5 +++ .../builder/model/widgets/TankWidget.java | 8 ++++ .../display/builder/model/messages.properties | 2 + .../widgets/RTProgressBarRepresentation.java | 24 ++++++------ .../javafx/widgets/TankRepresentation.java | 3 ++ .../org/csstudio/javafx/rtplot/RTTank.java | 15 ++++++++ .../javafx/rtplot/internal/YAxisImpl.java | 38 +++++++++++++++++-- docs/source/changelog.rst | 18 +++++++++ 10 files changed, 132 insertions(+), 18 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java index 1f56ddea77..86df17727c 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java @@ -185,6 +185,7 @@ public class Messages WidgetProperties_BorderAlarmSensitive, WidgetProperties_BorderColor, WidgetProperties_BorderWidth, + WidgetProperties_InnerPadding, WidgetProperties_CellColors, WidgetProperties_Class, WidgetProperties_ColorHiHi, @@ -326,6 +327,7 @@ public class Messages WidgetProperties_ShowLoLo, WidgetProperties_ShowMinorTicks, WidgetProperties_PerpendicularTickLabels, + WidgetProperties_ShowScaleLabels, WidgetProperties_ShowOK, WidgetProperties_ShowScale, WidgetProperties_ShowUnits, diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java index b1ca1fed0f..09ec78e112 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ProgressBarWidget.java @@ -12,7 +12,7 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propHorizontal; import static org.csstudio.display.builder.model.widgets.plots.PlotWidgetProperties.propLogscale; - +import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newIntegerPropertyDescriptor; import java.util.Arrays; import java.util.List; @@ -25,6 +25,8 @@ import org.csstudio.display.builder.model.WidgetConfigurator; import org.csstudio.display.builder.model.WidgetDescriptor; import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.WidgetPropertyCategory; +import org.csstudio.display.builder.model.WidgetPropertyDescriptor; import org.csstudio.display.builder.model.persist.ModelReader; import org.csstudio.display.builder.model.persist.NamedWidgetFonts; import org.csstudio.display.builder.model.persist.WidgetFontService; @@ -48,7 +50,8 @@ * {@code background_color}, {@code horizontal}, {@code limits_from_pv}, * {@code minimum}, {@code maximum} and {@code log_scale} keep the same * XML names. New properties ({@code format}, {@code precision}, - * {@code scale_visible}, {@code show_minor_ticks}, alarm limit properties) + * {@code scale_visible}, {@code show_minor_ticks}, {@code show_scale_labels}, + * {@code inner_padding}, alarm limit properties) * are silently ignored by older Phoebus versions. * * @author Kay Kasemir @@ -72,12 +75,20 @@ public class ProgressBarWidget extends ScaledPVWidget * {@code RegionBaseRepresentation} for all widget types). */ public static final Set SCALE_MODE_PROPS = Set.of( "format", "precision", - "scale_visible", "show_minor_ticks", "opposite_scale_visible", - "perpendicular_tick_labels", "font", + "scale_visible", "show_minor_ticks", "show_scale_labels", + "opposite_scale_visible", "perpendicular_tick_labels", "font", + "inner_padding", "alarm_limits_from_pv", "show_alarm_limits", "level_lolo", "level_low", "level_high", "level_hihi", "minor_alarm_color", "major_alarm_color"); + /** 'inner_padding' — extra inset from widget edge to the fill bar, in pixels (0..20). + * Defaults to 3, matching the CSS inset of the stock JFX ProgressBar. + * Set to 0 for a tight, edge-to-edge bar with no surrounding gap. */ + public static final WidgetPropertyDescriptor propInnerPadding = + newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "inner_padding", + Messages.WidgetProperties_InnerPadding, 0, 20); + /** Widget descriptor */ public static final WidgetDescriptor WIDGET_DESCRIPTOR = new WidgetDescriptor("progressbar", WidgetCategory.MONITOR, @@ -177,9 +188,11 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) private volatile WidgetProperty horizontal; private volatile WidgetProperty scale_visible; private volatile WidgetProperty show_minor_ticks; + private volatile WidgetProperty show_scale_labels; private volatile WidgetProperty opposite_scale_visible; private volatile WidgetProperty perpendicular_tick_labels; private volatile WidgetProperty border_width_prop; + private volatile WidgetProperty inner_padding_prop; /** Constructor */ public ProgressBarWidget() @@ -198,9 +211,11 @@ protected void defineProperties(final List> properties) properties.add(horizontal = propHorizontal.createProperty(this, true)); properties.add(scale_visible = propScaleVisible.createProperty(this, false)); properties.add(show_minor_ticks = propShowMinorTicks.createProperty(this, true)); + properties.add(show_scale_labels = propShowScaleLabels.createProperty(this, true)); properties.add(opposite_scale_visible = propOppositeScaleVisible.createProperty(this, false)); properties.add(perpendicular_tick_labels = propPerpendicularTickLabels.createProperty(this, false)); properties.add(border_width_prop = propBorderWidth.createProperty(this, 0)); + properties.add(inner_padding_prop = propInnerPadding.createProperty(this, 3)); } /** @return 'font' property */ @@ -245,6 +260,12 @@ public WidgetProperty propShowMinorTicks() return show_minor_ticks; } + /** @return 'show_scale_labels' property */ + public WidgetProperty propShowScaleLabels() + { + return show_scale_labels; + } + /** @return 'opposite_scale_visible' property */ public WidgetProperty propOppositeScaleVisible() { @@ -262,4 +283,10 @@ public WidgetProperty propBorderWidth() { return border_width_prop; } + + /** @return 'inner_padding' property (extra inset between widget edge and fill bar, 0..20 px) */ + public WidgetProperty propInnerPadding() + { + return inner_padding_prop; + } } diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java index 15d9177622..573e3ed1b7 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java @@ -145,6 +145,11 @@ public EnumWidgetProperty createProperty(final Widget widget, newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "perpendicular_tick_labels", Messages.WidgetProperties_PerpendicularTickLabels); + /** 'show_scale_labels' — show tick label text on the scale (ticks are always drawn) */ + public static final WidgetPropertyDescriptor propShowScaleLabels = + newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "show_scale_labels", + Messages.WidgetProperties_ShowScaleLabels); + /** 'border_width' — width in pixels of the border drawn around the widget (0..5) */ public static final WidgetPropertyDescriptor propBorderWidth = newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "border_width", diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java index 918711c5c6..8771df90eb 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java @@ -139,6 +139,7 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) private volatile WidgetProperty empty_color; private volatile WidgetProperty scale_visible; private volatile WidgetProperty show_minor_ticks; + private volatile WidgetProperty show_scale_labels; private volatile WidgetProperty perpendicular_tick_labels; private volatile WidgetProperty opposite_scale_visible; private volatile WidgetProperty log_scale; @@ -164,6 +165,7 @@ protected void defineProperties(final List> properties) properties.add(scale_visible = propScaleVisible.createProperty(this, true)); properties.add(opposite_scale_visible = propOppositeScaleVisible.createProperty(this, false)); properties.add(show_minor_ticks = propShowMinorTicks.createProperty(this, true)); + properties.add(show_scale_labels = propShowScaleLabels.createProperty(this, true)); properties.add(perpendicular_tick_labels = propPerpendicularTickLabels.createProperty(this, false)); properties.add(log_scale = propLogscale.createProperty(this, false)); properties.add(horizontal = propHorizontal.createProperty(this, false)); @@ -221,6 +223,12 @@ public WidgetProperty propShowMinorTicks() return show_minor_ticks; } + /** @return 'show_scale_labels' property */ + public WidgetProperty propShowScaleLabels() + { + return show_scale_labels; + } + /** @return 'perpendicular_tick_labels' property */ public WidgetProperty propPerpendicularTickLabels() { diff --git a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties index 278fcc4753..33600004a2 100644 --- a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties +++ b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties @@ -171,6 +171,7 @@ WidgetProperties_Bit=Bit WidgetProperties_BorderAlarmSensitive=Alarm Border WidgetProperties_BorderColor=Border Color WidgetProperties_BorderWidth=Border Width +WidgetProperties_InnerPadding=Inner Padding WidgetProperties_CellColors=Cell Colors WidgetProperties_Class=Class WidgetProperties_ColorHiHi=Color HiHi @@ -310,6 +311,7 @@ WidgetProperties_ShowLimits=Show Limits WidgetProperties_ShowLow=Show Low WidgetProperties_ShowLoLo=Show LoLo WidgetProperties_ShowMinorTicks=Show minor ticks +WidgetProperties_ShowScaleLabels=Show scale labels WidgetProperties_PerpendicularTickLabels=Labels perpendicular to axis WidgetProperties_ShowOK=Show OK WidgetProperties_ShowScale=Show Scale diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java index 9f52b72261..fa1f337cbb 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java @@ -34,15 +34,16 @@ @SuppressWarnings("nls") public class RTProgressBarRepresentation extends RTScaledWidgetRepresentation { - /** Called once after {@link #tank} is created; switch on flat-track and - * progress-bar inner padding (set dynamically per {@code scale_visible} - * in {@link #applyLookToTank}). */ + /** Called once after {@link #tank} is created; switch on the flat-track + * rendering style for the unfilled bar region. All other per-widget + * settings (inner padding, scale labels, …) are applied on every + * look update by {@link #applyLookToTank}. */ @Override protected void configureTank() { - // The original JFX ProgressBar has a flat (solid) track background — - // no gradient. Enabling flat_track removes the darker-center gradient - // that RTTank normally uses for the Tank widget's empty region. + // The stock JFX ProgressBar has a flat (solid) track background. + // flat_track replaces RTTank's darker-center gradient so the empty + // region is uniformly filled, matching the CSS default style. tank.setFlatTrack(true); } @@ -62,12 +63,14 @@ protected void registerLookListeners() model_widget.propBackgroundColor().addUntypedPropertyListener(lookListener); model_widget.propScaleVisible().addUntypedPropertyListener(lookListener); model_widget.propShowMinorTicks().addUntypedPropertyListener(lookListener); + model_widget.propShowScaleLabels().addUntypedPropertyListener(lookListener); model_widget.propOppositeScaleVisible().addUntypedPropertyListener(lookListener); model_widget.propPerpendicularTickLabels().addUntypedPropertyListener(lookListener); model_widget.propBorderWidth().addUntypedPropertyListener(lookListener); model_widget.propLogScale().addUntypedPropertyListener(lookListener); model_widget.propFormat().addUntypedPropertyListener(lookListener); model_widget.propPrecision().addUntypedPropertyListener(lookListener); + model_widget.propInnerPadding().addUntypedPropertyListener(lookListener); model_widget.propHorizontal().addPropertyListener(orientationChangedListener); } @@ -81,12 +84,14 @@ protected void unregisterLookListeners() model_widget.propBackgroundColor().removePropertyListener(lookListener); model_widget.propScaleVisible().removePropertyListener(lookListener); model_widget.propShowMinorTicks().removePropertyListener(lookListener); + model_widget.propShowScaleLabels().removePropertyListener(lookListener); model_widget.propOppositeScaleVisible().removePropertyListener(lookListener); model_widget.propPerpendicularTickLabels().removePropertyListener(lookListener); model_widget.propBorderWidth().removePropertyListener(lookListener); model_widget.propLogScale().removePropertyListener(lookListener); model_widget.propFormat().removePropertyListener(lookListener); model_widget.propPrecision().removePropertyListener(lookListener); + model_widget.propInnerPadding().removePropertyListener(lookListener); model_widget.propHorizontal().removePropertyListener(orientationChangedListener); } @@ -103,16 +108,13 @@ protected void applyLookToTank(final double width, final double height) final boolean scale_visible = model_widget.propScaleVisible().getValue(); tank.setScaleVisible(scale_visible); tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); + tank.setScaleLabelsVisible(model_widget.propShowScaleLabels().getValue()); tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); tank.setBorderWidth(model_widget.propBorderWidth().getValue()); tank.setLogScale(model_widget.propLogScale().getValue()); tank.setLabelFormat(model_widget.propFormat().getValue(), model_widget.propPrecision().getValue()); - // When there is no scale the canvas fills the widget boundary edge-to-edge. - // Apply inner padding to match the inset margin of the stock JFX ProgressBar - // (~7 px total / ~3 px per side in the default CSS), so displays from older - // Phoebus versions look consistent after switching to scale mode. - tank.setInnerPadding(scale_visible ? 0 : 3); + tank.setInnerPadding(model_widget.propInnerPadding().getValue()); } } diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java index 5937894431..daca61bf6f 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/TankRepresentation.java @@ -42,6 +42,7 @@ protected void registerLookListeners() model_widget.propEmptyColor().addUntypedPropertyListener(lookListener); model_widget.propScaleVisible().addUntypedPropertyListener(lookListener); model_widget.propShowMinorTicks().addUntypedPropertyListener(lookListener); + model_widget.propShowScaleLabels().addUntypedPropertyListener(lookListener); model_widget.propPerpendicularTickLabels().addUntypedPropertyListener(lookListener); model_widget.propFormat().addUntypedPropertyListener(lookListener); model_widget.propPrecision().addUntypedPropertyListener(lookListener); @@ -63,6 +64,7 @@ protected void unregisterLookListeners() model_widget.propEmptyColor().removePropertyListener(lookListener); model_widget.propScaleVisible().removePropertyListener(lookListener); model_widget.propShowMinorTicks().removePropertyListener(lookListener); + model_widget.propShowScaleLabels().removePropertyListener(lookListener); model_widget.propPerpendicularTickLabels().removePropertyListener(lookListener); model_widget.propFormat().removePropertyListener(lookListener); model_widget.propPrecision().removePropertyListener(lookListener); @@ -82,6 +84,7 @@ protected void applyLookToTank(final double width, final double height) tank.setEmptyColor(JFXUtil.convert(model_widget.propEmptyColor().getValue())); tank.setScaleVisible(model_widget.propScaleVisible().getValue()); tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); + tank.setScaleLabelsVisible(model_widget.propShowScaleLabels().getValue()); tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); tank.setLogScale(model_widget.propLogScale().getValue()); tank.setLabelFormat(model_widget.propFormat().getValue(), diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index f59b192d5e..d9e17b1d3a 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -254,6 +254,21 @@ public void setFlatTrack(final boolean flat) requestUpdate(); } + /** Show or hide tick label text on the scale while keeping tick marks visible. + *

    Use this to stack multiple scaled widgets with a single labelled scale: + * only the first widget shows text; the rest show aligned tick marks only, + * saving horizontal (or vertical) space without losing alignment cues. + *

    Layout and repaint are triggered automatically by the axis when + * the value actually changes; no-op when unchanged. + * @param visible {@code true} (default) = labels shown; {@code false} = ticks only */ + public void setScaleLabelsVisible(final boolean visible) + { + // Each axis fires its own requestLayout()/requestRefresh() via plot_part_listener + // when the state changes, propagating need_layout and requestUpdate automatically. + scale.setScaleLabelsVisible(visible); + right_scale.setScaleLabelsVisible(visible); + } + /** @param color Background color */ public void setBackground(final javafx.scene.paint.Color color) { diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java index 109ad205c5..0f248419ca 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java @@ -60,6 +60,12 @@ public class YAxisImpl> extends NumericAxis impl /** Show on right side? */ private volatile boolean is_right = false; + /** When {@code false}, tick marks are drawn but tick label text is suppressed. + * Use this to stack multiple scaled widgets with a single labelled scale: + * only the first widget shows text; the rest show aligned tick marks only. + *

    Read on the Java2D render thread; written from the JFX thread — must be volatile. */ + private volatile boolean show_labels = true; + /** When {@code true}, rotated tick labels always use the 'up' direction * (bottom-to-top) regardless of {@link #is_right}. This keeps the text * orientation of a right-side scale identical to a left-side scale. @@ -161,6 +167,21 @@ public void setForceTextUp(final boolean force) force_text_up = force; } + /** Show or hide tick label text while keeping tick marks visible. + * When {@code false}, the axis is rendered as ticks-only, consuming + * less horizontal space. Tick spacing is unchanged, so stacked + * progress bars or tanks can share a single labelled scale. + * @param show {@code true} (default) = labels visible; {@code false} = ticks only + */ + public void setScaleLabelsVisible(final boolean show) + { + if (show_labels == show) + return; + show_labels = show; + requestLayout(); + requestRefresh(); + } + /** Add trace to axis * @param trace {@link Trace} * @throws IllegalArgumentException if trace already on axis @@ -209,6 +230,10 @@ public int getDesiredPixelSize(final Rectangle region, final Graphics2D gc) if (! isVisible()) return 0; + // Ticks-only mode: only tick marks, no label text — just TICK_LENGTH wide. + if (!show_labels) + return TICK_LENGTH; + this.region = region; gc.setFont(label_font); @@ -334,6 +359,10 @@ public int[] getPixelGaps(final Graphics2D gc) if (! isVisible()) return super.getPixelGaps(gc); + // Ticks-only mode: no labels extend past the tick positions. + if (!show_labels) + return new int[] { 0, 0 }; + gc.setFont(scale_font); final FontMetrics metrics = gc.getFontMetrics(); @@ -413,7 +442,7 @@ public void paint(final Graphics2D gc, final Rectangle plot_bounds) } gc.setStroke(old_width); - if (showLabel[mi]) + if (showLabel[mi] && show_labels) drawTickLabel(gc, y, tick.getLabel(), false); } @@ -428,8 +457,11 @@ public void paint(final Graphics2D gc, final Rectangle plot_bounds) gc.setColor(old_fg); gc.setBackground(old_bg); - gc.setFont(label_font); - paintLabels(gc); + if (show_labels) + { + gc.setFont(label_font); + paintLabels(gc); + } } protected void paintLabels(final Graphics2D gc) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 13eff931e9..450a352a7d 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -18,6 +18,24 @@ Date: TBD * Scan infor parser performance improvement. * PV race condition fix. * Highlight overdrawn area of embedded display in edit mode. +* RTPlot / Tank widget: new ``parallel_rendering`` preference + (``org.csstudio.javafx.rtplot/parallel_rendering``). + When ``true``, each RTTank instance renders on the shared thread-pool rather + than on the single global ``UpdateThrottle.TIMER`` thread, eliminating the + serialisation bottleneck that caused visible lag with many simultaneous Tank + or scale-mode Progress Bar widgets. Defaults to ``false``; requires restart. +* Display Builder: new ``progressbar_scale_mode`` preference + (``org.csstudio.display.builder.representation/progressbar_scale_mode``). + When ``true``, the Progress Bar widget is rendered by the RTTank engine, + adding a numeric scale, tick marks, format/precision control, an optional + second (opposite) scale, and alarm-limit lines (``level_lolo`` / + ``level_low`` / ``level_high`` / ``level_hihi``). When ``false`` (default), + the standard JFX ``ProgressBar`` look-and-feel is preserved. + Requires restart. +* Display Builder (internal): extracted ``RTScaledWidgetRepresentation`` + abstract base class, shared by ``TankRepresentation`` and the new + ``RTProgressBarRepresentation``, to eliminate duplicated range/orientation + update logic. Release 4.6.4 From 395fc79cd70b9a1892a8538d6f359b68b31b3118 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Sat, 4 Apr 2026 10:52:52 -0600 Subject: [PATCH 11/11] fix(rtplot): simplify inset comment + extract drawAlarmLimits() to reduce complexity --- .../org/csstudio/javafx/rtplot/RTTank.java | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index d9e17b1d3a..4e3263bcd3 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -596,11 +596,7 @@ private void computeLayout(final Graphics2D gc, final Rectangle bounds) ends[1] = Math.max(ends[1], r_ends[1]); } - // Inset = ceil(border_width/2) keeps the outer stroke edge inside the canvas. - // On sides with a scale the label area provides ample margin so inset=0. - // When there is no border, inset=1 is the original clip guard. - // inner_padding is added on top of all four sides regardless of scale presence; - // it is 0 for the standard tank and > 0 for the progress-bar track mode. + // Inset: half border-width rounded up, plus inner_padding on all sides. final int half_bw_ceil = (border_width + 1) / 2; final int ip = inner_padding; final int inset_left = (left_width == 0) ? Math.max(1, half_bw_ceil) + ip : ip; @@ -692,25 +688,7 @@ protected Image updateImageBuffer() gc.setStroke(new BasicStroke(1f)); } - // Draw alarm / warning limit lines over the tank body - final double lim_lolo = limit_lolo; - final double lim_lo = limit_lo; - final double lim_hi = limit_hi; - final double lim_hihi = limit_hihi; - if (normal && (!Double.isNaN(lim_lolo) || !Double.isNaN(lim_lo) || - !Double.isNaN(lim_hi) || !Double.isNaN(lim_hihi))) - { - if (limits_from_pv) - gc.setStroke(new BasicStroke(2f)); - else - gc.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, - BasicStroke.JOIN_MITER, 10f, new float[]{6f, 4f}, 0f)); - drawLimitLineAt(gc, plot_bounds, min, max, lim_lolo, limit_major_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_lo, limit_minor_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_hi, limit_minor_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_hihi, limit_major_color); - gc.setStroke(new BasicStroke(1f)); - } + drawAlarmLimits(gc, plot_bounds, normal, min, max); gc.dispose(); @@ -718,6 +696,29 @@ protected Image updateImageBuffer() return SwingFXUtils.toFXImage(image, null); } + /** Draw alarm/warning limit lines over the tank body, if any limits are set */ + private void drawAlarmLimits(final Graphics2D gc, final Rectangle plot_bounds, + final boolean normal, final double min, final double max) + { + final double lim_lolo = limit_lolo; + final double lim_lo = limit_lo; + final double lim_hi = limit_hi; + final double lim_hihi = limit_hihi; + if (!normal || (Double.isNaN(lim_lolo) && Double.isNaN(lim_lo) && + Double.isNaN(lim_hi) && Double.isNaN(lim_hihi))) + return; + if (limits_from_pv) + gc.setStroke(new BasicStroke(2f)); + else + gc.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, 10f, new float[]{6f, 4f}, 0f)); + drawLimitLineAt(gc, plot_bounds, min, max, lim_lolo, limit_major_color); + drawLimitLineAt(gc, plot_bounds, min, max, lim_lo, limit_minor_color); + drawLimitLineAt(gc, plot_bounds, min, max, lim_hi, limit_minor_color); + drawLimitLineAt(gc, plot_bounds, min, max, lim_hihi, limit_major_color); + gc.setStroke(new BasicStroke(1f)); + } + /** Request a complete redraw of the plot */ final public void requestUpdate() {