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. *
Handles all logic that depends only on the {@link ScaledPVWidget} contract: + *
Subclasses provide: + *
Neither this class nor its subclasses have any knowledge of each other's
+ * widget type: {@code TankRepresentation} and {@code ProgressBarRepresentation}
+ * are fully independent.
+ *
+ * @param 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 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 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 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)