diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java index 1f56ddea77..86df17727c 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java @@ -185,6 +185,7 @@ public class Messages WidgetProperties_BorderAlarmSensitive, WidgetProperties_BorderColor, WidgetProperties_BorderWidth, + WidgetProperties_InnerPadding, WidgetProperties_CellColors, WidgetProperties_Class, WidgetProperties_ColorHiHi, @@ -326,6 +327,7 @@ public class Messages WidgetProperties_ShowLoLo, WidgetProperties_ShowMinorTicks, WidgetProperties_PerpendicularTickLabels, + WidgetProperties_ShowScaleLabels, WidgetProperties_ShowOK, WidgetProperties_ShowScale, WidgetProperties_ShowUnits, diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java index 6f8e5ecd11..573e3ed1b7 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ScaledPVWidget.java @@ -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,36 @@ 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); + + /** 'border_width' — width in pixels of the border drawn around the widget (0..5) */ + public static final WidgetPropertyDescriptor propBorderWidth = + newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "border_width", + Messages.WidgetProperties_BorderWidth, 0, 5); + // ---- Instance fields ------------------------------------------------ private volatile WidgetProperty format; diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java index 48666c55da..8771df90eb 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/TankWidget.java @@ -7,9 +7,7 @@ *******************************************************************************/ 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; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFillColor; import static org.csstudio.display.builder.model.properties.CommonWidgetProperties.propFont; @@ -74,36 +72,9 @@ 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); - /** '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 +139,7 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) private volatile WidgetProperty empty_color; private volatile WidgetProperty scale_visible; private volatile WidgetProperty show_minor_ticks; + private volatile WidgetProperty show_scale_labels; private volatile WidgetProperty perpendicular_tick_labels; private volatile WidgetProperty opposite_scale_visible; private volatile WidgetProperty log_scale; @@ -193,10 +165,11 @@ 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)); - properties.add(border_width_prop = propTankBorderWidth.createProperty(this, 0)); + properties.add(border_width_prop = propBorderWidth.createProperty(this, 0)); } @Override @@ -250,6 +223,12 @@ public WidgetProperty propShowMinorTicks() return show_minor_ticks; } + /** @return 'show_scale_labels' property */ + public WidgetProperty propShowScaleLabels() + { + return show_scale_labels; + } + /** @return 'perpendicular_tick_labels' property */ public WidgetProperty propPerpendicularTickLabels() { diff --git a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties index 278fcc4753..33600004a2 100644 --- a/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties +++ b/app/display/model/src/main/resources/org/csstudio/display/builder/model/messages.properties @@ -171,6 +171,7 @@ WidgetProperties_Bit=Bit WidgetProperties_BorderAlarmSensitive=Alarm Border WidgetProperties_BorderColor=Border Color WidgetProperties_BorderWidth=Border Width +WidgetProperties_InnerPadding=Inner Padding WidgetProperties_CellColors=Cell Colors WidgetProperties_Class=Class WidgetProperties_ColorHiHi=Color HiHi @@ -310,6 +311,7 @@ WidgetProperties_ShowLimits=Show Limits WidgetProperties_ShowLow=Show Low WidgetProperties_ShowLoLo=Show LoLo WidgetProperties_ShowMinorTicks=Show minor ticks +WidgetProperties_ShowScaleLabels=Show scale labels WidgetProperties_PerpendicularTickLabels=Labels perpendicular to axis WidgetProperties_ShowOK=Show OK WidgetProperties_ShowScale=Show Scale diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java new file mode 100644 index 0000000000..a86007a6e9 --- /dev/null +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/RTScaledWidgetRepresentation.java @@ -0,0 +1,343 @@ +/******************************************************************************* + * Copyright (c) 2015-2026 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.csstudio.display.builder.representation.javafx.widgets; + +import java.util.concurrent.TimeUnit; + +import org.csstudio.display.builder.model.DirtyFlag; +import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; +import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.WidgetPropertyListener; +import org.csstudio.display.builder.model.util.VTypeUtil; +import org.csstudio.display.builder.model.widgets.ScaledPVWidget; +import org.csstudio.display.builder.representation.Preferences; +import org.csstudio.display.builder.representation.javafx.JFXUtil; +import org.csstudio.javafx.rtplot.RTTank; +import org.epics.util.stats.Range; +import org.epics.vtype.Display; +import org.epics.vtype.VType; + +import javafx.scene.layout.Pane; +import javafx.scene.transform.Rotate; +import javafx.scene.transform.Translate; + +/** Abstract base for widget representations whose JFX node is an {@link RTTank}. + * + *

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

      + *
    • 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 (90° rotation for horizontal layout)
    • + *
    • Scheduling representation updates on property changes
    • + *
    + * + *

    Subclasses provide: + *

      + *
    • {@link #isHorizontal()} — read the widget's own {@code horizontal} property
    • + *
    • {@link #registerLookListeners()} / {@link #unregisterLookListeners()} — + * add / remove listeners on widget-specific appearance properties + * (colours, scale visibility, font, …)
    • + *
    • {@link #applyLookToTank(double, double)} — push the current appearance + * properties to the tank after size and orientation have been set
    • + *
    • {@link #configureTank()} — called once after tank creation; override to + * perform any one-time tank setup (e.g. enabling a rendering style)
    • + *
    + * + *

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

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

    All shared RTTank wiring (value updates, alarm limits, + * orientation handling) lives in {@link RTScaledWidgetRepresentation}. + * This class contributes only the Tank-specific appearance properties: + * background, foreground, fill and empty colours. + * * @author Kay Kasemir * @author Heredie Delvalle — CLS, alarm limits, dual scale, - * format/precision wiring + * format/precision wiring; refactored onto RTScaledWidgetRepresentation */ -public class TankRepresentation extends RegionBaseRepresentation +@SuppressWarnings("nls") +public class TankRepresentation extends RTScaledWidgetRepresentation { - private final DirtyFlag dirty_look = new DirtyFlag(); - private final UntypedWidgetPropertyListener lookListener = this::lookChanged; - private final UntypedWidgetPropertyListener valueListener = this::valueChanged; - private final UntypedWidgetPropertyListener limitsListener = this::limitsChanged; - private final WidgetPropertyListener orientationChangedListener = this::orientationChanged; - - private volatile RTTank tank; - @Override - public Pane createJFXNode() throws Exception + protected boolean isHorizontal() { - tank = new RTTank(); - tank.setUpdateThrottle(Preferences.image_update_delay, TimeUnit.MILLISECONDS); - return new Pane(tank); + return model_widget.propHorizontal().getValue(); } @Override - protected void registerListeners() + protected void registerLookListeners() { - super.registerListeners(); model_widget.propWidth().addUntypedPropertyListener(lookListener); model_widget.propHeight().addUntypedPropertyListener(lookListener); model_widget.propFont().addUntypedPropertyListener(lookListener); @@ -62,36 +42,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 +64,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(final double width, final double height) { - super.updateChanges(); - if (dirty_look.checkAndClear()) - { - double width = model_widget.propWidth().getValue(); - double height = model_widget.propHeight().getValue(); - if (model_widget.propHorizontal().getValue()) - { - tank.getTransforms().setAll(new Translate(width, 0), - new Rotate(90, 0, 0)); - was_transformed = true; - tank.setWidth(height); - tank.setHeight(width); - } - else - { - if (was_transformed) - tank.getTransforms().clear(); - tank.setWidth(width); - tank.setHeight(height); - } - jfx_node.setPrefSize(width, height); - tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); - tank.setBackground(JFXUtil.convert(model_widget.propBackground().getValue())); - tank.setForeground(JFXUtil.convert(model_widget.propForeground().getValue())); - tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); - tank.setEmptyColor(JFXUtil.convert(model_widget.propEmptyColor().getValue())); - tank.setScaleVisible(model_widget.propScaleVisible().getValue()); - tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); - tank.setPerpendicularTickLabels(model_widget.propPerpendicularTickLabels().getValue()); - tank.setLogScale(model_widget.propLogScale().getValue()); - tank.setLabelFormat(model_widget.propFormat().getValue(), - model_widget.propPrecision().getValue()); - tank.setAlarmColors( - JFXUtil.convert(model_widget.propMinorAlarmColor().getValue()), - JFXUtil.convert(model_widget.propMajorAlarmColor().getValue())); - tank.setRightScaleVisible(model_widget.propOppositeScaleVisible().getValue()); - tank.setBorderWidth(model_widget.propBorderWidth().getValue()); - } + tank.setFont(JFXUtil.convert(model_widget.propFont().getValue())); + tank.setBackground(JFXUtil.convert(model_widget.propBackground().getValue())); + tank.setForeground(JFXUtil.convert(model_widget.propForeground().getValue())); + tank.setFillColor(JFXUtil.convert(model_widget.propFillColor().getValue())); + tank.setEmptyColor(JFXUtil.convert(model_widget.propEmptyColor().getValue())); + tank.setScaleVisible(model_widget.propScaleVisible().getValue()); + tank.setShowMinorTicks(model_widget.propShowMinorTicks().getValue()); + tank.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 670417280f..85992f324a 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -16,6 +16,7 @@ import java.awt.image.BufferedImage; import java.text.NumberFormat; import java.util.Objects; +import java.util.regex.Pattern; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -93,6 +94,17 @@ public class RTTank extends Canvas /** Border width in pixels around the tank body; 0 = no border (default) */ private volatile int border_width = 0; + /** Extra inset from canvas edge to plot body on all four sides. + * 0 = default tank look; positive values give a recessed bar-track + * appearance suitable for progress-bar use without a visible scale. */ + private volatile int inner_padding = 0; + + /** When {@code true}, the empty (unfilled) portion of the tank is painted + * with a solid colour instead of the default left-to-center gradient. + * Use for progress-bar style tracks where the JFX original has a flat + * background and only the filled bar carries a visual gradient. */ + private volatile boolean flat_track = false; + /** Current value, i.e. fill level */ private volatile double value = 5.0; @@ -214,6 +226,45 @@ public void setBorderWidth(final int width) requestUpdate(); } + /** Extra inset from all four canvas edges to the plot body. + * Set to 0 (default) for the standard tank look. Set to a positive + * value (e.g. 4) when using RTTank as a progress-bar track so that + * the filled area is visually inset from the widget boundary, + * matching the margin of the JFX {@code ProgressBar} CSS style. + * @param pixels padding in pixels; clamped to [0, 20] */ + public void setInnerPadding(final int pixels) + { + inner_padding = Math.max(0, Math.min(20, pixels)); + need_layout.set(true); + requestUpdate(); + } + + /** Control whether the unfilled track region is painted with a solid colour + * ({@code true}) or the default left-to-center gradient ({@code false}). + * Enable for progress-bar use to match the flat track background of the + * JFX {@code ProgressBar}; disable (default) to keep the Tank look. + * @param flat {@code true} = solid track, {@code false} = gradient track */ + public void setFlatTrack(final boolean flat) + { + flat_track = flat; + requestUpdate(); + } + + /** Show or hide tick label text on the scale while keeping tick marks visible. + *

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

    Layout and repaint are triggered automatically by the axis when + * the value actually changes; no-op when unchanged. + * @param visible {@code true} (default) = labels shown; {@code false} = ticks only */ + public void setScaleLabelsVisible(final boolean visible) + { + // Each axis fires its own requestLayout()/requestRefresh() via plot_part_listener + // when the state changes, propagating need_layout and requestUpdate automatically. + scale.setScaleLabelsVisible(visible); + right_scale.setScaleLabelsVisible(visible); + } + /** @param color Background color */ public void setBackground(final javafx.scene.paint.Color color) { @@ -335,12 +386,12 @@ private static NumberFormat significantDigitsFormat(final int prec) @Override public StringBuffer format(final double v, final StringBuffer buf, final java.text.FieldPosition pos) { - return buf.append(String.format(java.util.Locale.ROOT, pattern, v)); + return buf.append(normaliseExponent(String.format(java.util.Locale.ROOT, pattern, v))); } @Override public StringBuffer format(final long v, final StringBuffer buf, final java.text.FieldPosition pos) { - return buf.append(String.format(java.util.Locale.ROOT, pattern, (double) v)); + return buf.append(normaliseExponent(String.format(java.util.Locale.ROOT, pattern, (double) v))); } @Override public Number parse(final String s, final java.text.ParsePosition pos) @@ -350,6 +401,28 @@ public Number parse(final String s, final java.text.ParsePosition pos) }; } + /** Pre-compiled pattern for stripping the sign and leading zeros from a + * {@code %g} exponent string such as {@code "-01"} or {@code "+02"}. + */ + private static final Pattern EXP_LEADING_ZEROS = Pattern.compile("^[+-]?0*"); + + /** Normalise a {@code %g}-formatted string to match Phoebus axis convention: + * uppercase {@code E}, no leading zeros on the exponent, no {@code +} sign. + * Examples: {@code "1.0e-01"} → {@code "1.0E-1"}, + * {@code "2.5e+02"} → {@code "2.5E2"}. + */ + private static String normaliseExponent(final String s) + { + final int e = s.indexOf('e'); + if (e < 0) + return s; // decimal notation — no exponent to fix + final String mantissa = s.substring(0, e); + final String raw = s.substring(e + 1); // e.g. "-01", "+02" + final boolean neg = raw.startsWith("-"); + final String digits = EXP_LEADING_ZEROS.matcher(raw).replaceFirst(""); + return mantissa + "E" + (neg ? "-" : "") + (digits.isEmpty() ? "0" : digits); + } + /** Set alarm and warning limit values to display as horizontal lines on the tank. * Pass {@link Double#NaN} for any limit that should not be shown. * @param lolo LOLO (major alarm) lower limit @@ -519,14 +592,13 @@ private void computeLayout(final Graphics2D gc, final Rectangle bounds) ends[1] = Math.max(ends[1], r_ends[1]); } - // Inset = ceil(border_width/2) keeps the outer stroke edge inside the canvas. - // On sides with a scale the label area provides ample margin so inset=0. - // When there is no border, inset=1 is the original clip guard. + // Inset: half border-width rounded up, plus inner_padding on all sides. 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; @@ -582,8 +654,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)); @@ -610,25 +684,7 @@ protected Image updateImageBuffer() gc.setStroke(new BasicStroke(1f)); } - // Draw alarm / warning limit lines over the tank body - final double lim_lolo = limit_lolo; - final double lim_lo = limit_lo; - final double lim_hi = limit_hi; - final double lim_hihi = limit_hihi; - if (normal && (!Double.isNaN(lim_lolo) || !Double.isNaN(lim_lo) || - !Double.isNaN(lim_hi) || !Double.isNaN(lim_hihi))) - { - if (limits_from_pv) - gc.setStroke(new BasicStroke(2f)); - else - gc.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, - BasicStroke.JOIN_MITER, 10f, new float[]{6f, 4f}, 0f)); - drawLimitLineAt(gc, plot_bounds, min, max, lim_lolo, limit_major_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_lo, limit_minor_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_hi, limit_minor_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_hihi, limit_major_color); - gc.setStroke(new BasicStroke(1f)); - } + drawAlarmLimits(gc, plot_bounds, normal, min, max); gc.dispose(); @@ -636,6 +692,29 @@ protected Image updateImageBuffer() return SwingFXUtils.toFXImage(image, null); } + /** Draw alarm/warning limit lines over the tank body, if any limits are set */ + private void drawAlarmLimits(final Graphics2D gc, final Rectangle plot_bounds, + final boolean normal, final double min, final double max) + { + final double lim_lolo = limit_lolo; + final double lim_lo = limit_lo; + final double lim_hi = limit_hi; + final double lim_hihi = limit_hihi; + if (!normal || (Double.isNaN(lim_lolo) && Double.isNaN(lim_lo) && + Double.isNaN(lim_hi) && Double.isNaN(lim_hihi))) + return; + if (limits_from_pv) + gc.setStroke(new BasicStroke(2f)); + else + gc.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, 10f, new float[]{6f, 4f}, 0f)); + drawLimitLineAt(gc, plot_bounds, min, max, lim_lolo, limit_major_color); + drawLimitLineAt(gc, plot_bounds, min, max, lim_lo, limit_minor_color); + drawLimitLineAt(gc, plot_bounds, min, max, lim_hi, limit_minor_color); + drawLimitLineAt(gc, plot_bounds, min, max, lim_hihi, limit_major_color); + gc.setStroke(new BasicStroke(1f)); + } + /** Request a complete redraw of the plot */ final public void requestUpdate() { diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java index 109ad205c5..0f248419ca 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/YAxisImpl.java @@ -60,6 +60,12 @@ public class YAxisImpl> extends NumericAxis impl /** Show on right side? */ private volatile boolean is_right = false; + /** When {@code false}, tick marks are drawn but tick label text is suppressed. + * Use this to stack multiple scaled widgets with a single labelled scale: + * only the first widget shows text; the rest show aligned tick marks only. + *

    Read on the Java2D render thread; written from the JFX thread — must be volatile. */ + private volatile boolean show_labels = true; + /** When {@code true}, rotated tick labels always use the 'up' direction * (bottom-to-top) regardless of {@link #is_right}. This keeps the text * orientation of a right-side scale identical to a left-side scale. @@ -161,6 +167,21 @@ public void setForceTextUp(final boolean force) force_text_up = force; } + /** Show or hide tick label text while keeping tick marks visible. + * When {@code false}, the axis is rendered as ticks-only, consuming + * less horizontal space. Tick spacing is unchanged, so stacked + * progress bars or tanks can share a single labelled scale. + * @param show {@code true} (default) = labels visible; {@code false} = ticks only + */ + public void setScaleLabelsVisible(final boolean show) + { + if (show_labels == show) + return; + show_labels = show; + requestLayout(); + requestRefresh(); + } + /** Add trace to axis * @param trace {@link Trace} * @throws IllegalArgumentException if trace already on axis @@ -209,6 +230,10 @@ public int getDesiredPixelSize(final Rectangle region, final Graphics2D gc) if (! isVisible()) return 0; + // Ticks-only mode: only tick marks, no label text — just TICK_LENGTH wide. + if (!show_labels) + return TICK_LENGTH; + this.region = region; gc.setFont(label_font); @@ -334,6 +359,10 @@ public int[] getPixelGaps(final Graphics2D gc) if (! isVisible()) return super.getPixelGaps(gc); + // Ticks-only mode: no labels extend past the tick positions. + if (!show_labels) + return new int[] { 0, 0 }; + gc.setFont(scale_font); final FontMetrics metrics = gc.getFontMetrics(); @@ -413,7 +442,7 @@ public void paint(final Graphics2D gc, final Rectangle plot_bounds) } gc.setStroke(old_width); - if (showLabel[mi]) + if (showLabel[mi] && show_labels) drawTickLabel(gc, y, tick.getLabel(), false); } @@ -428,8 +457,11 @@ public void paint(final Graphics2D gc, final Rectangle plot_bounds) gc.setColor(old_fg); gc.setBackground(old_bg); - gc.setFont(label_font); - paintLabels(gc); + if (show_labels) + { + gc.setFont(label_font); + paintLabels(gc); + } } protected void paintLabels(final Graphics2D gc)