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)