From 04d83ab74049f11b89d0e3049f28c42c07988a80 Mon Sep 17 00:00:00 2001 From: Rebecca Williams Date: Mon, 26 Oct 2026 10:31:16 +0000 Subject: [PATCH 1/2] Allow corner radius of action button to be configurable Add a new widget preference to an action button that allows users to change the corner radius. Also update the representation so that alarm borders follow the corner radius correctly. --- .../display/builder/model/Messages.java | 1 + .../model/widgets/ActionButtonWidget.java | 18 +++++++++++++++-- .../display/builder/model/messages.properties | 1 + .../builder/model/messages_fr.properties | 1 + .../widgets/ActionButtonRepresentation.java | 20 ++++++++++++++++++- 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/Messages.java index 73b88c918b..dd314e6af5 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 @@ -201,6 +201,7 @@ public class Messages WidgetProperties_ConfirmMessage, WidgetProperties_Connected, WidgetProperties_CornerHeight, + WidgetProperties_CornerRadius, WidgetProperties_CornerWidth, WidgetProperties_CursorCrosshair, WidgetProperties_CursorInfo, diff --git a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java index be5b2cad25..c9d436f867 100644 --- a/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java +++ b/app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/ActionButtonWidget.java @@ -25,12 +25,15 @@ import java.util.List; import org.csstudio.display.builder.model.MacroizedWidgetProperty; +import org.csstudio.display.builder.model.Messages; import org.csstudio.display.builder.model.Version; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.WidgetCategory; import org.csstudio.display.builder.model.WidgetConfigurator; import org.csstudio.display.builder.model.WidgetDescriptor; import org.csstudio.display.builder.model.WidgetProperty; +import org.csstudio.display.builder.model.WidgetPropertyCategory; +import org.csstudio.display.builder.model.WidgetPropertyDescriptor; import org.csstudio.display.builder.model.persist.ModelReader; import org.csstudio.display.builder.model.persist.NamedWidgetFonts; import org.csstudio.display.builder.model.persist.WidgetFontService; @@ -223,6 +226,9 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) return new ActionButtonConfigurator(persisted_version); } + public static final WidgetPropertyDescriptor propCornerRadius = + CommonWidgetProperties.newIntegerPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "corner_radius", Messages.WidgetProperties_CornerRadius); + // Has pv_name and pv_writable, but no pv_value which would make it a WritablePVWidget private volatile WidgetProperty enabled; private volatile WidgetProperty text; @@ -237,6 +243,7 @@ public WidgetConfigurator getConfigurator(final Version persisted_version) private volatile WidgetProperty confirm_dialog; private volatile WidgetProperty confirm_message; private volatile WidgetProperty password; + private volatile WidgetProperty corner_radius; /** Constructor */ public ActionButtonWidget() @@ -271,6 +278,7 @@ protected void defineProperties(final List> properties) properties.add(confirm_dialog = propConfirmDialog.createProperty(this, false)); properties.add(confirm_message = propConfirmMessage.createProperty(this, "Are your sure you want to do this?")); properties.add(password = propPassword.createProperty(this, "")); + properties.add(corner_radius = propCornerRadius.createProperty(this, 3)); } @Override @@ -310,13 +318,13 @@ public WidgetProperty propTransparent() { return transparent; } - + /** @return 'horizontal_alignment' property */ public WidgetProperty propHorizontalAlignment() { return horizontal_alignment; } - + /** @return 'vertical_alignment' property */ public WidgetProperty propVerticalAlignment() { @@ -358,4 +366,10 @@ public WidgetProperty propPassword() { return password; } + + /** @return 'corner_radius' property */ + public WidgetProperty propCornerRadius() + { + return corner_radius; + } } 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 5142ee46a9..46e08dc6b1 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 @@ -187,6 +187,7 @@ WidgetProperties_ConfirmDialog=Confirmation Dialog WidgetProperties_ConfirmMessage=Confirmation Message WidgetProperties_Connected=Connected WidgetProperties_CornerHeight=Corner Height +WidgetProperties_CornerRadius=Corner Radius WidgetProperties_CornerWidth=Corner Width WidgetProperties_CursorCrosshair=Cursor Crosshair WidgetProperties_CursorInfo=Cursor Info 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 64fb765e8f..7c0287b859 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 @@ -187,6 +187,7 @@ WidgetProperties_ConfirmDialog=Dialogue de confirmation WidgetProperties_ConfirmMessage=Message de confirmation WidgetProperties_Connected=Connecté WidgetProperties_CornerHeight=Hauteur du coin +WidgetProperties_CornerRadius=Rayon du coin WidgetProperties_CornerWidth=Largeur du coin WidgetProperties_CursorCrosshair=Curseur en croix WidgetProperties_CursorInfo=Info du curseur diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java index 669684f2c2..29b14748ad 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/ActionButtonRepresentation.java @@ -73,7 +73,7 @@ public class ActionButtonRepresentation extends RegionBaseRepresentation property, final Object old_va */ private void representationChanged(final WidgetProperty property, final Object old_value, final Object new_value) { updateColors(); + updateCorners(); dirty_representation.mark(); toolkit.scheduleUpdate(this); } @@ -398,6 +403,19 @@ private void updateColors() { style += " -fx-text-base-color: " + fg_css + ";"; } + /** + * Add the corner radii to the style + */ + private void updateCorners() { + style += " -fx-background-radius: "+ model_widget.propCornerRadius().getValue()+" ;"; + } + + @Override + public int[] getBorderRadii() + { + return new int[] {model_widget.propCornerRadius().getValue(),model_widget.propCornerRadius().getValue()}; + } + @Override public void updateChanges() { super.updateChanges(); From e6c93b75b7ebdda24ee6e60759a896f15f4effa5 Mon Sep 17 00:00:00 2001 From: Rebecca Williams Date: Tue, 27 Oct 2026 11:43:34 +0000 Subject: [PATCH 2/2] Add unit test for ActionButtonWidget --- .../model/widgets/ActionButtonWidgetTest.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ActionButtonWidgetTest.java diff --git a/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ActionButtonWidgetTest.java b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ActionButtonWidgetTest.java new file mode 100644 index 0000000000..427a192956 --- /dev/null +++ b/app/display/model/src/test/java/org/csstudio/display/builder/model/widgets/ActionButtonWidgetTest.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.csstudio.display.builder.model.widgets; + +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.persist.ModelReader; +import org.csstudio.display.builder.model.persist.ModelWriter; +import org.csstudio.display.builder.model.persist.NamedWidgetFonts; +import org.csstudio.display.builder.model.persist.WidgetFontService; +import org.csstudio.display.builder.model.properties.HorizontalAlignment; +import org.csstudio.display.builder.model.properties.NamedWidgetFont; +import org.csstudio.display.builder.model.properties.RotationStep; +import org.csstudio.display.builder.model.properties.VerticalAlignment; +import org.junit.jupiter.api.Test; +import org.phoebus.ui.color.NamedWidgetColor; +import org.phoebus.ui.color.NamedWidgetColors; +import org.phoebus.ui.color.WidgetColorService; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + *Unit tests for {@link ActionButtonWidget}. + */ +@SuppressWarnings("nls") +public class ActionButtonWidgetTest +{ + /** + * Verify ActionButton default property values. + */ + @Test + public void testActionButtonWidgetDefaults() + { + final ActionButtonWidget button = new ActionButtonWidget(); + + assertThat(button.propText().getValue(), equalTo("No actions")); + assertThat(button.propFont().getValue(), equalTo(WidgetFontService.get(NamedWidgetFonts.DEFAULT))); + assertThat(button.propForegroundColor().getValue(), equalTo(WidgetColorService.getColor(NamedWidgetColors.TEXT))); + assertThat(button.propBackgroundColor().getValue(), equalTo(WidgetColorService.getColor(NamedWidgetColors.BUTTON_BACKGROUND))); + assertThat(button.propTransparent().getValue(), equalTo(false)); + assertThat(button.propHorizontalAlignment().getValue(), equalTo(HorizontalAlignment.CENTER)); + assertThat(button.propVerticalAlignment().getValue(), equalTo(VerticalAlignment.MIDDLE)); + assertThat(button.propRotationStep().getValue(), equalTo(RotationStep.NONE)); + assertThat(button.propEnabled().getValue(), equalTo(true)); + assertThat(button.runtimePropPVWritable().getValue(), equalTo(true)); + assertThat(button.propConfirmDialog().getValue(), equalTo(false)); + assertThat(button.propConfirmMessage().getValue(), equalTo("Are your sure you want to do this?")); + assertThat(button.propPassword().getValue(), equalTo("")); + assertThat(button.propCornerRadius().getValue(), equalTo(3)); + } + + /** + * Test that changed (i.e. non-default) properties persist + * in the XML write/read. + * + * @throws Exception if cannot be converted + */ + @Test + public void testNonDefaultPropertiesInXml() throws Exception + { + // Test (non-default) values + String text = "Testing"; + NamedWidgetColor backgroundColor = WidgetColorService.getColor(NamedWidgetColors.WRITE_BACKGROUND); + NamedWidgetColor foregroundColor = WidgetColorService.getColor(NamedWidgetColors.ACTIVE_TEXT); + NamedWidgetFont font = WidgetFontService.get(NamedWidgetFonts.DEFAULT_BOLD); + HorizontalAlignment hAlign = HorizontalAlignment.LEFT; + VerticalAlignment vAlign = VerticalAlignment.TOP; + RotationStep rotation = RotationStep.NINETY; + String confirmMessage = "Is this OK?"; + String password = "pw"; + int radii = 10; + + ActionButtonWidget button = new ActionButtonWidget(); + + button.propText().setValue(text); + button.propFont().setValue(font); + button.propForegroundColor().setValue(foregroundColor); + button.propBackgroundColor().setValue(backgroundColor); + button.propTransparent().setValue(true); + button.propHorizontalAlignment().setValue(hAlign); + button.propVerticalAlignment().setValue(vAlign); + button.propRotationStep().setValue(rotation); + button.propEnabled().setValue(false); + button.propConfirmDialog().setValue(true); + button.propConfirmMessage().setValue(confirmMessage); + button.propPassword().setValue(password); + button.propCornerRadius().setValue(radii); + + final String xml = ModelWriter.getXML(List.of(button)); + + final DisplayModel model = ModelReader.parseXML(xml); + final List widgets = model.getChildren(); + assertThat(widgets.size(), equalTo(1)); + assertThat(widgets.getFirst(), instanceOf(ActionButtonWidget.class)); + button = (ActionButtonWidget) widgets.getFirst(); + + assertThat(button.propText().getValue(), equalTo(text)); + assertThat(button.propFont().getValue(), equalTo(font)); + assertThat(button.propForegroundColor().getValue(), equalTo(foregroundColor)); + assertThat(button.propBackgroundColor().getValue(), equalTo(backgroundColor)); + assertThat(button.propTransparent().getValue(), equalTo(true)); + assertThat(button.propHorizontalAlignment().getValue(), equalTo(hAlign)); + assertThat(button.propVerticalAlignment().getValue(), equalTo(vAlign)); + assertThat(button.propRotationStep().getValue(), equalTo(rotation)); + assertThat(button.propEnabled().getValue(), equalTo(false)); + assertThat(button.propConfirmDialog().getValue(), equalTo(true)); + assertThat(button.propConfirmMessage().getValue(), equalTo(confirmMessage)); + assertThat(button.propPassword().getValue(), equalTo(password)); + assertThat(button.propCornerRadius().getValue(), equalTo(radii)); + } + + /** + * Test that the properties with default values do not get saved + * to the XML. + * + * @throws Exception if cannot be converted + */ + @Test + public void testOptionalPropertiesInXml() throws Exception + { + ActionButtonWidget button = new ActionButtonWidget(); + final String xml = ModelWriter.getXML(List.of(button)); + + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + assertThat(xml, not(containsString(""))); + } +}