From 6c2855896cce44e013ced8eca8fa0685081bc694 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 18 Sep 2026 13:40:06 -0600 Subject: [PATCH 1/4] refactor(display): extract RTScaledWidgetRepresentation and add show_scale_labels to Tank Move everything in TankRepresentation that only depends on the ScaledPVWidget contract into an abstract RTScaledWidgetRepresentation: creating the RTTank, forwarding value and range updates, evaluating the alarm limits, the orientation transform and the update scheduling. TankRepresentation keeps the Tank specific listeners and colors. The behaviour of the Tank widget does not change. Add a 'show_scale_labels' property (default true) to the Tank so that stacked widgets can share one labelled scale: with the property off, YAxisImpl draws the tick marks and the axis line only. Tick positions are the same as with labels. TankWidgetUnitTest covers the new property. --- .../display/builder/model/Messages.java | 1 + .../builder/model/widgets/ScaledPVWidget.java | 31 +- .../builder/model/widgets/TankWidget.java | 29 +- .../display/builder/model/messages.properties | 1 + .../builder/model/messages_fr.properties | 1 + .../model/widgets/TankWidgetUnitTest.java | 4 + .../widgets/RTScaledWidgetRepresentation.java | 305 ++++++++++++++++++ .../javafx/widgets/TankRepresentation.java | 242 ++------------ .../org/csstudio/javafx/rtplot/RTTank.java | 11 + .../javafx/rtplot/internal/YAxisImpl.java | 39 ++- 10 files changed, 427 insertions(+), 237 deletions(-) create mode 100644 app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java 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..27bdbcc778 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 @@ -326,6 +326,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/ScaledPVWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java index 6f8e5ecd11..bdfd7af1b1 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.
  • @@ -125,6 +125,31 @@ 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); + + /** '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); + // ---- 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..8756a5f411 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 @@ -168,6 +147,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; @@ -193,6 +173,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)); @@ -250,6 +231,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..586c0cf6c7 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 @@ -310,6 +310,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/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties index a5764e7ef7..3bc18b6855 100644 --- a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties +++ b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties @@ -310,6 +310,7 @@ WidgetProperties_ShowLimits=Afficher les limites WidgetProperties_ShowLow=Afficher Low WidgetProperties_ShowLoLo=Afficher LoLo WidgetProperties_ShowMinorTicks=Afficher les petites graduations +WidgetProperties_ShowScaleLabels=Afficher les labels de l'échelle WidgetProperties_PerpendicularTickLabels=Labels perpendiculaires à l'axe WidgetProperties_ShowOK=Afficher OK WidgetProperties_ShowScale=Afficher l'échelle diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java index f544fa699e..57fe2b840b 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java @@ -77,6 +77,7 @@ public void testTankWidgetDefaults() assertThat(tank.propScaleVisible().getValue(), equalTo(true)); assertThat(tank.propOppositeScaleVisible().getValue(), equalTo(false)); assertThat(tank.propShowMinorTicks().getValue(), equalTo(true)); + assertThat(tank.propShowScaleLabels().getValue(), equalTo(true)); assertThat(tank.propPerpendicularTickLabels().getValue(), equalTo(false)); assertThat(tank.propFormat().getValue(), equalTo(ScaleFormat.DEFAULT)); assertThat(tank.propPrecision().getValue(), equalTo(2)); @@ -138,6 +139,7 @@ public void testXmlRoundTrip() throws Exception original.propOppositeScaleVisible().setValue(true); original.propBorderWidth().setValue(3); original.propPerpendicularTickLabels().setValue(true); + original.propShowScaleLabels().setValue(false); original.propFormat().setValue(ScaleFormat.DECIMAL); original.propPrecision().setValue(3); @@ -184,6 +186,7 @@ public void testXmlRoundTrip() throws Exception assertThat(tank.propOppositeScaleVisible().getValue(), equalTo(true)); assertThat(tank.propBorderWidth().getValue(), equalTo(3)); assertThat(tank.propPerpendicularTickLabels().getValue(), equalTo(true)); + assertThat(tank.propShowScaleLabels().getValue(), equalTo(false)); assertThat(tank.propFormat().getValue(), equalTo(ScaleFormat.DECIMAL)); assertThat(tank.propPrecision().getValue(), equalTo(3)); } @@ -213,5 +216,6 @@ public void testNewPropertiesAreOptional() throws Exception assertThat(xml, not(containsString(""))); assertThat(xml, not(containsString(""))); assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); } } 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..bff17b53a9 --- /dev/null +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java @@ -0,0 +1,305 @@ +/******************************************************************************* + * 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: + *

      + *
    • Creating and throttle-configuring the {@link RTTank}
    • + *
    • Forwarding PV value and display-range changes to the tank
    • + *
    • Evaluating alarm limit lines from PV metadata or widget properties
    • + *
    • Orientation transform (rotation for horizontal layout)
    • + *
    • Scheduling representation updates on property changes
    • + *
    + * + *

    Subclasses provide: + *

      + *
    • {@link #isHorizontal()}: the widget's own {@code horizontal} property
    • + *
    • {@link #registerLookListeners()} / {@link #unregisterLookListeners()}: + * listeners on the widget-specific appearance properties + * (colors, scale visibility, font, ...)
    • + *
    • {@link #applyLookToTank()}: push the current appearance properties + * to the tank after size and orientation have been set
    • + *
    + * + * @param concrete {@link ScaledPVWidget} subtype + * @author Heredie Delvalle + */ +@SuppressWarnings("nls") +public abstract class RTScaledWidgetRepresentation + extends RegionBaseRepresentation +{ + /** 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(); + + /** 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 and 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; + + @Override + public Pane createJFXNode() throws Exception + { + tank = new RTTank(); + tank.setUpdateThrottle(Preferences.image_update_delay, TimeUnit.MILLISECONDS); + return new Pane(tank); + } + + /** Register listeners on the {@link ScaledPVWidget} value and limit + * properties, then call {@link #registerLookListeners()} for the + * subclass to add its widget-specific appearance listeners. + *

    The current range, value and limits are applied once at the end. */ + @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 (colors, scale, font, ...) + registerLookListeners(); + + // Apply the current 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()}. */ + 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(); + + /** Called on every PV value update and on range-related property changes. + * Updates the range, the alarm limits from the PV metadata and the + * fill level of the tank. */ + 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(); + updateRange(vtype, limits_from_pv); + + if (model_widget.propAlarmLimitsFromPV().getValue()) + applyAlarmLimits(vtype); + + // In edit mode there is no PV, so the widget range is the effective range + 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()); + } + + /** @return whether this widget is currently in horizontal orientation */ + protected abstract boolean isHorizontal(); + + /** Swaps width and 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 the current widget-specific appearance properties to the tank. + * Called from {@link #updateChanges()} after size and orientation + * have been applied. */ + protected abstract void applyLookToTank(); + + @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 degrees 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(); + 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..2d514e86b0 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,31 @@ *******************************************************************************/ 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 colors. + * * @author Kay Kasemir - * @author Heredie Delvalle — CLS, alarm limits, dual scale, - * format/precision wiring + * @author Heredie Delvalle */ -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); @@ -62,36 +41,18 @@ protected void registerListeners() 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); - 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); @@ -102,171 +63,32 @@ protected void unregisterListeners() 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); - 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() { - 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.setScaleLabelsVisible(model_widget.propShowScaleLabels().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/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index 8c294c7b05..7ed55b8b34 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 @@ -284,6 +284,17 @@ public void setShowMinorTicks(final boolean show) requestUpdate(); } + /** Show or hide the tick labels while keeping the tick marks. + * Stacked widgets can then share one labelled scale: only the first + * shows labels, the others show aligned tick marks. + * @param visible {@code true} (default) for labels, {@code false} for ticks only */ + public void setScaleLabelsVisible(final boolean visible) + { + // The axes request layout and refresh themselves when this changes + scale.setScaleLabelsVisible(visible); + right_scale.setScaleLabelsVisible(visible); + } + /** Configure the number format used for scale tick labels. * @param format Display format; {@code null} or {@link ScaleFormat#DEFAULT} restores automatic formatting. * @param precision Number of decimal places; clamped to [0, 15]. 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..0df928fe37 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,11 @@ public class YAxisImpl> extends NumericAxis impl /** Show on right side? */ private volatile boolean is_right = false; + /** When {@code false}, only the tick marks are drawn: no tick labels + * and no axis label. Stacked widgets can then share one labelled scale. + * Read on the render thread, written from the JavaFX thread. */ + 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 +166,20 @@ public void setForceTextUp(final boolean force) force_text_up = force; } + /** Show or hide the tick labels and the axis label while keeping the + * tick marks. Tick positions do not change, so stacked widgets can + * share one labelled scale. + * @param show {@code true} (default) for labels, {@code false} for 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 @@ -210,6 +229,11 @@ public int getDesiredPixelSize(final Rectangle region, final Graphics2D gc) return 0; this.region = region; + + // Ticks only: the tick marks plus the axis line + if (!show_labels) + return TICK_LENGTH + 1; + gc.setFont(label_font); FontMetrics metrics = gc.getFontMetrics(); @@ -334,6 +358,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(); @@ -392,7 +420,9 @@ public void paint(final Graphics2D gc, final Rectangle plot_bounds) // Skip the visibility pass when LogTicks already thinned the labeled set: // a second greedy pass would destroy the intentional symmetric spacing. final boolean skipVisibility = (ticks instanceof LogTicks) && ((LogTicks) ticks).isThinned(); - final boolean[] showLabel = skipVisibility + final boolean[] showLabel = !show_labels + ? new boolean[majorTicks.size()] + : skipVisibility ? allLabeled(majorTicks) : computeTickLabelVisibility(majorTicks, gc.getFontMetrics()); @@ -428,8 +458,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) From 18dad8756b5ffe2504119c07494c9d5f39076435 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 18 Sep 2026 13:46:02 -0600 Subject: [PATCH 2/4] feat(display): make ProgressBarWidget a ScaledPVWidget Move the progress bar onto the ScaledPVWidget base class that the Tank already uses, so it carries the same range, scale and alarm limit properties, plus an inner padding. The scale look properties (font, foreground and fill color, log scale, scale visibility, tick options, border width) now live in ScaledPVWidget with a defineScaleLookProperties() helper, and the Tank uses the same fields in its own order. The border keeps the Tank's 'tank_border_width' name for all scaled widgets: legacy BOY files carry a 'border_width' element for every widget, which must not become a bar border. The stock renderer ignores the additions, and existing .bob files keep loading unchanged since the pre-existing properties keep their names. The BOY importer picks up 'show_scale', 'scale_font' and the 'level_hi' and 'level_lo' names that differ from ours. ProgressBarWidgetUnitTest covers defaults, legacy .bob and BOY files, the XML round trip and the list of renderer-only properties. --- .../display/builder/model/Messages.java | 1 + .../model/widgets/ProgressBarWidget.java | 114 +++++---- .../builder/model/widgets/ScaledPVWidget.java | 93 +++++++ .../builder/model/widgets/TankWidget.java | 80 +----- .../display/builder/model/messages.properties | 1 + .../builder/model/messages_fr.properties | 1 + .../widgets/ProgressBarWidgetUnitTest.java | 232 ++++++++++++++++++ .../model/widgets/TankWidgetUnitTest.java | 12 + 8 files changed, 401 insertions(+), 133 deletions(-) create mode 100644 app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ProgressBarWidgetUnitTest.java 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 27bdbcc778..9a62e6a502 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 @@ -241,6 +241,7 @@ public class Messages WidgetProperties_HourTickMarkColor, WidgetProperties_HourTickMarkVisible, WidgetProperties_Increment, + WidgetProperties_InnerPadding, WidgetProperties_InitialIndex, WidgetProperties_Insets, WidgetProperties_Interactive, 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..a8de7841f5 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 @@ -8,15 +8,11 @@ package org.csstudio.display.builder.model.widgets; 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.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 java.util.Arrays; import java.util.List; +import java.util.Set; import org.csstudio.display.builder.model.Version; import org.csstudio.display.builder.model.Widget; @@ -34,12 +30,41 @@ import org.w3c.dom.Element; /** Widget that displays a progress bar + * + *

    Extends {@link ScaledPVWidget} so the bar offers the same range, + * scale and alarm limit properties as the {@link TankWidget}. + * The scale related properties only take effect with the RTTank based + * renderer, see {@link #SCALE_MODE_PROPS}. + * + *

    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 their + * XML names. Older Phoebus versions ignore the new properties. + * * @author Kay Kasemir * @author Amanda Carpenter + * @author Heredie Delvalle - CLS, scale support */ @SuppressWarnings("nls") -public class ProgressBarWidget extends PVWidget +public class ProgressBarWidget extends ScaledPVWidget { + /** Properties that only have an effect with the RTTank based renderer + * ({@code progressbar_scale_mode=true}). + * + *

    The property editor hides these while the stock JavaFX renderer + * is in use. Properties that both renderers honour, like the range, + * {@code horizontal}, {@code log_scale}, fill and background color, + * are not listed here. + */ + public static final Set SCALE_MODE_PROPS = Set.of( + "format", "precision", "font", "foreground_color", + "scale_visible", "show_minor_ticks", "show_scale_labels", + "opposite_scale_visible", "perpendicular_tick_labels", + "inner_padding", "tank_border_width", + "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, @@ -55,7 +80,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) @@ -82,14 +107,18 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid 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 + // BOY names that differ from ours. level_hihi and level_lolo match. + readLegacyElement(model_reader, xml, "show_scale", bar.propScaleVisible()); + readLegacyElement(model_reader, xml, "scale_font", bar.propFont()); + readLegacyElement(model_reader, xml, "level_hi", bar.propLevelHigh()); + readLegacyElement(model_reader, xml, "level_lo", bar.propLevelLow()); + + // Create a companion TextUpdate widget for the BOY value label. if (XMLUtil.getChildBoolean(xml, "show_label").orElse(true)) { final Document doc = xml.getOwnerDocument(); @@ -116,6 +145,14 @@ public boolean configureFromXML(final ModelReader model_reader, final Widget wid return true; } + + private static void readLegacyElement(final ModelReader model_reader, final Element xml, + final String name, final WidgetProperty property) throws Exception + { + final Element element = XMLUtil.getChildElement(xml, name); + if (element != null) + property.readFromXML(model_reader, element); + } } @Override @@ -125,37 +162,24 @@ 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 fill_color; private volatile WidgetProperty background_color; - private volatile WidgetProperty horizontal; + private volatile WidgetProperty horizontal; + private volatile WidgetProperty inner_padding; /** Constructor */ public ProgressBarWidget() { - super(WIDGET_DESCRIPTOR.getType()); + super(WIDGET_DESCRIPTOR.getType(), 100, 20); } @Override protected void defineProperties(final List> properties) { super.defineProperties(properties); - properties.add(fill_color = propFillColor.createProperty(this, new WidgetColor(60, 255, 60))); + defineScaleLookProperties(properties, false, false); 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)); - } - - /** @return 'fill_color' property */ - public WidgetProperty propFillColor() - { - return fill_color; + properties.add(horizontal = propHorizontal.createProperty(this, true)); + properties.add(inner_padding = propInnerPadding.createProperty(this, 3)); } /** @return 'background_color' property */ @@ -164,33 +188,15 @@ 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() - { - return log_scale; - } - /** @return 'horizontal' property */ public WidgetProperty propHorizontal() { return horizontal; } + + /** @return 'inner_padding' property */ + public WidgetProperty propInnerPadding() + { + return inner_padding; + } } 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 bdfd7af1b1..b2478b5001 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 @@ -11,9 +11,13 @@ import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newColorPropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newDoublePropertyDescriptor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.newIntegerPropertyDescriptor; +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.propForegroundColor; 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 java.util.List; @@ -22,7 +26,10 @@ 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.NamedWidgetFonts; +import org.csstudio.display.builder.model.persist.WidgetFontService; import org.csstudio.display.builder.model.properties.EnumWidgetProperty; +import org.csstudio.display.builder.model.properties.WidgetFont; import org.phoebus.ui.color.NamedWidgetColors; import org.phoebus.ui.color.WidgetColor; import org.phoebus.ui.color.WidgetColorService; @@ -150,6 +157,18 @@ public EnumWidgetProperty createProperty(final Widget widget, newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "show_scale_labels", Messages.WidgetProperties_ShowScaleLabels); + /** 'tank_border_width': width in pixels of the border drawn around the + * bar (0..5). The name avoids the 'border_width' element that legacy + * BOY files carry for every widget. */ + public static final WidgetPropertyDescriptor propTankBorderWidth = + newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "tank_border_width", + Messages.WidgetProperties_BorderWidth, 0, 5); + + /** 'inner_padding': gap between the widget edge and the bar, in pixels (0..20) */ + public static final WidgetPropertyDescriptor propInnerPadding = + newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "inner_padding", + Messages.WidgetProperties_InnerPadding, 0, 20); + // ---- Instance fields ------------------------------------------------ private volatile WidgetProperty format; @@ -166,6 +185,19 @@ public EnumWidgetProperty createProperty(final Widget widget, private volatile WidgetProperty minor_alarm_color; private volatile WidgetProperty major_alarm_color; + // Scale look. Subclasses create these in defineScaleLookProperties(), + // or one by one when they need a different order or defaults. + protected volatile WidgetProperty font; + protected volatile WidgetProperty foreground; + protected volatile WidgetProperty fill_color; + protected volatile WidgetProperty log_scale; + protected volatile WidgetProperty scale_visible; + protected volatile WidgetProperty show_minor_ticks; + protected volatile WidgetProperty show_scale_labels; + protected volatile WidgetProperty opposite_scale_visible; + protected volatile WidgetProperty perpendicular_tick_labels; + protected volatile WidgetProperty border_width; + protected ScaledPVWidget(final String type, final int default_width, final int default_height) { super(type, default_width, default_height); @@ -203,6 +235,67 @@ protected void defineProperties(final List> properties) WidgetColorService.getColor(NamedWidgetColors.ALARM_MAJOR))); } + /** Define the scale look properties: font, foreground and fill color, + * log scale, scale visibility and tick options, border width. + * @param properties Property list of the widget + * @param show_scale Default for 'scale_visible' + * @param perpendicular_labels Default for 'perpendicular_tick_labels' + */ + protected void defineScaleLookProperties(final List> properties, + final boolean show_scale, + final boolean perpendicular_labels) + { + properties.add(font = propFont.createProperty(this, WidgetFontService.get(NamedWidgetFonts.DEFAULT))); + properties.add(foreground = propForegroundColor.createProperty(this, WidgetColorService.getColor(NamedWidgetColors.TEXT))); + properties.add(fill_color = propFillColor.createProperty(this, new WidgetColor(60, 255, 60))); + properties.add(log_scale = propLogscale.createProperty(this, false)); + properties.add(scale_visible = propScaleVisible.createProperty(this, show_scale)); + 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, perpendicular_labels)); + properties.add(border_width = propTankBorderWidth.createProperty(this, 0)); + } + + /** @return The scale look properties, for representations that listen to all of them */ + public List> getScaleLookProperties() + { + return List.of(font, foreground, fill_color, log_scale, + scale_visible, show_minor_ticks, show_scale_labels, + opposite_scale_visible, perpendicular_tick_labels, + border_width); + } + + /** @return 'font' property */ + public WidgetProperty propFont() { return font; } + + /** @return 'foreground_color' property */ + public WidgetProperty propForeground() { return foreground; } + + /** @return 'fill_color' property */ + public WidgetProperty propFillColor() { return fill_color; } + + /** @return 'log_scale' property */ + public WidgetProperty propLogScale() { return log_scale; } + + /** @return 'scale_visible' property */ + public WidgetProperty propScaleVisible() { return scale_visible; } + + /** @return 'show_minor_ticks' property */ + 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() { return opposite_scale_visible; } + + /** @return 'perpendicular_tick_labels' property */ + public WidgetProperty propPerpendicularTickLabels() { return perpendicular_tick_labels; } + + /** @return 'tank_border_width' property (0 = no border) */ + public WidgetProperty propBorderWidth() { return border_width; } + /** @return 'format' property (scale label format) */ public WidgetProperty propFormat() { return 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 8756a5f411..cac4d4f91f 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; @@ -31,7 +30,6 @@ 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.properties.WidgetFont; import org.phoebus.ui.color.NamedWidgetColors; import org.phoebus.ui.color.WidgetColor; import org.phoebus.ui.color.WidgetColorService; @@ -73,13 +71,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); @@ -140,19 +131,9 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) return new CustomConfigurator(persisted_version); } - private volatile WidgetProperty font; - private volatile WidgetProperty foreground; private volatile WidgetProperty background; - private volatile WidgetProperty fill_color; 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; private volatile WidgetProperty horizontal; - private volatile WidgetProperty border_width_prop; /** Constructor */ @@ -177,7 +158,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 = propTankBorderWidth.createProperty(this, 0)); } @Override @@ -189,81 +170,22 @@ public WidgetProperty getProperty(String name) throws IllegalArgumentExceptio return super.getProperty(name); } - /** @return 'font' property */ - public WidgetProperty propFont() - { - return font; - } - - /** @return 'foreground_color' property */ - public WidgetProperty propForeground() - { - return foreground; - } - /** @return 'background_color' property */ public WidgetProperty propBackground() { return background; } - /** @return 'fill_color' property */ - public WidgetProperty propFillColor() - { - return fill_color; - } - /** @return 'empty_color' property */ public WidgetProperty propEmptyColor() { return empty_color; } - /** @return 'scale_visible' property */ - public WidgetProperty propScaleVisible() - { - return scale_visible; - } - - /** @return 'show_minor_ticks' property */ - 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() - { - return perpendicular_tick_labels; - } - - /** @return 'opposite_scale_visible' property */ - public WidgetProperty propOppositeScaleVisible() - { - return opposite_scale_visible; - } - - /** @return 'log_scale' property */ - public WidgetProperty propLogScale() - { - return log_scale; - } - /** @return 'horizontal' property */ public WidgetProperty propHorizontal() { return horizontal; } - /** @return 'border_width' property (0 = no border) */ - public WidgetProperty propBorderWidth() - { - return border_width_prop; - } } 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 586c0cf6c7..850272fa44 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 @@ -227,6 +227,7 @@ WidgetProperties_HourColor=Hour Color WidgetProperties_HourTickMarkColor=Hour Tick Mark Color WidgetProperties_HourTickMarkVisible=Hour Tick Mark Visible WidgetProperties_Increment=Increment +WidgetProperties_InnerPadding=Inner Padding WidgetProperties_InitialIndex=Initial Index WidgetProperties_Insets=Insets WidgetProperties_Interactive=Interactive diff --git a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties index 3bc18b6855..1b8ddb720b 100644 --- a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties +++ b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages_fr.properties @@ -227,6 +227,7 @@ WidgetProperties_HourColor=Couleur de l'heure WidgetProperties_HourTickMarkColor=Couleur des graduations horaires WidgetProperties_HourTickMarkVisible=Graduations horaires visibles WidgetProperties_Increment=Incrément +WidgetProperties_InnerPadding=Marge intérieure WidgetProperties_InitialIndex=Index initial WidgetProperties_Insets=Marges WidgetProperties_Interactive=Interactif diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ProgressBarWidgetUnitTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ProgressBarWidgetUnitTest.java new file mode 100644 index 0000000000..bdc953f9a3 --- /dev/null +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ProgressBarWidgetUnitTest.java @@ -0,0 +1,232 @@ +/******************************************************************************* + * Copyright (c) 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.model.widgets; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.List; + +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.junit.jupiter.api.Test; +import org.phoebus.ui.color.WidgetColor; +import org.phoebus.ui.vtype.ScaleFormat; + +/** JUnit tests for {@link ProgressBarWidget} as a {@link ScaledPVWidget} + * + *

    The progress bar must keep loading the files written before it had + * a scale, and files written now must not confuse an older Phoebus. + * + * @author Heredie Delvalle + */ +@SuppressWarnings("nls") +public class ProgressBarWidgetUnitTest +{ + /** Defaults, in particular those that the stock renderer relies on */ + @Test + public void testDefaults() + { + final ProgressBarWidget bar = new ProgressBarWidget(); + + assertThat(bar.propWidth().getValue(), equalTo(100)); + assertThat(bar.propHeight().getValue(), equalTo(20)); + assertThat(bar.propHorizontal().getValue(), equalTo(true)); + assertThat(bar.propLimitsFromPV().getValue(), equalTo(true)); + assertThat(bar.propMinimum().getValue(), equalTo(0.0)); + assertThat(bar.propMaximum().getValue(), equalTo(100.0)); + assertThat(bar.propLogScale().getValue(), equalTo(false)); + assertThat(bar.propFillColor().getValue(), equalTo(new WidgetColor(60, 255, 60))); + assertThat(bar.propBackgroundColor().getValue(), equalTo(new WidgetColor(250, 250, 250))); + + // A bar looks like a bar until a scale is asked for + assertThat(bar.propScaleVisible().getValue(), equalTo(false)); + assertThat(bar.propShowMinorTicks().getValue(), equalTo(true)); + assertThat(bar.propShowScaleLabels().getValue(), equalTo(true)); + assertThat(bar.propOppositeScaleVisible().getValue(), equalTo(false)); + assertThat(bar.propPerpendicularTickLabels().getValue(), equalTo(false)); + assertThat(bar.propBorderWidth().getValue(), equalTo(0)); + assertThat(bar.propInnerPadding().getValue(), equalTo(3)); + assertThat(bar.propFormat().getValue(), equalTo(ScaleFormat.DEFAULT)); + assertThat(bar.propShowAlarmLimits().getValue(), equalTo(false)); + } + + /** Every name that the property panel hides for the stock renderer must be a real property */ + @Test + public void testScaleModePropertiesExist() + { + final ProgressBarWidget bar = new ProgressBarWidget(); + for (String name : ProgressBarWidget.SCALE_MODE_PROPS) + assertTrue(bar.checkProperty(name).isPresent(), "Unknown property " + name); + } + + /** A file written before the progress bar had a scale must load as before */ + @Test + public void testLegacyFileLoads() throws Exception + { + final String xml = + "\n" + + "\n" + + " \n" + + " Bar\n" + + " loc://x\n" + + " \n" + + " \n" + + " false\n" + + " 5.0\n" + + " 50.0\n" + + " true\n" + + " false\n" + + " \n" + + ""; + final ProgressBarWidget bar = (ProgressBarWidget) read(xml); + + assertThat(bar.propFillColor().getValue(), equalTo(new WidgetColor(10, 20, 30))); + assertThat(bar.propBackgroundColor().getValue(), equalTo(new WidgetColor(1, 2, 3))); + assertThat(bar.propLimitsFromPV().getValue(), equalTo(false)); + assertThat(bar.propMinimum().getValue(), equalTo(5.0)); + assertThat(bar.propMaximum().getValue(), equalTo(50.0)); + assertThat(bar.propLogScale().getValue(), equalTo(true)); + assertThat(bar.propHorizontal().getValue(), equalTo(false)); + // Scale properties keep their defaults + assertThat(bar.propScaleVisible().getValue(), equalTo(false)); + } + + /** Non-default values of the new properties survive save and load */ + @Test + public void testXmlRoundTrip() throws Exception + { + final ProgressBarWidget original = new ProgressBarWidget(); + original.propScaleVisible().setValue(true); + original.propShowScaleLabels().setValue(false); + original.propOppositeScaleVisible().setValue(true); + original.propPerpendicularTickLabels().setValue(true); + original.propBorderWidth().setValue(2); + original.propInnerPadding().setValue(7); + original.propFormat().setValue(ScaleFormat.EXPONENTIAL); + original.propPrecision().setValue(1); + original.propShowAlarmLimits().setValue(true); + original.propAlarmLimitsFromPV().setValue(false); + original.propLevelHigh().setValue(80.0); + original.propMinimum().setValue(100.0); + original.propMaximum().setValue(0.0); + + final String xml = write(original, false); + assertThat(xml, containsString("")); + assertThat(xml, containsString("")); + assertThat(xml, containsString("")); + + final ProgressBarWidget bar = (ProgressBarWidget) read(xml); + assertThat(bar.propScaleVisible().getValue(), equalTo(true)); + assertThat(bar.propShowScaleLabels().getValue(), equalTo(false)); + assertThat(bar.propOppositeScaleVisible().getValue(), equalTo(true)); + assertThat(bar.propPerpendicularTickLabels().getValue(), equalTo(true)); + assertThat(bar.propBorderWidth().getValue(), equalTo(2)); + assertThat(bar.propInnerPadding().getValue(), equalTo(7)); + assertThat(bar.propFormat().getValue(), equalTo(ScaleFormat.EXPONENTIAL)); + assertThat(bar.propPrecision().getValue(), equalTo(1)); + assertThat(bar.propShowAlarmLimits().getValue(), equalTo(true)); + assertThat(bar.propAlarmLimitsFromPV().getValue(), equalTo(false)); + assertThat(bar.propLevelHigh().getValue(), equalTo(80.0)); + assertThat(bar.propMinimum().getValue(), equalTo(100.0)); + assertThat(bar.propMaximum().getValue(), equalTo(0.0)); + } + + /** Only changed properties are written, so a file that uses the + * pre-existing properties looks the same as before */ + @Test + public void testLegacyPropertiesWriteNoNewElements() throws Exception + { + final ProgressBarWidget bar = new ProgressBarWidget(); + bar.propFillColor().setValue(new WidgetColor(1, 2, 3)); + bar.propMinimum().setValue(5.0); + bar.propMaximum().setValue(50.0); + bar.propLogScale().setValue(true); + bar.propHorizontal().setValue(false); + final String xml = write(bar, true); + for (String name : List.of("fill_color", "minimum", "maximum", "log_scale", "horizontal")) + assertThat(xml, containsString("<" + name + ">")); + for (String name : ProgressBarWidget.SCALE_MODE_PROPS) + assertThat(xml, not(containsString("<" + name + ">"))); + } + + /** A BOY progress bar is imported with its scale and levels, and the + * generic BOY 'border_width' element does not become a bar border */ + @Test + public void testBoyFileLoads() throws Exception + { + final String xml = + "\n" + + "\n" + + " \n" + + " Bar\n" + + " loc://x\n" + + " 10\n" + + " 10\n" + + " 200\n" + + " 60\n" + + " 0\n" + + " 1\n" + + " false\n" + + " true\n" + + " false\n" + + " 80.0\n" + + " 90.0\n" + + " 20.0\n" + + " 10.0\n" + + " false\n" + + " 0.0\n" + + " 50.0\n" + + " \n" + + ""; + final ProgressBarWidget bar = (ProgressBarWidget) read(xml); + + assertThat(bar.propBorderWidth().getValue(), equalTo(0)); + assertThat(bar.propScaleVisible().getValue(), equalTo(false)); + assertThat(bar.propLevelHigh().getValue(), equalTo(80.0)); + assertThat(bar.propLevelHiHi().getValue(), equalTo(90.0)); + assertThat(bar.propLevelLow().getValue(), equalTo(20.0)); + assertThat(bar.propLevelLoLo().getValue(), equalTo(10.0)); + assertThat(bar.propLimitsFromPV().getValue(), equalTo(false)); + assertThat(bar.propMaximum().getValue(), equalTo(50.0)); + // BOY reserved 25 px above the bar for the markers + assertThat(bar.propY().getValue(), equalTo(35)); + assertThat(bar.propHeight().getValue(), equalTo(35)); + } + + private static String write(final Widget widget, final boolean skip_defaults) throws Exception + { + final DisplayModel model = new DisplayModel(); + model.runtimeChildren().addChild(widget); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final boolean saved = ModelWriter.skip_defaults; + ModelWriter.skip_defaults = skip_defaults; + try (ModelWriter writer = new ModelWriter(out)) + { + writer.writeModel(model); + } + finally + { + ModelWriter.skip_defaults = saved; + } + return out.toString(); + } + + private static Widget read(final String xml) throws Exception + { + final ModelReader reader = new ModelReader(new ByteArrayInputStream(xml.getBytes())); + return reader.readModel().getChildren().get(0); + } +} diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java index 57fe2b840b..82df2a5bd1 100644 --- a/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/TankWidgetUnitTest.java @@ -86,6 +86,18 @@ public void testTankWidgetDefaults() assertThat(tank.propBorderWidth().getValue(), equalTo(0)); } + /** The Tank defines the shared scale look properties in its own order; + * the list for representations must contain all of them */ + @Test + public void testScaleLookProperties() + { + final TankWidget tank = new TankWidget(); + final List> look = tank.getScaleLookProperties(); + assertThat(look.size(), equalTo(10)); + assertTrue(look.contains(tank.propBorderWidth())); + assertTrue(look.contains(tank.propForeground())); + } + /** Verify that alarm properties appear together and in the expected * order when listed in the property panel. * From 7e3565c52b9daed823095561c88655ba9aa8ec3f Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 18 Sep 2026 13:46:02 -0600 Subject: [PATCH 3/4] feat(display): RTTank based Progress Bar renderer, opt-in via preference Add RTProgressBarRepresentation, which draws the progress bar with the RTTank engine shared with the Tank widget. This gives the bar a numeric scale with format and precision, an optional opposite scale, minor ticks, tick-only mode for stacked bars, and alarm limit lines. RTTank gains an inner padding and a flat track for the progress bar look. RTScaledWidgetRepresentation gains registerScaleLookListeners() and applyScaleLook() for the scale look properties that ScaledPVWidget defines; TankRepresentation and the new representation use them and only handle their own extras. The renderer is selected with the new preference org.csstudio.display.builder.representation/progressbar_scale_mode which defaults to false. With the default, the stock JavaFX ProgressBarRepresentation is used and nothing changes for existing displays. The property panel hides the renderer-only properties while the stock renderer is in use. --- .../properties/PropertyPanelSection.java | 21 ++++++ .../widgets/BaseWidgetRepresentations.java | 5 +- .../widgets/RTProgressBarRepresentation.java | 74 +++++++++++++++++++ .../widgets/RTScaledWidgetRepresentation.java | 54 ++++++++++++++ .../javafx/widgets/TankRepresentation.java | 43 +---------- .../builder/representation/Preferences.java | 7 ++ ...play_representation_preferences.properties | 7 ++ .../org/csstudio/javafx/rtplot/RTTank.java | 41 ++++++++-- 8 files changed, 205 insertions(+), 47 deletions(-) create mode 100644 app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.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..fe4502af1d 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; @@ -148,6 +150,22 @@ public boolean hasFocus() { return has_focus; } + /** Some widgets can be drawn by the stock JavaFX renderer or by an + * RTTank based one that adds a scale, selected via preference. + * Properties that only the RTTank renderer honours are hidden while + * the stock renderer is in use, so the panel only lists what has + * an effect. + * + * @param property Property about to be listed + * @return {@code true} if the property has no effect with the current renderer + */ + private static boolean unusedByCurrentRenderer(final WidgetProperty property) { + if (property.getWidget() instanceof ProgressBarWidget) + return !Preferences.progressbar_scale_mode + && ProgressBarWidget.SCALE_MODE_PROPS.contains(property.getName()); + return false; + } + void fill(final UndoableActionManager undo, final Collection> properties, final List other) { @@ -163,6 +181,9 @@ void fill(final UndoableActionManager undo, if (property instanceof WidgetClassProperty && class_mode) continue; + if (unusedByCurrentRenderer(property)) + continue; + // Start of new category that needs to be shown? if (property.getCategory() != category) { category = property.getCategory(); 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..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 @@ -55,6 +55,7 @@ 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; @@ -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/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..edd6e7746f --- /dev/null +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTProgressBarRepresentation.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 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; + +/** Progress Bar representation based on {@link org.csstudio.javafx.rtplot.RTTank} + * + *

    Adds a numeric scale with format/precision, an optional second scale + * and alarm limit lines to the progress bar. Used instead of the stock + * {@link ProgressBarRepresentation} when the {@code progressbar_scale_mode} + * preference is set. + * + *

    Value, range, alarm limit and orientation handling are shared with + * the Tank in {@link RTScaledWidgetRepresentation}. This class only maps + * the progress bar's appearance properties onto the tank. + * + * @author Heredie Delvalle + */ +@SuppressWarnings("nls") +public class RTProgressBarRepresentation extends RTScaledWidgetRepresentation +{ + @Override + protected void configureTank() + { + // The stock ProgressBar has a plain track, without the tank's + // shaded gradient for the empty region + tank.setFlatTrack(true); + } + + @Override + protected boolean isHorizontal() + { + return model_widget.propHorizontal().getValue(); + } + + @Override + protected void registerLookListeners() + { + registerScaleLookListeners(); + model_widget.propBackgroundColor().addUntypedPropertyListener(lookListener); + model_widget.propInnerPadding().addUntypedPropertyListener(lookListener); + model_widget.propHorizontal().addPropertyListener(orientationChangedListener); + } + + @Override + protected void unregisterLookListeners() + { + unregisterScaleLookListeners(); + model_widget.propBackgroundColor().removePropertyListener(lookListener); + model_widget.propInnerPadding().removePropertyListener(lookListener); + model_widget.propHorizontal().removePropertyListener(orientationChangedListener); + } + + @Override + protected void applyLookToTank() + { + applyScaleLook(); + // Background and empty color are the same so the unfilled part of + // the bar blends with the margin, like the stock progress bar track + final Color background = JFXUtil.convert(model_widget.propBackgroundColor().getValue()); + tank.setBackground(background); + tank.setEmptyColor(background); + tank.setInnerPadding(model_widget.propInnerPadding().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 index bff17b53a9..63c80f08bb 100644 --- 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 @@ -45,8 +45,12 @@ * (colors, scale visibility, font, ...) *

  • {@link #applyLookToTank()}: push the current appearance properties * to the tank after size and orientation have been set
  • + *
  • {@link #configureTank()}: optional one-time tank setup
  • * * + *

    {@link #registerScaleLookListeners()} and {@link #applyScaleLook()} + * handle the scale look properties that all {@link ScaledPVWidget}s share. + * * @param concrete {@link ScaledPVWidget} subtype * @author Heredie Delvalle */ @@ -85,9 +89,15 @@ public Pane createJFXNode() throws Exception { tank = new RTTank(); tank.setUpdateThrottle(Preferences.image_update_delay, TimeUnit.MILLISECONDS); + configureTank(); return new Pane(tank); } + /** Called once after the tank is created, for one-time settings like a rendering style */ + protected void configureTank() + { + } + /** Register listeners on the {@link ScaledPVWidget} value and limit * properties, then call {@link #registerLookListeners()} for the * subclass to add its widget-specific appearance listeners. @@ -123,6 +133,30 @@ protected void registerListeners() limitsChanged(null, null, null); } + /** Listen to the widget size, the label format and the scale look + * properties shared by all scaled widgets. + * Subclasses call this from {@link #registerLookListeners()}. */ + protected void registerScaleLookListeners() + { + model_widget.propWidth().addUntypedPropertyListener(lookListener); + model_widget.propHeight().addUntypedPropertyListener(lookListener); + model_widget.propFormat().addUntypedPropertyListener(lookListener); + model_widget.propPrecision().addUntypedPropertyListener(lookListener); + for (WidgetProperty property : model_widget.getScaleLookProperties()) + property.addUntypedPropertyListener(lookListener); + } + + /** Undo {@link #registerScaleLookListeners()} */ + protected void unregisterScaleLookListeners() + { + model_widget.propWidth().removePropertyListener(lookListener); + model_widget.propHeight().removePropertyListener(lookListener); + model_widget.propFormat().removePropertyListener(lookListener); + model_widget.propPrecision().removePropertyListener(lookListener); + for (WidgetProperty property : model_widget.getScaleLookProperties()) + property.removePropertyListener(lookListener); + } + /** 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, @@ -263,6 +297,26 @@ protected void orientationChanged(final WidgetProperty prop, toolkit.scheduleUpdate(this); } + /** Push the scale look properties shared by all scaled widgets to the + * tank: font, colors, log scale, label format, scale visibility and + * tick options, border width. + * Subclasses call this from {@link #applyLookToTank()}. */ + protected void applyScaleLook() + { + tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); + tank.setForeground(JFXUtil.convert(model_widget.propForeground().getValue())); + tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); + tank.setLogScale(model_widget.propLogScale().getValue()); + tank.setLabelFormat(model_widget.propFormat().getValue(), + model_widget.propPrecision().getValue()); + tank.setScaleVisible(model_widget.propScaleVisible().getValue()); + 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()); + } + /** Push the current widget-specific appearance properties to the tank. * Called from {@link #updateChanges()} after size and orientation * have been applied. */ 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 2d514e86b0..bf86521954 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 @@ -32,63 +32,26 @@ protected boolean isHorizontal() @Override protected void registerLookListeners() { - model_widget.propWidth().addUntypedPropertyListener(lookListener); - model_widget.propHeight().addUntypedPropertyListener(lookListener); - model_widget.propFont().addUntypedPropertyListener(lookListener); - model_widget.propForeground().addUntypedPropertyListener(lookListener); + registerScaleLookListeners(); model_widget.propBackground().addUntypedPropertyListener(lookListener); - model_widget.propFillColor().addUntypedPropertyListener(lookListener); 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); - model_widget.propOppositeScaleVisible().addUntypedPropertyListener(lookListener); - model_widget.propBorderWidth().addUntypedPropertyListener(lookListener); - model_widget.propLogScale().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.propForeground().removePropertyListener(lookListener); + unregisterScaleLookListeners(); model_widget.propBackground().removePropertyListener(lookListener); - model_widget.propFillColor().removePropertyListener(lookListener); 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); - model_widget.propOppositeScaleVisible().removePropertyListener(lookListener); - model_widget.propBorderWidth().removePropertyListener(lookListener); - model_widget.propLogScale().removePropertyListener(lookListener); model_widget.propHorizontal().removePropertyListener(orientationChangedListener); } @Override protected void applyLookToTank() { - tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); + applyScaleLook(); 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.setScaleLabelsVisible(model_widget.propShowScaleLabels().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/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index 7ed55b8b34..302c24548c 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,15 @@ 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 the canvas edge to the plot body on all four sides. + * 0 for the tank look; a progress bar uses it for the gap between the + * widget edge and the bar. */ + private volatile int inner_padding = 0; + + /** Paint the empty part of the tank in a solid color instead of the + * shaded gradient, like the track of a progress bar. */ + private volatile boolean flat_track = false; + /** Current value, i.e. fill level */ private volatile double value = 5.0; @@ -219,6 +228,22 @@ public void setBorderWidth(final int width) requestUpdate(); } + /** @param pixels Extra inset from all four canvas edges to the plot body, 0..20 */ + public void setInnerPadding(final int pixels) + { + inner_padding = Math.max(0, Math.min(20, pixels)); + need_layout.set(true); + requestUpdate(); + } + + /** @param flat Paint the empty part of the tank solid ({@code true}) + * or with the shaded gradient ({@code false}, default) */ + public void setFlatTrack(final boolean flat) + { + flat_track = flat; + requestUpdate(); + } + /** @param color Background color */ public void setBackground(final javafx.scene.paint.Color color) { @@ -560,11 +585,13 @@ 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 all four sides regardless of scale presence. 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; @@ -620,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 1774da5d90270dd2bf0b5d30029d2456887f4751 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 18 Sep 2026 13:46:02 -0600 Subject: [PATCH 4/4] feat(convert-edm): map EDM bar scale, range, precision and border The EDM 'activeBar' carries a scale flag, a fixed range, a precision and a border flag that the progress bar can now represent. --- .../edm/widgets/Convert_activeBarClass.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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..79c1814872 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,22 @@ 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())); + + 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()); + } + + // EDM precision 0 usually means "not set" + if (r.getPrecision() > 0) + widget.propPrecision().setValue(r.getPrecision()); + + // EDM only knows border on/off + if (r.isBorder()) + widget.propBorderWidth().setValue(1); } @Override