From adb3aff3460583df56d60edc7f51e760fa258a2e Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 20 May 2026 14:35:32 +0200 Subject: [PATCH 1/7] Transform ItemConfigDialog UI to fxml --- .../applications/alarm/ui/Messages.java | 6 + .../DateTimePicker.java | 12 +- .../alarm/ui/config/ItemConfigDialog.java | 72 ++++ .../ui/config/ItemConfigDialogController.java | 382 ++++++++++++++++++ .../datetimepicker => config}/LICENSE.txt | 0 .../alarm/ui/tree/AlarmTreeView.java | 2 +- .../ui/tree/ConfigureComponentAction.java | 1 + .../alarm/ui/tree/ItemConfigDialog.java | 370 ----------------- .../alarm/ui/config/ItemConfigDialog.fxml | 174 ++++++++ .../applications/alarm/ui/messages.properties | 28 ++ 10 files changed, 672 insertions(+), 375 deletions(-) rename app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/{tree/datetimepicker => config}/DateTimePicker.java (89%) create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java rename app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/{tree/datetimepicker => config}/LICENSE.txt (100%) delete mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java create mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java index 18455f7dfa..4636c4c988 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java @@ -15,12 +15,18 @@ public class Messages public static String acknowledgeFailed; public static String addComponentFailed; + public static String configure; + public static String delayTooltip0; + public static String delayTooltip1; + public static String delayTooltip2; public static String disableAlarmFailed; public static String disabled; public static String disabledUntil; public static String enableAlarmFailed; public static String moveItemFailed; public static String partlyDisabled; + public static String promptTitle; + public static String promptContent; public static String removeComponentFailed; public static String renameItemFailed; public static String timer; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/datetimepicker/DateTimePicker.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java similarity index 89% rename from app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/datetimepicker/DateTimePicker.java rename to app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java index 33af059b3d..e5498b1af5 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/datetimepicker/DateTimePicker.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java @@ -1,4 +1,8 @@ -package org.phoebus.applications.alarm.ui.tree.datetimepicker; +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.ui.config; import java.time.LocalDate; import java.time.LocalDateTime; @@ -14,15 +18,15 @@ /** * A DateTimePicker with configurable datetime format where both date and time can be changed * via the text field and the date can additionally be changed via the JavaFX default date picker. - * Modified from https://github.com/edvin/tornadofx-controls/blob/master/src/main/java/tornadofx/control/DateTimePicker.java + * Modified from DateTimePicker */ @SuppressWarnings("unused") public class DateTimePicker extends DatePicker { private static final String DefaultFormat = "yyyy-MM-dd HH:mm"; private DateTimeFormatter formatter; - private ObjectProperty dateTimeValue = new SimpleObjectProperty<>(null); - private ObjectProperty format = new SimpleObjectProperty() { + private final ObjectProperty dateTimeValue = new SimpleObjectProperty<>(null); + private final ObjectProperty format = new SimpleObjectProperty<>() { @Override public void set(String newValue) { super.set(newValue); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java new file mode 100644 index 0000000000..b00f2359f4 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ +package org.phoebus.applications.alarm.ui.config; + +import javafx.event.ActionEvent; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.ScrollPane; +import javafx.stage.Modality; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.framework.nls.NLS; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Dialog for editing {@link AlarmTreeItem}. + * + *

Layout is defined in {@code ItemConfigDialog.fxml}. + * Runtime wiring (model data, bindings, event handlers) is handled by + * {@link ItemConfigDialogController}. + * + *

When pressing "OK", the dialog sends the updated configuration. + */ +@SuppressWarnings("nls") +public class ItemConfigDialog extends Dialog { + + public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { + super(); + // Allow multiple instances + initModality(Modality.NONE); + setTitle(Messages.configure + " " + item.getName()); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + try { + final FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(NLS.getMessages(Messages.class)); + fxmlLoader.setLocation(this.getClass().getResource("ItemConfigDialog.fxml")); + fxmlLoader.setControllerFactory(clazz -> { + try { + return clazz.getConstructor(AlarmClient.class, + AlarmTreeItem.class).newInstance(model, item); + } catch (Exception e) { + Logger.getLogger(ItemConfigDialog.class.getName()).log(Level.SEVERE, "Failed to construct ItemConfigDialogController", e); + } + return null; + }); + + // Load returns the root node (ScrollPane) declared in the FXML + final ScrollPane root = fxmlLoader.load(); + + // ── OK-button validation filter ─────────────────────────────────────── + final Button ok = (Button) getDialogPane().lookupButton(ButtonType.OK); + ok.addEventFilter(ActionEvent.ACTION, + event -> ((ItemConfigDialogController)fxmlLoader.getController()).validateAndStore()); + + getDialogPane().setContent(root); + + } catch (Exception ex) { + throw new RuntimeException("Failed to load ItemConfigDialog.fxml", ex); + } + + setResizable(true); + + setResultConverter(button -> button == ButtonType.OK); + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java new file mode 100644 index 0000000000..85f15881fd --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ +package org.phoebus.applications.alarm.ui.config; + +import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.DateCell; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import javafx.util.StringConverter; +import org.phoebus.applications.alarm.AlarmSystem; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.client.AlarmClientNode; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.applications.alarm.ui.tree.TitleDetailDelayTable; +import org.phoebus.applications.alarm.ui.tree.TitleDetailTable; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.util.time.SecondsParser; +import org.phoebus.util.time.TimeParser; + +import java.text.MessageFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAmount; + +/** + * FXML controller for ItemConfigDialog.fxml. + */ +@SuppressWarnings("nls") +public class ItemConfigDialogController { + + // ── FXML-injected fields ────────────────────────────────────────────────── + + @SuppressWarnings("unused") + @FXML + private ScrollPane scroll; + @SuppressWarnings("unused") + @FXML + private javafx.scene.layout.GridPane layout; + + // Path row (always visible) + @SuppressWarnings("unused") + @FXML + private TextField path; + + // Leaf-only rows + @SuppressWarnings("unused") + @FXML + private Label descriptionLabel; + @SuppressWarnings("unused") + @FXML + private TextField description; + + @SuppressWarnings("unused") + @FXML + private Label behaviorLabel; + @SuppressWarnings("unused") + @FXML + private HBox behaviorBox; + @SuppressWarnings("unused") + @FXML + private CheckBox enabled; + @SuppressWarnings("unused") + @FXML + private CheckBox latching; + @SuppressWarnings("unused") + @FXML + private CheckBox annunciating; + + @SuppressWarnings("unused") + @FXML + private Label disableUntilLabel; + @SuppressWarnings("unused") + @FXML + private ComboBox relativeDate; + + @SuppressWarnings("unused") + @FXML + private Label delayLabel; + @SuppressWarnings("unused") + @FXML + private Spinner delay; + @SuppressWarnings("unused") + @FXML + private Label countLabel; + @SuppressWarnings("unused") + @FXML + private Spinner count; + + @SuppressWarnings("unused") + @FXML + private Label filterLabel; + @SuppressWarnings("unused") + @FXML + private TextField filter; + + // Shared table placeholders + @SuppressWarnings("unused") + @FXML + private StackPane guidancePlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane displaysPlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane commandsPlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane actionsPlaceholder; + @SuppressWarnings("unused") + @FXML + private DateTimePicker enabledDatePicker; + @SuppressWarnings("unused") + @FXML + private HBox untilBox; + + private TitleDetailTable guidance; + private TitleDetailTable displays; + private TitleDetailTable commands; + private TitleDetailDelayTable actions; + + private final SimpleBooleanProperty itemEnabled = new SimpleBooleanProperty(); + + private final AlarmClient alarmClient; + private final AlarmTreeItem alarmTreeItem; + + public ItemConfigDialogController(AlarmClient alarmClient, AlarmTreeItem alarmTreeItem) { + this.alarmClient = alarmClient; + this.alarmTreeItem = alarmTreeItem; + } + + @SuppressWarnings("unused") + @FXML + public void initialize() { + + path.setText(alarmTreeItem.getPathName()); + + // ── Leaf-only section ───────────────────────────────────────────────── + if (alarmTreeItem instanceof AlarmClientLeaf leaf) { + itemEnabled.setValue(leaf.isEnabled()); + + // Description + description.setText(leaf.getDescription()); + + // Behavior checkboxes + enabled.setSelected(leaf.isEnabled()); + latching.setSelected(leaf.isLatching()); + annunciating.setSelected(leaf.isAnnunciating()); + + latching.disableProperty().bind(itemEnabled.not()); + annunciating.disableProperty().bind(itemEnabled.not()); + + itemEnabled.addListener((obs, o, n) -> leaf.setEnabled(n)); + + enabledDatePicker.disableProperty().bind(itemEnabled.not()); + + // Day-cell factory – disable past dates + enabledDatePicker.setDayCellFactory(picker -> new DateCell() { + @Override + public void updateItem(LocalDate date, boolean empty) { + super.updateItem(date, empty); + setDisable(empty || date.isBefore(LocalDate.now())); + } + }); + + // ENTER key handler on the date picker's editor + enabledDatePicker.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER) { + try { + TextFormatter tf = enabledDatePicker.getEditor().getTextFormatter(); + @SuppressWarnings("unchecked") + StringConverter conv = + (StringConverter) tf.getValueConverter(); + conv.fromString(enabledDatePicker.getEditor().getText()); + enabledDatePicker.getEditor().commitValue(); + } catch (DateTimeParseException ex) { + keyEvent.consume(); + } + } + }); + + // Relative-date combo + relativeDate.getItems().addAll(AlarmSystem.shelving_options); + relativeDate.setDisable(!leaf.isEnabled()); + relativeDate.disableProperty().bind(itemEnabled.not()); + + // Relative-date action handler (must be a field so it can be removed temporarily) + final EventHandler relativeEventHandler = e -> { + enabled.setSelected(false); + enabledDatePicker.getEditor().clear(); + }; + relativeDate.setOnAction(relativeEventHandler); + + // Date-picker action handler + enabledDatePicker.setOnAction(e -> { + if (enabledDatePicker.getDateTimeValue() != null) { + relativeDate.setOnAction(null); + enabled.setSelected(false); + enabledDatePicker.getEditor().commitValue(); + relativeDate.getSelectionModel().clearSelection(); + relativeDate.setValue(null); + relativeDate.setOnAction(relativeEventHandler); + } + }); + + // Enabled checkbox action + enabled.setOnAction(e -> { + itemEnabled.setValue(enabled.isSelected()); + if (enabled.isSelected()) { + relativeDate.getSelectionModel().clearSelection(); + relativeDate.setValue(null); + enabledDatePicker.getEditor().clear(); + enabledDatePicker.setValue(null); + } + if (!enabled.isSelected()) { + leaf.setEnabled(false); + } + }); + + // Delay spinner + delay.setValueFactory( + new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, leaf.getDelay())); + delay.setEditable(true); + delay.setPrefWidth(80); + delay.disableProperty().bind(itemEnabled.not()); + final Tooltip delayTt = new Tooltip(); + delayTt.setShowDuration(Duration.seconds(30)); + delayTt.setOnShowing(event -> { + final int seconds = leaf.getDelay(); + final String detail; + if (seconds <= 0) + detail = Messages.delayTooltip1; + else { + detail = MessageFormat.format(Messages.delayTooltip2, seconds, SecondsParser.formatSeconds(seconds)); + } + delayTt.setText(MessageFormat.format(Messages.delayTooltip0, detail)); + }); + delay.setTooltip(delayTt); + + // Count spinner + count.setValueFactory( + new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, leaf.getCount())); + count.setEditable(true); + count.setPrefWidth(80); + count.disableProperty().bind(itemEnabled.not()); + + // Filter field + filter.setText(leaf.getFilter()); + filter.disableProperty().bind(itemEnabled.not()); + + // Initial focus + Platform.runLater(() -> description.requestFocus()); + + } else { + // Hide all leaf-only rows when item is not a leaf + setLeafSectionVisible(false); + } + + // ── Shared tables (guidance, displays, commands, actions) ───────────── + guidance = new TitleDetailTable(alarmTreeItem.getGuidance()); + guidance.setPrefHeight(100); + guidancePlaceholder.getChildren().setAll(guidance); + + displays = new TitleDetailTable(alarmTreeItem.getDisplays()); + displays.setPrefHeight(100); + displaysPlaceholder.getChildren().setAll(displays); + + commands = new TitleDetailTable(alarmTreeItem.getCommands()); + commands.setPrefHeight(100); + commandsPlaceholder.getChildren().setAll(commands); + + actions = new TitleDetailDelayTable(alarmTreeItem.getActions()); + actions.setPrefHeight(100); + actionsPlaceholder.getChildren().setAll(actions); + + // ── Scroll-pane width listener ──────────────────────────────────────── + scroll.widthProperty().addListener((p, old, width) -> + layout.setPrefWidth(Math.max(width.doubleValue() - 40, 450))); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + /** + * Show or hide every control that belongs to the leaf-only section. + */ + private void setLeafSectionVisible(boolean visible) { + for (javafx.scene.Node n : new javafx.scene.Node[]{ + descriptionLabel, description, + behaviorLabel, behaviorBox, + disableUntilLabel, untilBox, + delayLabel, delay, + countLabel, count, + filterLabel, filter}) { + n.setVisible(visible); + n.setManaged(visible); + } + } + + // ── Validation / store ──────────────────────────────────────────────────── + + /** + * Validates input and sends the configuration off to the message broker. + */ + public void validateAndStore() { + final AlarmTreeItem config; + + if (alarmTreeItem instanceof AlarmClientLeaf) { + final AlarmClientLeaf pv = new AlarmClientLeaf(null, alarmTreeItem.getName()); + + boolean validEnableDate; + { + final LocalDateTime selectedEnableDate = enabledDatePicker.getDateTimeValue(); + final String relativeEnableDate = relativeDate.getValue(); + + if (selectedEnableDate != null) { + validEnableDate = pv.setEnabledDate(selectedEnableDate); + } else if (relativeEnableDate != null) { + final TemporalAmount amount = TimeParser.parseTemporalAmount(relativeEnableDate); + final LocalDateTime updateDate = LocalDateTime.now().plus(amount); + validEnableDate = pv.setEnabledDate(updateDate); + } else { + pv.setEnabled(itemEnabled.get()); + validEnableDate = true; + } + } + + if (!validEnableDate) { + Alert prompt = new Alert(Alert.AlertType.INFORMATION); + prompt.setTitle(Messages.promptTitle); + prompt.setHeaderText(Messages.promptTitle); + prompt.setContentText(Messages.promptContent); + DialogHelper.positionDialog(prompt, enabledDatePicker, 0, 0); + prompt.showAndWait(); + return; + } + + pv.setDescription(description.getText().trim()); + pv.setLatching(latching.isSelected()); + pv.setAnnunciating(annunciating.isSelected()); + pv.setDelay(delay.getValue()); + pv.setCount(count.getValue()); + // TODO Check filter expression + pv.setFilter(filter.getText().trim()); + + config = pv; + } else { + config = new AlarmClientNode(null, alarmTreeItem.getName()); + } + + config.setGuidance(guidance.getItems()); + config.setDisplays(displays.getItems()); + config.setCommands(commands.getItems()); + config.setActions(actions.getItems()); + + try { + alarmClient.sendItemConfigurationUpdate(alarmTreeItem.getPathName(), config); + } catch (Exception ex) { + ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); + } + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/datetimepicker/LICENSE.txt b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LICENSE.txt similarity index 100% rename from app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/datetimepicker/LICENSE.txt rename to app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LICENSE.txt diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index 179b971ce0..2d4bfa7184 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -19,7 +19,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.stream.Collectors; @@ -34,6 +33,7 @@ import org.phoebus.applications.alarm.model.BasicState; import org.phoebus.applications.alarm.ui.AlarmContextMenuHelper; import org.phoebus.applications.alarm.ui.AlarmUI; +import org.phoebus.applications.alarm.ui.config.ItemConfigDialog; import org.phoebus.framework.selection.Selection; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuService; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ConfigureComponentAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ConfigureComponentAction.java index 282641c809..febedc90cd 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ConfigureComponentAction.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ConfigureComponentAction.java @@ -10,6 +10,7 @@ import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.config.ItemConfigDialog; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java deleted file mode 100644 index 6a8edaafc3..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ItemConfigDialog.java +++ /dev/null @@ -1,370 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2018-2021 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.phoebus.applications.alarm.ui.tree; - -import javafx.application.Platform; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Pos; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.DateCell; -import javafx.scene.control.Dialog; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Spinner; -import javafx.scene.control.TextField; -import javafx.scene.control.TextFormatter; -import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.stage.Modality; -import javafx.util.Duration; -import javafx.util.StringConverter; -import org.phoebus.applications.alarm.AlarmSystem; -import org.phoebus.applications.alarm.client.AlarmClient; -import org.phoebus.applications.alarm.client.AlarmClientLeaf; -import org.phoebus.applications.alarm.client.AlarmClientNode; -import org.phoebus.applications.alarm.model.AlarmTreeItem; -import org.phoebus.applications.alarm.ui.tree.datetimepicker.DateTimePicker; -import org.phoebus.ui.dialog.DialogHelper; -import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; -import org.phoebus.util.time.SecondsParser; -import org.phoebus.util.time.TimeParser; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeParseException; -import java.time.temporal.TemporalAmount; - - -/** - * Dialog for editing {@link AlarmTreeItem} - * - *

When pressing "OK", dialog sends updated - * configuration. - */ -@SuppressWarnings("nls") -class ItemConfigDialog extends Dialog { - private TextField description; - private CheckBox enabled, latching, annunciating; - private DateTimePicker enabled_date_picker; - private Spinner delay, count; - private TextField filter; - private ComboBox relative_date; - private final TitleDetailTable guidance, displays, commands; - private final TitleDetailDelayTable actions; - - private final SimpleBooleanProperty itemEnabled = new SimpleBooleanProperty(); - - public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { - // Allow multiple instances - initModality(Modality.NONE); - setTitle("Configure " + item.getName()); - - final GridPane layout = new GridPane(); - layout.setHgap(5); - layout.setVgap(5); - - // First fixed-size column for labels - // Second column grows - final ColumnConstraints col1 = new ColumnConstraints(190); - final ColumnConstraints col2 = new ColumnConstraints(); - col2.setHgrow(Priority.ALWAYS); - layout.getColumnConstraints().setAll(col1, col2); - - int row = 0; - - // Show item path, allow copying it out. - // Can't edit; that's done via rename or move actions. - layout.add(new Label("Path:"), 0, row); - final TextField path = new TextField(item.getPathName()); - path.setEditable(false); - layout.add(path, 1, row++); - - if (item instanceof AlarmClientLeaf leaf) { - itemEnabled.setValue(((AlarmClientLeaf) item).isEnabled()); - - layout.add(new Label("Description:"), 0, row); - description = new TextField(leaf.getDescription()); - description.setTooltip(new Tooltip("Alarm description, also used for annunciation")); - layout.add(description, 1, row++); - GridPane.setHgrow(description, Priority.ALWAYS); - - layout.add(new Label("Behavior:"), 0, row); - enabled = new CheckBox("Enabled"); - enabled.setTooltip(new Tooltip("Enable alarms? See also 'Enabling Filter'")); - enabled.setSelected(leaf.isEnabled()); - - itemEnabled.addListener((obs, o, n) -> { - ((AlarmClientLeaf) item).setEnabled(n); - }); - - enabled.setOnAction(e -> { - itemEnabled.setValue(enabled.isSelected()); - if (enabled.isSelected()) { - relative_date.getSelectionModel().clearSelection(); - relative_date.setValue(null); - enabled_date_picker.getEditor().clear(); - enabled_date_picker.setValue(null); - } - // User has unchecked checkbox to disable alarm -> disable indefinitely. - if (!enabled.isSelected()) { - ((AlarmClientLeaf) item).setEnabled(false); - } - }); - - latching = new CheckBox("Latch"); - latching.setTooltip(new Tooltip("Latch alarm until acknowledged?")); - latching.setSelected(leaf.isLatching()); - latching.disableProperty().bind(itemEnabled.not()); - - annunciating = new CheckBox("Annunciate"); - annunciating.setTooltip(new Tooltip("Request audible alarm annunciation (using the description)?")); - annunciating.setSelected(leaf.isAnnunciating()); - annunciating.disableProperty().bind(itemEnabled.not()); - - layout.add(new HBox(10, enabled, latching, annunciating), 1, row++); - - layout.add(new Label("Disable until:"), 0, row); - enabled_date_picker = new DateTimePicker(); - enabled_date_picker.setTooltip(new Tooltip("Select a date until which the alarm should be disabled")); - enabled_date_picker.setDateTimeValue(leaf.getEnabledDate()); - enabled_date_picker.setPrefSize(280, 25); - enabled_date_picker.setDisable(!leaf.isEnabled()); - enabled_date_picker.disableProperty().bind(itemEnabled.not()); - - - enabled_date_picker.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> { - if (keyEvent.getCode() == KeyCode.ENTER) { - try { - // Test that the input is well-formed (if the input - // isn't well-formed, valueConverter.fromString() - // throws a DateTimeParseException): - TextFormatter textFormatter = enabled_date_picker.getEditor().getTextFormatter(); - StringConverter valueConverter = textFormatter.getValueConverter(); - LocalDate dateTime = (LocalDate) valueConverter.fromString(enabled_date_picker.getEditor().getText()); - - enabled_date_picker.getEditor().commitValue(); - } - catch (DateTimeParseException dateTimeParseException) { - // The input was not well-formed. Prevent further - // processing by consuming the key-event: - keyEvent.consume(); - } - } - }); - - relative_date = new ComboBox<>(); - relative_date.setTooltip(new Tooltip("Select a predefined duration for disabling the alarm")); - relative_date.getItems().addAll(AlarmSystem.shelving_options); - relative_date.setPrefSize(200, 25); - relative_date.setDisable(!leaf.isEnabled()); - relative_date.disableProperty().bind(itemEnabled.not()); - - final EventHandler relative_event_handler = (ActionEvent e) -> - { - enabled.setSelected(false); - enabled_date_picker.getEditor().clear(); - }; - - relative_date.setOnAction(relative_event_handler); - - // setOnAction for relative date must be set to null as to not trigger event when setting value - enabled_date_picker.setOnAction((ActionEvent e) -> - { - if (enabled_date_picker.getDateTimeValue() != null) { - relative_date.setOnAction(null); - enabled.setSelected(false); - enabled_date_picker.getEditor().commitValue(); - relative_date.getSelectionModel().clearSelection(); - relative_date.setValue(null); - relative_date.setOnAction(relative_event_handler); - } - }); - - // Configure date picker to disable selection of all dates in the past. - enabled_date_picker.setDayCellFactory(picker -> new DateCell() { - public void updateItem(LocalDate date, boolean empty) { - super.updateItem(date, empty); - LocalDate today = LocalDate.now(); - setDisable(empty || date.isBefore(today)); - } - }); - - final HBox until_box = new HBox(10, enabled_date_picker, relative_date); - until_box.setAlignment(Pos.CENTER); - HBox.setHgrow(relative_date, Priority.ALWAYS); - layout.add(until_box, 1, row++); - - layout.add(new Label("Alarm Delay [seconds]:"), 0, row); - delay = new Spinner<>(0, Integer.MAX_VALUE, leaf.getDelay()); - final Tooltip delay_tt = new Tooltip(); - delay_tt.setShowDuration(Duration.seconds(30)); - delay_tt.setOnShowing(event -> - { - final int seconds = leaf.getDelay(); - final String detail; - if (seconds <= 0) - detail = "With the current delay of 0 seconds, alarms trigger immediately"; - else { - final String hhmmss = SecondsParser.formatSeconds(seconds); - detail = "With the current delay of " + seconds + " seconds, alarms trigger after " + hhmmss + " hours:minutes:seconds"; - } - delay_tt.setText("Alarms are indicated when they persist for at least this long.\n" + detail); - }); - delay.setTooltip(delay_tt); - delay.setEditable(true); - delay.setPrefWidth(80); - delay.disableProperty().bind(itemEnabled.not()); - layout.add(delay, 1, row++); - - layout.add(new Label("Alarm Count [within delay]:"), 0, row); - count = new Spinner<>(0, Integer.MAX_VALUE, leaf.getCount()); - count.setTooltip(new Tooltip("Alarms are indicated when they occur this often within the delay")); - count.setEditable(true); - count.setPrefWidth(80); - count.disableProperty().bind(itemEnabled.not()); - layout.add(count, 1, row++); - - layout.add(new Label("Enabling Filter:"), 0, row); - filter = new TextField(leaf.getFilter()); - filter.setTooltip(new Tooltip("Optional expression for enabling the alarm")); - filter.disableProperty().bind(itemEnabled.not()); - layout.add(filter, 1, row++); - - // Initial focus on description - Platform.runLater(() -> description.requestFocus()); - } - - // Layout has two column - // The PV-specific items above use two columns. - // If there's no PV, - // the following items use one column or span two columns. - // There must be _something_ in the second column with Hgrow=Always - // to cause the layout to fill its parent area. - // 'dummy' is used for that. - - // Guidance: - layout.add(new Label("Guidance:"), 0, row++, 2, 1); - guidance = new TitleDetailTable(item.getGuidance()); - guidance.setPrefHeight(100); - layout.add(guidance, 0, row++, 2, 1); - - // Displays: - layout.add(new Label("Displays:"), 0, row++, 2, 1); - displays = new TitleDetailTable(item.getDisplays()); - displays.setPrefHeight(100); - layout.add(displays, 0, row++, 2, 1); - - // Commands: - layout.add(new Label("Commands:"), 0, row++, 2, 1); - commands = new TitleDetailTable(item.getCommands()); - commands.setPrefHeight(100); - layout.add(commands, 0, row++, 2, 1); - - // Automated Actions: - layout.add(new Label("Automated Actions:"), 0, row++, 2, 1); - actions = new TitleDetailDelayTable(item.getActions()); - actions.setPrefHeight(100); - layout.add(actions, 0, row++, 2, 1); - - // Dialog is quite high; allow scroll - final ScrollPane scroll = new ScrollPane(layout); - - // Scroll pane stops the content from resizing, - // so tell content to use the widths of the scroll pane - // minus 40 to provide space for the scroll bar, and suggest minimum width - scroll.widthProperty().addListener((p, old, width) -> layout.setPrefWidth(Math.max(width.doubleValue() - 40, 450))); - - getDialogPane().setContent(scroll); - setResizable(true); - - getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - - final Button ok = (Button) getDialogPane().lookupButton(ButtonType.OK); - ok.addEventFilter(ActionEvent.ACTION, event -> - validateAndStore(model, item, event)); - - setResultConverter(button -> button == ButtonType.OK); - } - - /** - * Send requested configuration - * - * @param model {@link AlarmClient} - * @param item Original item - * @param event Button click event, consumed if save action fails (e.g. Kafka not reachable) - */ - private void validateAndStore(final AlarmClient model, final AlarmTreeItem item, ActionEvent event) { - final AlarmTreeItem config; - - if (item instanceof AlarmClientLeaf) { - final AlarmClientLeaf pv = new AlarmClientLeaf(null, item.getName()); - - boolean validEnableDate; - { - final LocalDateTime selected_enable_date = enabled_date_picker.getDateTimeValue(); - final String relative_enable_date = relative_date.getValue(); - - if ((selected_enable_date != null)) { - validEnableDate = pv.setEnabledDate(selected_enable_date); - } else if (relative_enable_date != null) { - final TemporalAmount amount = TimeParser.parseTemporalAmount(relative_enable_date); - final LocalDateTime update_date = LocalDateTime.now().plus(amount); - validEnableDate = pv.setEnabledDate(update_date); - } else { - pv.setEnabled(itemEnabled.get()); - validEnableDate = true; - } - } - - if (!validEnableDate) { - Alert prompt = new Alert(Alert.AlertType.INFORMATION); - prompt.setTitle("'Disable until' is set to a point in time in the past"); - prompt.setHeaderText("'Disable until' is set to a point in time in the past"); - prompt.setContentText("The option 'disable until' must be set to a point in time in the future."); - DialogHelper.positionDialog(prompt, enabled_date_picker, 0, 0); - prompt.showAndWait(); - - event.consume(); - return; - } - - pv.setDescription(description.getText().trim()); - pv.setLatching(latching.isSelected()); - pv.setAnnunciating(annunciating.isSelected()); - pv.setDelay(delay.getValue()); - pv.setCount(count.getValue()); - // TODO Check filter expression - pv.setFilter(filter.getText().trim()); - - config = pv; - } else - config = new AlarmClientNode(null, item.getName()); - config.setGuidance(guidance.getItems()); - config.setDisplays(displays.getItems()); - config.setCommands(commands.getItems()); - config.setActions(actions.getItems()); - - try { - model.sendItemConfigurationUpdate(item.getPathName(), config); - } catch (Exception ex) { - ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); - event.consume(); - } - } -} \ No newline at end of file diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml new file mode 100644 index 0000000000..961da34a32 --- /dev/null +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties index b2ab1ecd20..3bf6e7d718 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties @@ -17,15 +17,43 @@ # # +alarmCount=Alarm Count [within delay]: +alarmCountTooltip=Alarms are indicated when they occur this often within the delay +alarmDelay=Alarm Delay [seconds]: +annunciate=Annunciate +annunciateTooltip=Request audible alarm annunciation (using the description)? +automatedActions=Automated Actions: +commands=Commands: +configure=Configure +dateTimePickerTooltip=Select a date until which the alarm should be disabled +delayTooltip0=Alarms are indicated when they persist for at least this long.\n{0} +delayTooltip1=With the current delay of 0 seconds, alarms trigger immediately +delayTooltip2=With the current delay of {0}seconds, alarms trigger after {1} hours:minutes:seconds +disableUntil=Disable until: +displays=Displays: +enabled=Enabled +enablingFilter=Enabling Filter: +enablingFilterTooltip=Optional expression for enabling the alarm error=Error acknowledgeFailed=Failed to acknowledge alarm(s) addComponentFailed=Failed to add component +behavior=Behavior: +behaviorTooltip=Enable alarms? See also 'Enabling Filter' +description=Description: +descriptionTooltip=Alarm description, also used for annunciation disableAlarmFailed=Failed to disable alarm disabled=Disabled disabledUntil=Disabled until enableAlarmFailed=Failed to enable alarm +guidance=Guidance: +latch=Latch +latchTooltip=Latch alarm until acknowledged? moveItemFailed=Failed to move item partlyDisabled=Partly disabled +path=Path: +promptTitle='Disable until' is set to a point in time in the past +promptContent=The option 'disable until' must be set to a point in time in the future. +relativeDateTooltip=Select a predefined duration for disabling the alarm removeComponentFailed=Failed to remove component renameItemFailed=Failed to rename item timer=Timer From c38d2e4fecbdbffc280acf122abe1ab325fb0b8d Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 21 May 2026 13:03:46 +0200 Subject: [PATCH 2/7] Externalize code to enable/disable alarms --- .../applications/alarm/ui/Messages.java | 6 + .../alarm/ui/tree/ComponentActionHelper.java | 102 ++++++++++++++++ .../alarm/ui/tree/EnableComponentAction.java | 115 +++--------------- .../applications/alarm/ui/messages.properties | 6 + 4 files changed, 133 insertions(+), 96 deletions(-) create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java index 4636c4c988..00818d3ae1 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java @@ -20,9 +20,15 @@ public class Messages public static String delayTooltip1; public static String delayTooltip2; public static String disableAlarmFailed; + public static String disableAlarms; public static String disabled; public static String disabledUntil; public static String enableAlarmFailed; + public static String enableAlarms; + public static String headerAlreadyDisabled; + public static String headerAlreadyEnabled; + public static String headerConfirmDisable; + public static String headerConfirmEnable; public static String moveItemFailed; public static String partlyDisabled; public static String promptTitle; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java new file mode 100644 index 0000000000..1f17709bde --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.ui.tree; + +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.framework.jobs.JobManager; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * Code externalized from {@link EnableComponentAction}. + */ +public class ComponentActionHelper { + + /** + * Updates a component or PV node to enable or disable alarms + * @param node The visual component relative to which a confirmation dialog is positioned. + * @param model {@link AlarmClient} dispatching producer messages. + * @param items {@link List} of items subject for update, e.g. selected by user. + * @param enable If true, enable alarms on selected nodes, otherwise disable. + */ + public static void updateEnablement(final Node node, final AlarmClient model, final List> items, boolean enable){ + final List pvs = new ArrayList<>(); + for (AlarmTreeItem item : items) { + findAffectedPVs(item, pvs, enable); + } + + // If this affects exactly one PV, just do it. + // Otherwise ask for confirmation + if (pvs.size() != 1) { + final Alert dialog = new Alert(Alert.AlertType.CONFIRMATION); + dialog.setTitle(enable ? Messages.enableAlarms : Messages.disableAlarms); + if (pvs.isEmpty()) { + dialog.setHeaderText( + enable + ? Messages.headerAlreadyEnabled + : Messages.headerAlreadyDisabled); + } + else { + dialog.setHeaderText(MessageFormat.format( + enable + ? Messages.headerConfirmEnable + : Messages.headerConfirmDisable, + pvs.size())); + } + DialogHelper.positionDialog(dialog, node, -100, -50); + if (dialog.showAndWait().get() != ButtonType.OK) { + return; + } + } + + JobManager.schedule(enable ? Messages.enableAlarms : Messages.disableAlarms, monitor -> + { + for (AlarmClientLeaf pv : pvs) + { + final AlarmClientLeaf copy = pv.createDetachedCopy(); + if (copy.setEnabled(enable)) + try { + model.sendItemConfigurationUpdate(pv.getPathName(), copy); + } catch (Exception e) { + ExceptionDetailsErrorDialog.openError(Messages.error, + copy.isEnabled() ? Messages.enableAlarmFailed : Messages.disableAlarmFailed, + e); + throw e; + } + } + }); + } + + /** @param item Node where to start recursing for PVs that would be affected + * @param pvs Array to update with PVs that would be affected + */ + private static void findAffectedPVs(final AlarmTreeItem item, final List pvs, boolean enable) + { + if (item instanceof AlarmClientLeaf) + { + final AlarmClientLeaf pv = (AlarmClientLeaf) item; + // If pv has different enablement, and wasn't already added + // because selection contains its parent as well as the PV itself... + if (pv.isEnabled() != enable && !pvs.contains(pv)) { + pvs.add(pv); + } + } + else { + for (AlarmTreeItem sub : item.getChildren()) { + findAffectedPVs(sub, pvs, enable); + } + } + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java index ea64bdc4c0..625d3513e0 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/EnableComponentAction.java @@ -7,119 +7,42 @@ *******************************************************************************/ package org.phoebus.applications.alarm.ui.tree; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.List; - +import javafx.scene.Node; +import javafx.scene.control.MenuItem; import org.phoebus.applications.alarm.client.AlarmClient; -import org.phoebus.applications.alarm.client.AlarmClientLeaf; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.ui.AlarmUI; import org.phoebus.applications.alarm.ui.Messages; -import org.phoebus.framework.jobs.JobManager; -import org.phoebus.ui.dialog.DialogHelper; -import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.ImageCache; -import javafx.scene.Node; -import javafx.scene.control.Alert; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.ButtonType; -import javafx.scene.control.MenuItem; +import java.util.List; -/** Action that enables items in the alarm tree configuration - * @author Kay Kasemir +/** + * Action that enables items in the alarm tree configuration + * + * @author Kay Kasemir */ @SuppressWarnings("nls") -class EnableComponentAction extends MenuItem -{ - /** @param node Node to position dialog - * @param model {@link AlarmClient} - * @param items Items to enable +class EnableComponentAction extends MenuItem { + /** + * @param node Node to position dialog + * @param model {@link AlarmClient} + * @param items Items to enable */ - public EnableComponentAction(final Node node, final AlarmClient model, final List> items) - { - if (doEnable()) - { - setText("Enable Alarms"); + public EnableComponentAction(final Node node, final AlarmClient model, final List> items) { + if (doEnable()) { + setText(Messages.enableAlarms); setGraphic(ImageCache.getImageView(AlarmUI.class, "/icons/enabled.png")); - } - else - { - setText("Disable Alarms"); + } else { + setText(Messages.disableAlarms); setGraphic(ImageCache.getImageView(AlarmUI.class, "/icons/disabled.png")); } - setOnAction(event -> - { - final List pvs = new ArrayList<>(); - for (AlarmTreeItem item : items) - findAffectedPVs(item, pvs); - - // If this affects exactly one PV, just do it. - // Otherwise ask for confirmation - if (pvs.size() != 1) - { - final Alert dialog = new Alert(AlertType.CONFIRMATION); - dialog.setTitle(getText()); - if (pvs.size() == 0) - dialog.setHeaderText( - doEnable() - ? "All PVs in the selected section are already enabled" - : "All PVs in the selected section are already disabled"); - else - dialog.setHeaderText(MessageFormat.format( - doEnable() - ? "Enable all PVs in the selected section of the alarm hierarchy?\n" + - "This would enable {0} PVs" - : "Disable all PVs in the selected section of the alarm hierarchy?\n" + - "This would disable {0} PVs", - pvs.size())); - DialogHelper.positionDialog(dialog, node, -100, -50); - if (dialog.showAndWait().get() != ButtonType.OK) - return; - } - - JobManager.schedule(getText(), monitor -> - { - for (AlarmClientLeaf pv : pvs) - { - final AlarmClientLeaf copy = pv.createDetachedCopy(); - if (copy.setEnabled(doEnable())) - try { - model.sendItemConfigurationUpdate(pv.getPathName(), copy); - } catch (Exception e) { - ExceptionDetailsErrorDialog.openError(Messages.error, - copy.isEnabled() ? Messages.enableAlarmFailed : Messages.disableAlarmFailed, - e); - throw e; - } - } - }); - }); + setOnAction(event -> ComponentActionHelper.updateEnablement(node, model, items, doEnable())); } // Implementation can actually disable or enable. Which one is it going to be? - protected boolean doEnable() - { + protected boolean doEnable() { return true; } - - /** @param item Node where to start recursing for PVs that would be affected - * @param pvs Array to update with PVs that would be affected - */ - private void findAffectedPVs(final AlarmTreeItem item, final List pvs) - { - if (item instanceof AlarmClientLeaf) - { - final AlarmClientLeaf pv = (AlarmClientLeaf) item; - // If pv has different enablement, and wasn't already added - // because selection contains its parent as well as the PV itself... - if (pv.isEnabled() != doEnable() && !pvs.contains(pv)) - pvs.add(pv); - } - else - for (AlarmTreeItem sub : item.getChildren()) - findAffectedPVs(sub, pvs); - } } diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties index 3bf6e7d718..eb7e5a456d 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties @@ -42,10 +42,16 @@ behaviorTooltip=Enable alarms? See also 'Enabling Filter' description=Description: descriptionTooltip=Alarm description, also used for annunciation disableAlarmFailed=Failed to disable alarm +disableAlarms=Disable Alarms disabled=Disabled disabledUntil=Disabled until enableAlarmFailed=Failed to enable alarm +enableAlarms=Enable Alarms guidance=Guidance: +headerAlreadyDisabled=All PVs in the selected section are already disabled +headerAlreadyEnabled=All PVs in the selected section are already enabled +headerConfirmDisable=Disable all PVs in the selected section of the alarm hierarchy?\nThis would disable {0} PVs. +headerConfirmEnable=Enable all PVs in the selected section of the alarm hierarchy?\nThis would enable {0} PVs. latch=Latch latchTooltip=Latch alarm until acknowledged? moveItemFailed=Failed to move item From a6e4e1403886deec6c18695cff338a601aad9a4a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 27 May 2026 12:38:16 +0200 Subject: [PATCH 3/7] Config dialog for alarm component node (i.e. not leaf node) --- .../applications/alarm/ui/Messages.java | 1 + .../ComponentConfigDialogController.java | 219 ++++++++++ .../ui/config/ConfigDialogController.java | 229 +++++++++++ .../alarm/ui/config/DateTimePicker.java | 2 +- .../alarm/ui/config/ItemConfigDialog.java | 21 +- .../ui/config/ItemConfigDialogController.java | 382 ------------------ .../ui/config/LeafConfigDialogController.java | 198 +++++++++ .../alarm/ui/tree/ComponentActionHelper.java | 31 +- .../ui/config/ComponentConfigDialog.fxml | 108 +++++ .../alarm/ui/config/ItemConfigDialog.fxml | 174 -------- .../alarm/ui/config/LeafConfigDialog.fxml | 149 +++++++ .../applications/alarm/ui/messages.properties | 23 +- .../ComponentConfigDialogControllerTest.java | 46 +++ .../ui/tree/ComponentActionHelperTest.java | 74 ++++ 14 files changed, 1064 insertions(+), 593 deletions(-) create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java delete mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java create mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml delete mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml create mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/LeafConfigDialog.fxml create mode 100644 app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogControllerTest.java create mode 100644 app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelperTest.java diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java index 00818d3ae1..df38bf1bae 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java @@ -28,6 +28,7 @@ public class Messages public static String headerAlreadyDisabled; public static String headerAlreadyEnabled; public static String headerConfirmDisable; + public static String headerConfirmDisableWithEnableDate; public static String headerConfirmEnable; public static String moveItemFailed; public static String partlyDisabled; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java new file mode 100644 index 0000000000..26012c28b7 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ +package org.phoebus.applications.alarm.ui.config; + +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.applications.alarm.ui.tree.ComponentActionHelper; +import org.phoebus.framework.jobs.JobManager; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; + +import java.text.MessageFormat; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * FXML controller for LeafConfigDialog.fxml. + */ +@SuppressWarnings("nls") +public class ComponentConfigDialogController extends ConfigDialogController { + + + // ── FXML-injected fields ────────────────────────────────────────────────── + + @SuppressWarnings("unused") + @FXML + private ScrollPane scroll; + @SuppressWarnings("unused") + @FXML + private javafx.scene.layout.GridPane layout; + + // Path row (always visible) + @SuppressWarnings("unused") + @FXML + private TextField path; + + // Leaf-only rows + @SuppressWarnings("unused") + @FXML + private Label descriptionLabel; + @SuppressWarnings("unused") + @FXML + private TextField description; + + @SuppressWarnings("unused") + @FXML + private Label behaviorLabel; + @SuppressWarnings("unused") + @FXML + private HBox behaviorBox; + @SuppressWarnings("unused") + @FXML + private CheckBox enabled; + + @SuppressWarnings("unused") + @FXML + private Label disableUntilLabel; + @SuppressWarnings("unused") + @FXML + private ComboBox relativeDate; + + @SuppressWarnings("unused") + @FXML + private DateTimePicker enabledDatePicker; + + @SuppressWarnings("unused") + @FXML + private Label partlyDisabledLabel; + + private List alarmClientLeaves; + + public ComponentConfigDialogController(AlarmClient alarmClient, AlarmTreeItem alarmTreeItem) { + super(alarmClient, alarmTreeItem); + } + + @SuppressWarnings("unused") + @FXML + public void initialize() { + + super.initialize(); + + alarmClientLeaves = new ArrayList<>(); + List disabled = new ArrayList<>(); + List withEnableDate = new ArrayList<>(); + // Check subtree for disabled PVs and PVs with non-null enable date + findAffectedPVs(alarmTreeItem, alarmClientLeaves, disabled, withEnableDate); + + if(disabled.isEmpty()) { + enabled.setSelected(true); + } + else if (alarmClientLeaves.size() != disabled.size()) { + partlyDisabledLabel.setVisible(true); + enabled.setSelected(false); + } + + if (!withEnableDate.isEmpty()) { + relativeDate.setDisable(true); + enabledDatePicker.setDisable(true); + } + } + + /** + * Validates input and sends the configuration off to the message broker. + * + */ + public void validateAndStore() { + + // First check if user has specified a valid enable date + LocalDateTime enableDate; + try { + enableDate = determineEnableDate(); + } catch (Exception e) { + Logger.getLogger(LeafConfigDialogController.class.getName()) + .log(Level.WARNING, "Invalid enable date specified", e); + return; + } + + // Next store guidance, displays... + alarmTreeItem.setGuidance(guidance.getItems()); + alarmTreeItem.setDisplays(displays.getItems()); + alarmTreeItem.setCommands(commands.getItems()); + alarmTreeItem.setActions(actions.getItems()); + + try { + alarmClient.sendItemConfigurationUpdate(alarmTreeItem.getPathName(), alarmTreeItem); + } catch (Exception ex) { + ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); + return; + } + + // Lastly update enable or - if non-null - set enable date. + if (enableDate != null) { + updateEnablement(enableDate); + } else { + ComponentActionHelper.updateEnablement(scroll, alarmClient, List.of(alarmTreeItem), itemEnabledProperty.get()); + } + } + + /** + * Updates a component to disable a hierarchy of PVs with an enable date. + * + * @param enableDate The {@link LocalDateTime} to set on all leaf nodes specified in items . + */ + private void updateEnablement(LocalDateTime enableDate) { + if (alarmClientLeaves.isEmpty()) { + return; + } + if (alarmClientLeaves.size() > 1) { + final Alert dialog = new Alert(Alert.AlertType.CONFIRMATION); + dialog.setTitle(Messages.disableAlarms); + dialog.setHeaderText(MessageFormat.format(Messages.headerConfirmDisableWithEnableDate, enableDate, alarmClientLeaves.size())); + + DialogHelper.positionDialog(dialog, scroll, -50, -25); + if (dialog.showAndWait().get() != ButtonType.OK) { + return; + } + } + + JobManager.schedule(Messages.disableAlarms, monitor -> + { + for (AlarmClientLeaf pv : alarmClientLeaves) { + final AlarmClientLeaf copy = pv.createDetachedCopy(); + if (copy.setEnabledDate(enableDate)) { + try { + alarmClient.sendItemConfigurationUpdate(pv.getPathName(), copy); + } catch (Exception e) { + ExceptionDetailsErrorDialog.openError(Messages.error, + Messages.disableAlarmFailed, + e); + throw e; + } + } + } + }); + } + + /** + * Recursively counts alarm tree items in a subtree to find total number, number of disabled, and + * number of disabled with enable date. + * + * @param item Root item + * @param total {@link AtomicInteger} that will hold the total number of leaf nodes + * @param disabled {@link AtomicInteger} that will hold the number of disabled leaf nodes (with or without enable date) + * @param withEnableDate {@link AtomicInteger} that will hold the number of leaf nodes with non-null enable date + * + */ + public static void findAffectedPVs(final AlarmTreeItem item, final List total, final List disabled, final List withEnableDate) { + if (item instanceof AlarmClientLeaf) { + final AlarmClientLeaf pv = (AlarmClientLeaf) item; + total.add(pv); + if (!pv.isEnabled()) { + disabled.add(pv); + if (pv.getEnabledDate() != null) { + withEnableDate.add(pv); + } + } + } else { + for (AlarmTreeItem sub : item.getChildren()) { + findAffectedPVs(sub, total, disabled, withEnableDate); + } + } + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java new file mode 100644 index 0000000000..8f1a11b740 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.ui.config; + +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.DateCell; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.StackPane; +import javafx.util.StringConverter; +import org.phoebus.applications.alarm.AlarmSystem; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.applications.alarm.ui.tree.TitleDetailDelayTable; +import org.phoebus.applications.alarm.ui.tree.TitleDetailTable; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.util.time.TimeParser; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAmount; + +public abstract class ConfigDialogController { + + @SuppressWarnings("unused") + @FXML + private ScrollPane scroll; + @SuppressWarnings("unused") + @FXML + private GridPane layout; + + + @SuppressWarnings("unused") + @FXML + private TextField path; + + @SuppressWarnings("unused") + @FXML + protected CheckBox enabled; + + @SuppressWarnings("unused") + @FXML + protected ComboBox relativeDate; + + + @SuppressWarnings("unused") + @FXML + protected DateTimePicker enabledDatePicker; + + // Shared table placeholders + @SuppressWarnings("unused") + @FXML + private StackPane guidancePlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane displaysPlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane commandsPlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane actionsPlaceholder; + + protected TitleDetailTable guidance; + protected TitleDetailTable displays; + protected TitleDetailTable commands; + protected TitleDetailDelayTable actions; + + protected final AlarmClient alarmClient; + protected final AlarmTreeItem alarmTreeItem; + + protected final SimpleBooleanProperty itemEnabledProperty = new SimpleBooleanProperty(); + protected final SimpleStringProperty relativeDateProperty = new SimpleStringProperty(null); + protected final SimpleObjectProperty enableDateProperty = + new SimpleObjectProperty<>(null); + + public ConfigDialogController(AlarmClient alarmClient, AlarmTreeItem alarmTreeItem) { + this.alarmClient = alarmClient; + this.alarmTreeItem = alarmTreeItem; + } + + @FXML + public void initialize() { + + path.setText(alarmTreeItem.getPathName()); + + // ── Shared tables (guidance, displays, commands, actions) ───────────── + guidance = new TitleDetailTable(alarmTreeItem.getGuidance()); + guidance.setPrefHeight(100); + guidancePlaceholder.getChildren().setAll(guidance); + + displays = new TitleDetailTable(alarmTreeItem.getDisplays()); + displays.setPrefHeight(100); + displaysPlaceholder.getChildren().setAll(displays); + + commands = new TitleDetailTable(alarmTreeItem.getCommands()); + commands.setPrefHeight(100); + commandsPlaceholder.getChildren().setAll(commands); + + actions = new TitleDetailDelayTable(alarmTreeItem.getActions()); + actions.setPrefHeight(100); + actionsPlaceholder.getChildren().setAll(actions); + + relativeDate.valueProperty().bindBidirectional(relativeDateProperty); + enabledDatePicker.dateTimeValueProperty().bindBidirectional(enableDateProperty); + + enabled.setOnAction(e -> { + itemEnabledProperty.setValue(enabled.isSelected()); + relativeDateProperty.set(null); + enableDateProperty.set(null); + }); + + enableDateProperty.addListener((observable, oldValue, newValue) -> { + enabled.setSelected(newValue == null && relativeDateProperty.isNull().get()); + if (newValue != null) { + relativeDateProperty.setValue(null); + } + }); + + relativeDateProperty.addListener((observable, oldValue, newValue) -> { + enabled.setSelected(newValue == null && enableDateProperty.isNull().get()); + if (newValue != null) { + enableDateProperty.setValue(null); + } + }); + + // Day-cell factory – disable past dates + enabledDatePicker.setDayCellFactory(picker -> new DateCell() { + @Override + public void updateItem(LocalDate date, boolean empty) { + super.updateItem(date, empty); + setDisable(empty || date.isBefore(LocalDate.now())); + } + }); + + // ENTER key handler on the date picker's editor + enabledDatePicker.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER) { + try { + TextFormatter tf = enabledDatePicker.getEditor().getTextFormatter(); + @SuppressWarnings("unchecked") + StringConverter conv = + (StringConverter) tf.getValueConverter(); + conv.fromString(enabledDatePicker.getEditor().getText()); + enableDateProperty.set(conv.fromString(enabledDatePicker.getEditor().getText())); + enabledDatePicker.getEditor().commitValue(); + } catch (DateTimeParseException ex) { + keyEvent.consume(); + } + } + }); + + // Make sure first element in shelving options is null + // so user can "deselect" a relative date. + String[] shelvingOptions = new String[AlarmSystem.shelving_options.length + 1]; + System.arraycopy(AlarmSystem.shelving_options, 0, shelvingOptions, 1, AlarmSystem.shelving_options.length); + relativeDate.getItems().addAll(shelvingOptions); + + // ── Scroll-pane width listener ──────────────────────────────────────── + scroll.widthProperty().addListener((p, old, width) -> + layout.setPrefWidth(Math.max(width.doubleValue() - 40, 450))); + } + + /** + * Attempts to determine a {@link LocalDateTime} based on the user input. + * + * @return A non-null {@link LocalDateTime} if user has specified a valid date/time, or null if + * there is no user input from which to determine a date/time. + * @throws IllegalArgumentException if user has entered an invalid date/time. + */ + protected LocalDateTime determineEnableDate() { + + if (enableDateProperty.isNotNull().get()) { + if (isEnableDateValid(enableDateProperty.get())) { + return enableDateProperty.get(); + } else { + showInvalidEnableDateDialog(); + throw new IllegalArgumentException("Enable date invalid"); + } + } else if (relativeDateProperty.isNotNull().get()) { + final TemporalAmount amount = + TimeParser.parseTemporalAmount(relativeDateProperty.get()); + final LocalDateTime updateDate = LocalDateTime.now().plus(amount); + if (isEnableDateValid(updateDate)) { + return updateDate; + } else { + showInvalidEnableDateDialog(); + throw new IllegalArgumentException("Enable date invalid"); + } + } + return null; + } + + /** + * @param enableDate A non-null {@link LocalDateTime} + * @return true if the specified date/time is considered valid, e.g. in the future. + */ + private boolean isEnableDateValid(LocalDateTime enableDate) { + return !enableDate.isBefore(LocalDateTime.now()) && !enableDate.isEqual(LocalDateTime.now()); + } + + /** + * Shows a dialog indicate that the user-specified date is invalid, e.g. a date/time not in the future. + */ + private void showInvalidEnableDateDialog() { + Alert prompt = new Alert(Alert.AlertType.INFORMATION); + prompt.setTitle(Messages.promptTitle); + prompt.setHeaderText(Messages.promptTitle); + prompt.setContentText(Messages.promptContent); + DialogHelper.positionDialog(prompt, enabledDatePicker, 0, 0); + prompt.showAndWait(); + } + + public abstract void validateAndStore(); +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java index e5498b1af5..cfa68442b5 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java @@ -96,7 +96,7 @@ public void setDateTimeValue(LocalDateTime dateTimeValue) { this.dateTimeValue.set(dateTimeValue); } - private ObjectProperty dateTimeValueProperty() { + protected ObjectProperty dateTimeValueProperty() { return dateTimeValue; } diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java index b00f2359f4..dfec2bbc99 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java @@ -11,6 +11,7 @@ import javafx.scene.control.ScrollPane; import javafx.stage.Modality; import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.ui.Messages; import org.phoebus.framework.nls.NLS; @@ -21,9 +22,9 @@ /** * Dialog for editing {@link AlarmTreeItem}. * - *

Layout is defined in {@code ItemConfigDialog.fxml}. + *

Layout is defined in {@code LeafConfigDialog.fxml}. * Runtime wiring (model data, bindings, event handlers) is handled by - * {@link ItemConfigDialogController}. + * {@link LeafConfigDialogController}. * *

When pressing "OK", the dialog sends the updated configuration. */ @@ -40,13 +41,17 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { try { final FXMLLoader fxmlLoader = new FXMLLoader(); fxmlLoader.setResources(NLS.getMessages(Messages.class)); - fxmlLoader.setLocation(this.getClass().getResource("ItemConfigDialog.fxml")); + if (item instanceof AlarmClientLeaf){ + fxmlLoader.setLocation(this.getClass().getResource("LeafConfigDialog.fxml")); + } + else{ + fxmlLoader.setLocation(this.getClass().getResource("ComponentConfigDialog.fxml")); + } fxmlLoader.setControllerFactory(clazz -> { try { - return clazz.getConstructor(AlarmClient.class, - AlarmTreeItem.class).newInstance(model, item); + return clazz.getConstructor(AlarmClient.class, AlarmTreeItem.class).newInstance(model, item); } catch (Exception e) { - Logger.getLogger(ItemConfigDialog.class.getName()).log(Level.SEVERE, "Failed to construct ItemConfigDialogController", e); + Logger.getLogger(ItemConfigDialog.class.getName()).log(Level.SEVERE, "Failed to construct ConfigDialogController", e); } return null; }); @@ -57,12 +62,12 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { // ── OK-button validation filter ─────────────────────────────────────── final Button ok = (Button) getDialogPane().lookupButton(ButtonType.OK); ok.addEventFilter(ActionEvent.ACTION, - event -> ((ItemConfigDialogController)fxmlLoader.getController()).validateAndStore()); + event -> ((ConfigDialogController)fxmlLoader.getController()).validateAndStore()); getDialogPane().setContent(root); } catch (Exception ex) { - throw new RuntimeException("Failed to load ItemConfigDialog.fxml", ex); + throw new RuntimeException("Failed to load LeafConfigDialog.fxml", ex); } setResizable(true); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java deleted file mode 100644 index 85f15881fd..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialogController.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ -package org.phoebus.applications.alarm.ui.config; - -import javafx.application.Platform; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.fxml.FXML; -import javafx.scene.control.Alert; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.DateCell; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Spinner; -import javafx.scene.control.SpinnerValueFactory; -import javafx.scene.control.TextField; -import javafx.scene.control.TextFormatter; -import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import javafx.util.Duration; -import javafx.util.StringConverter; -import org.phoebus.applications.alarm.AlarmSystem; -import org.phoebus.applications.alarm.client.AlarmClient; -import org.phoebus.applications.alarm.client.AlarmClientLeaf; -import org.phoebus.applications.alarm.client.AlarmClientNode; -import org.phoebus.applications.alarm.model.AlarmTreeItem; -import org.phoebus.applications.alarm.ui.Messages; -import org.phoebus.applications.alarm.ui.tree.TitleDetailDelayTable; -import org.phoebus.applications.alarm.ui.tree.TitleDetailTable; -import org.phoebus.ui.dialog.DialogHelper; -import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; -import org.phoebus.util.time.SecondsParser; -import org.phoebus.util.time.TimeParser; - -import java.text.MessageFormat; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeParseException; -import java.time.temporal.TemporalAmount; - -/** - * FXML controller for ItemConfigDialog.fxml. - */ -@SuppressWarnings("nls") -public class ItemConfigDialogController { - - // ── FXML-injected fields ────────────────────────────────────────────────── - - @SuppressWarnings("unused") - @FXML - private ScrollPane scroll; - @SuppressWarnings("unused") - @FXML - private javafx.scene.layout.GridPane layout; - - // Path row (always visible) - @SuppressWarnings("unused") - @FXML - private TextField path; - - // Leaf-only rows - @SuppressWarnings("unused") - @FXML - private Label descriptionLabel; - @SuppressWarnings("unused") - @FXML - private TextField description; - - @SuppressWarnings("unused") - @FXML - private Label behaviorLabel; - @SuppressWarnings("unused") - @FXML - private HBox behaviorBox; - @SuppressWarnings("unused") - @FXML - private CheckBox enabled; - @SuppressWarnings("unused") - @FXML - private CheckBox latching; - @SuppressWarnings("unused") - @FXML - private CheckBox annunciating; - - @SuppressWarnings("unused") - @FXML - private Label disableUntilLabel; - @SuppressWarnings("unused") - @FXML - private ComboBox relativeDate; - - @SuppressWarnings("unused") - @FXML - private Label delayLabel; - @SuppressWarnings("unused") - @FXML - private Spinner delay; - @SuppressWarnings("unused") - @FXML - private Label countLabel; - @SuppressWarnings("unused") - @FXML - private Spinner count; - - @SuppressWarnings("unused") - @FXML - private Label filterLabel; - @SuppressWarnings("unused") - @FXML - private TextField filter; - - // Shared table placeholders - @SuppressWarnings("unused") - @FXML - private StackPane guidancePlaceholder; - @SuppressWarnings("unused") - @FXML - private StackPane displaysPlaceholder; - @SuppressWarnings("unused") - @FXML - private StackPane commandsPlaceholder; - @SuppressWarnings("unused") - @FXML - private StackPane actionsPlaceholder; - @SuppressWarnings("unused") - @FXML - private DateTimePicker enabledDatePicker; - @SuppressWarnings("unused") - @FXML - private HBox untilBox; - - private TitleDetailTable guidance; - private TitleDetailTable displays; - private TitleDetailTable commands; - private TitleDetailDelayTable actions; - - private final SimpleBooleanProperty itemEnabled = new SimpleBooleanProperty(); - - private final AlarmClient alarmClient; - private final AlarmTreeItem alarmTreeItem; - - public ItemConfigDialogController(AlarmClient alarmClient, AlarmTreeItem alarmTreeItem) { - this.alarmClient = alarmClient; - this.alarmTreeItem = alarmTreeItem; - } - - @SuppressWarnings("unused") - @FXML - public void initialize() { - - path.setText(alarmTreeItem.getPathName()); - - // ── Leaf-only section ───────────────────────────────────────────────── - if (alarmTreeItem instanceof AlarmClientLeaf leaf) { - itemEnabled.setValue(leaf.isEnabled()); - - // Description - description.setText(leaf.getDescription()); - - // Behavior checkboxes - enabled.setSelected(leaf.isEnabled()); - latching.setSelected(leaf.isLatching()); - annunciating.setSelected(leaf.isAnnunciating()); - - latching.disableProperty().bind(itemEnabled.not()); - annunciating.disableProperty().bind(itemEnabled.not()); - - itemEnabled.addListener((obs, o, n) -> leaf.setEnabled(n)); - - enabledDatePicker.disableProperty().bind(itemEnabled.not()); - - // Day-cell factory – disable past dates - enabledDatePicker.setDayCellFactory(picker -> new DateCell() { - @Override - public void updateItem(LocalDate date, boolean empty) { - super.updateItem(date, empty); - setDisable(empty || date.isBefore(LocalDate.now())); - } - }); - - // ENTER key handler on the date picker's editor - enabledDatePicker.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> { - if (keyEvent.getCode() == KeyCode.ENTER) { - try { - TextFormatter tf = enabledDatePicker.getEditor().getTextFormatter(); - @SuppressWarnings("unchecked") - StringConverter conv = - (StringConverter) tf.getValueConverter(); - conv.fromString(enabledDatePicker.getEditor().getText()); - enabledDatePicker.getEditor().commitValue(); - } catch (DateTimeParseException ex) { - keyEvent.consume(); - } - } - }); - - // Relative-date combo - relativeDate.getItems().addAll(AlarmSystem.shelving_options); - relativeDate.setDisable(!leaf.isEnabled()); - relativeDate.disableProperty().bind(itemEnabled.not()); - - // Relative-date action handler (must be a field so it can be removed temporarily) - final EventHandler relativeEventHandler = e -> { - enabled.setSelected(false); - enabledDatePicker.getEditor().clear(); - }; - relativeDate.setOnAction(relativeEventHandler); - - // Date-picker action handler - enabledDatePicker.setOnAction(e -> { - if (enabledDatePicker.getDateTimeValue() != null) { - relativeDate.setOnAction(null); - enabled.setSelected(false); - enabledDatePicker.getEditor().commitValue(); - relativeDate.getSelectionModel().clearSelection(); - relativeDate.setValue(null); - relativeDate.setOnAction(relativeEventHandler); - } - }); - - // Enabled checkbox action - enabled.setOnAction(e -> { - itemEnabled.setValue(enabled.isSelected()); - if (enabled.isSelected()) { - relativeDate.getSelectionModel().clearSelection(); - relativeDate.setValue(null); - enabledDatePicker.getEditor().clear(); - enabledDatePicker.setValue(null); - } - if (!enabled.isSelected()) { - leaf.setEnabled(false); - } - }); - - // Delay spinner - delay.setValueFactory( - new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, leaf.getDelay())); - delay.setEditable(true); - delay.setPrefWidth(80); - delay.disableProperty().bind(itemEnabled.not()); - final Tooltip delayTt = new Tooltip(); - delayTt.setShowDuration(Duration.seconds(30)); - delayTt.setOnShowing(event -> { - final int seconds = leaf.getDelay(); - final String detail; - if (seconds <= 0) - detail = Messages.delayTooltip1; - else { - detail = MessageFormat.format(Messages.delayTooltip2, seconds, SecondsParser.formatSeconds(seconds)); - } - delayTt.setText(MessageFormat.format(Messages.delayTooltip0, detail)); - }); - delay.setTooltip(delayTt); - - // Count spinner - count.setValueFactory( - new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, leaf.getCount())); - count.setEditable(true); - count.setPrefWidth(80); - count.disableProperty().bind(itemEnabled.not()); - - // Filter field - filter.setText(leaf.getFilter()); - filter.disableProperty().bind(itemEnabled.not()); - - // Initial focus - Platform.runLater(() -> description.requestFocus()); - - } else { - // Hide all leaf-only rows when item is not a leaf - setLeafSectionVisible(false); - } - - // ── Shared tables (guidance, displays, commands, actions) ───────────── - guidance = new TitleDetailTable(alarmTreeItem.getGuidance()); - guidance.setPrefHeight(100); - guidancePlaceholder.getChildren().setAll(guidance); - - displays = new TitleDetailTable(alarmTreeItem.getDisplays()); - displays.setPrefHeight(100); - displaysPlaceholder.getChildren().setAll(displays); - - commands = new TitleDetailTable(alarmTreeItem.getCommands()); - commands.setPrefHeight(100); - commandsPlaceholder.getChildren().setAll(commands); - - actions = new TitleDetailDelayTable(alarmTreeItem.getActions()); - actions.setPrefHeight(100); - actionsPlaceholder.getChildren().setAll(actions); - - // ── Scroll-pane width listener ──────────────────────────────────────── - scroll.widthProperty().addListener((p, old, width) -> - layout.setPrefWidth(Math.max(width.doubleValue() - 40, 450))); - } - - // ── Helpers ─────────────────────────────────────────────────────────────── - - /** - * Show or hide every control that belongs to the leaf-only section. - */ - private void setLeafSectionVisible(boolean visible) { - for (javafx.scene.Node n : new javafx.scene.Node[]{ - descriptionLabel, description, - behaviorLabel, behaviorBox, - disableUntilLabel, untilBox, - delayLabel, delay, - countLabel, count, - filterLabel, filter}) { - n.setVisible(visible); - n.setManaged(visible); - } - } - - // ── Validation / store ──────────────────────────────────────────────────── - - /** - * Validates input and sends the configuration off to the message broker. - */ - public void validateAndStore() { - final AlarmTreeItem config; - - if (alarmTreeItem instanceof AlarmClientLeaf) { - final AlarmClientLeaf pv = new AlarmClientLeaf(null, alarmTreeItem.getName()); - - boolean validEnableDate; - { - final LocalDateTime selectedEnableDate = enabledDatePicker.getDateTimeValue(); - final String relativeEnableDate = relativeDate.getValue(); - - if (selectedEnableDate != null) { - validEnableDate = pv.setEnabledDate(selectedEnableDate); - } else if (relativeEnableDate != null) { - final TemporalAmount amount = TimeParser.parseTemporalAmount(relativeEnableDate); - final LocalDateTime updateDate = LocalDateTime.now().plus(amount); - validEnableDate = pv.setEnabledDate(updateDate); - } else { - pv.setEnabled(itemEnabled.get()); - validEnableDate = true; - } - } - - if (!validEnableDate) { - Alert prompt = new Alert(Alert.AlertType.INFORMATION); - prompt.setTitle(Messages.promptTitle); - prompt.setHeaderText(Messages.promptTitle); - prompt.setContentText(Messages.promptContent); - DialogHelper.positionDialog(prompt, enabledDatePicker, 0, 0); - prompt.showAndWait(); - return; - } - - pv.setDescription(description.getText().trim()); - pv.setLatching(latching.isSelected()); - pv.setAnnunciating(annunciating.isSelected()); - pv.setDelay(delay.getValue()); - pv.setCount(count.getValue()); - // TODO Check filter expression - pv.setFilter(filter.getText().trim()); - - config = pv; - } else { - config = new AlarmClientNode(null, alarmTreeItem.getName()); - } - - config.setGuidance(guidance.getItems()); - config.setDisplays(displays.getItems()); - config.setCommands(commands.getItems()); - config.setActions(actions.getItems()); - - try { - alarmClient.sendItemConfigurationUpdate(alarmTreeItem.getPathName(), config); - } catch (Exception ex) { - ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); - } - } -} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java new file mode 100644 index 0000000000..5f5eed6163 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ +package org.phoebus.applications.alarm.ui.config; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.util.Duration; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.util.time.SecondsParser; + +import java.text.MessageFormat; +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.time.LocalDateTime; +import java.util.function.UnaryOperator; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * FXML controller for LeafConfigDialog.fxml. Intended for configuration + * of alarm tree leaf items. + */ +@SuppressWarnings("nls") +public class LeafConfigDialogController extends ConfigDialogController { + + @SuppressWarnings("unused") + @FXML + private TextField description; + @SuppressWarnings("unused") + @FXML + private HBox behaviorBox; + @SuppressWarnings("unused") + @FXML + private CheckBox latching; + @SuppressWarnings("unused") + @FXML + private CheckBox annunciating; + @SuppressWarnings("unused") + @FXML + private Spinner delay; + @SuppressWarnings("unused") + @FXML + private Spinner count; + @SuppressWarnings("unused") + @FXML + private TextField filter; + + private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(""); + private final SimpleBooleanProperty latchingProperty = new SimpleBooleanProperty(); + private final SimpleBooleanProperty annunciatingProperty = new SimpleBooleanProperty(); + private final SimpleStringProperty enablingFilterProperty = new SimpleStringProperty(""); + + private SpinnerValueFactory countValueFactory; + private SpinnerValueFactory delayValueFactory; + + public LeafConfigDialogController(AlarmClient alarmClient, AlarmTreeItem alarmTreeItem) { + super(alarmClient, alarmTreeItem); + } + + @SuppressWarnings("unused") + @FXML + public void initialize() { + + super.initialize(); + + description.textProperty().bindBidirectional(descriptionProperty); + latching.selectedProperty().bindBidirectional(latchingProperty); + annunciating.selectedProperty().bindBidirectional(annunciatingProperty); + filter.textProperty().bindBidirectional(enablingFilterProperty); + + relativeDate.disableProperty().bind(itemEnabledProperty.not()); + enabledDatePicker.disableProperty().bind(itemEnabledProperty.not()); + + AlarmClientLeaf leaf = (AlarmClientLeaf) alarmTreeItem; + + // Filter to disallow anything but numbers in the spinner + UnaryOperator integerFilter = c -> { + if (c.isContentChange()) { + ParsePosition parsePosition = new ParsePosition(0); + // NumberFormat evaluates the beginning of the text + NumberFormat.getIntegerInstance().parse(c.getControlNewText(), parsePosition); + if (parsePosition.getIndex() == 0 || parsePosition.getIndex() < c.getControlNewText().length()) { + // reject parsing the complete text failed + return null; + } + } + return c; + }; + + countValueFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, leaf.getCount(), 1); + TextFormatter countSpinnerFormatter = new TextFormatter<>(countValueFactory.getConverter(), countValueFactory.getValue(), integerFilter); + countValueFactory.valueProperty().bindBidirectional(countSpinnerFormatter.valueProperty()); + count.getEditor().setTextFormatter(countSpinnerFormatter); + count.setValueFactory(countValueFactory); + + delayValueFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, leaf.getDelay(), 1); + TextFormatter delaySpinnerFormatter = new TextFormatter<>(delayValueFactory.getConverter(), delayValueFactory.getValue(), integerFilter); + delayValueFactory.valueProperty().bindBidirectional(delaySpinnerFormatter.valueProperty()); + delay.getEditor().setTextFormatter(delaySpinnerFormatter); + delay.setValueFactory(delayValueFactory); + + descriptionProperty.set(leaf.getDescription()); + enablingFilterProperty.set(leaf.getFilter()); + enableDateProperty.set(leaf.getEnabledDate()); + + // Behavior checkboxes + itemEnabledProperty.setValue(leaf.isEnabled()); + enabled.selectedProperty().set(leaf.isEnabled()); + latchingProperty.setValue(leaf.isLatching()); + annunciatingProperty.setValue(leaf.isAnnunciating()); + + BooleanBinding binding = Bindings.createBooleanBinding(() -> + itemEnabledProperty.not().get() || relativeDateProperty.isNotNull().get() || enableDateProperty.isNotNull().get(), + itemEnabledProperty, relativeDateProperty, enableDateProperty); + + latching.disableProperty().bind(binding); + annunciating.disableProperty().bind(binding); + count.disableProperty().bind(binding); + delay.disableProperty().bind(binding); + filter.disableProperty().bind(binding); + + // Delay spinner + final Tooltip delayTt = new Tooltip(); + delayTt.setShowDuration(Duration.seconds(30)); + delayTt.setOnShowing(event -> { + final int seconds = leaf.getDelay(); + final String detail; + if (seconds <= 0) + detail = Messages.delayTooltip1; + else { + detail = MessageFormat.format(Messages.delayTooltip2, seconds, SecondsParser.formatSeconds(seconds)); + } + delayTt.setText(MessageFormat.format(Messages.delayTooltip0, detail)); + }); + delay.setTooltip(delayTt); + + // Initial focus + Platform.runLater(() -> description.requestFocus()); + } + + /** + * Validates input and sends the configuration off to the message broker. + */ + @Override + public void validateAndStore() { + + final AlarmClientLeaf pv = new AlarmClientLeaf(null, alarmTreeItem.getName()); + + LocalDateTime enableDate; + try { + enableDate = determineEnableDate(); + } catch (Exception e) { + Logger.getLogger(LeafConfigDialogController.class.getName()) + .log(Level.WARNING, "Invalid enable date specified", e); + return; + } + if (enableDate != null) { + pv.setEnabledDate(enableDate); + } else { + pv.setEnabled(itemEnabledProperty.get()); + } + + pv.setDescription(descriptionProperty.get()); + pv.setLatching(latchingProperty.get()); + pv.setAnnunciating(annunciatingProperty.get()); + pv.setDelay(delayValueFactory.getValue()); + pv.setCount(countValueFactory.getValue()); + // TODO Check filter expression + pv.setFilter(enablingFilterProperty.getValue()); + + pv.setGuidance(guidance.getItems()); + pv.setDisplays(displays.getItems()); + pv.setCommands(commands.getItems()); + pv.setActions(actions.getItems()); + + try { + alarmClient.sendItemConfigurationUpdate(alarmTreeItem.getPathName(), pv); + } catch (Exception ex) { + ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); + } + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java index 1f17709bde..0426b0730c 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelper.java @@ -26,12 +26,13 @@ public class ComponentActionHelper { /** * Updates a component or PV node to enable or disable alarms - * @param node The visual component relative to which a confirmation dialog is positioned. - * @param model {@link AlarmClient} dispatching producer messages. - * @param items {@link List} of items subject for update, e.g. selected by user. + * + * @param node The visual component relative to which a confirmation dialog is positioned. + * @param model {@link AlarmClient} dispatching producer messages. + * @param items {@link List} of items subject for update, e.g. selected by user. * @param enable If true, enable alarms on selected nodes, otherwise disable. */ - public static void updateEnablement(final Node node, final AlarmClient model, final List> items, boolean enable){ + public static void updateEnablement(final Node node, final AlarmClient model, final List> items, boolean enable) { final List pvs = new ArrayList<>(); for (AlarmTreeItem item : items) { findAffectedPVs(item, pvs, enable); @@ -47,8 +48,7 @@ public static void updateEnablement(final Node node, final AlarmClient model, fi enable ? Messages.headerAlreadyEnabled : Messages.headerAlreadyDisabled); - } - else { + } else { dialog.setHeaderText(MessageFormat.format( enable ? Messages.headerConfirmEnable @@ -63,8 +63,7 @@ public static void updateEnablement(final Node node, final AlarmClient model, fi JobManager.schedule(enable ? Messages.enableAlarms : Messages.disableAlarms, monitor -> { - for (AlarmClientLeaf pv : pvs) - { + for (AlarmClientLeaf pv : pvs) { final AlarmClientLeaf copy = pv.createDetachedCopy(); if (copy.setEnabled(enable)) try { @@ -79,21 +78,19 @@ public static void updateEnablement(final Node node, final AlarmClient model, fi }); } - /** @param item Node where to start recursing for PVs that would be affected - * @param pvs Array to update with PVs that would be affected + /** + * @param item Node where to start recursing for PVs that would be affected + * @param pvs Array to update with PVs that would be affected */ - private static void findAffectedPVs(final AlarmTreeItem item, final List pvs, boolean enable) - { - if (item instanceof AlarmClientLeaf) - { + public static void findAffectedPVs(final AlarmTreeItem item, final List pvs, boolean enable) { + if (item instanceof AlarmClientLeaf) { final AlarmClientLeaf pv = (AlarmClientLeaf) item; // If pv has different enablement, and wasn't already added // because selection contains its parent as well as the PV itself... - if (pv.isEnabled() != enable && !pvs.contains(pv)) { + if (pv.isEnabled() != enable && !pvs.contains(pv)) { pvs.add(pv); } - } - else { + } else { for (AlarmTreeItem sub : item.getChildren()) { findAffectedPVs(sub, pvs, enable); } diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml new file mode 100644 index 0000000000..ec7d0991cb --- /dev/null +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml deleted file mode 100644 index 961da34a32..0000000000 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.fxml +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/LeafConfigDialog.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/LeafConfigDialog.fxml new file mode 100644 index 0000000000..dacf80906b --- /dev/null +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/LeafConfigDialog.fxml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties index eb7e5a456d..4aee77de3b 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties @@ -16,46 +16,47 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # - +acknowledgeFailed=Failed to acknowledge alarm(s) +addComponentFailed=Failed to add component alarmCount=Alarm Count [within delay]: alarmCountTooltip=Alarms are indicated when they occur this often within the delay alarmDelay=Alarm Delay [seconds]: annunciate=Annunciate annunciateTooltip=Request audible alarm annunciation (using the description)? automatedActions=Automated Actions: +behavior=Behavior: +behaviorTooltip=Enable alarms? See also 'Enabling Filter' commands=Commands: configure=Configure dateTimePickerTooltip=Select a date until which the alarm should be disabled delayTooltip0=Alarms are indicated when they persist for at least this long.\n{0} delayTooltip1=With the current delay of 0 seconds, alarms trigger immediately delayTooltip2=With the current delay of {0}seconds, alarms trigger after {1} hours:minutes:seconds +description=Description: +descriptionTooltip=Alarm description, also used for annunciation +disabled=Disabled +disableAlarmFailed=Failed to disable alarm +disableAlarms=Disable Alarms +disabledUntil=Disabled until disableUntil=Disable until: displays=Displays: enabled=Enabled enablingFilter=Enabling Filter: enablingFilterTooltip=Optional expression for enabling the alarm error=Error -acknowledgeFailed=Failed to acknowledge alarm(s) -addComponentFailed=Failed to add component -behavior=Behavior: -behaviorTooltip=Enable alarms? See also 'Enabling Filter' -description=Description: -descriptionTooltip=Alarm description, also used for annunciation -disableAlarmFailed=Failed to disable alarm -disableAlarms=Disable Alarms -disabled=Disabled -disabledUntil=Disabled until enableAlarmFailed=Failed to enable alarm enableAlarms=Enable Alarms guidance=Guidance: headerAlreadyDisabled=All PVs in the selected section are already disabled headerAlreadyEnabled=All PVs in the selected section are already enabled headerConfirmDisable=Disable all PVs in the selected section of the alarm hierarchy?\nThis would disable {0} PVs. +headerConfirmDisableWithEnableDate=Disable all PVs in the selected section of the alarm hierarchy until {0}?\nThis would affect {1} PVs. headerConfirmEnable=Enable all PVs in the selected section of the alarm hierarchy?\nThis would enable {0} PVs. latch=Latch latchTooltip=Latch alarm until acknowledged? moveItemFailed=Failed to move item partlyDisabled=Partly disabled +partlyDisabled2=(Partly disabled) path=Path: promptTitle='Disable until' is set to a point in time in the past promptContent=The option 'disable until' must be set to a point in time in the future. diff --git a/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogControllerTest.java b/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogControllerTest.java new file mode 100644 index 0000000000..f65a9cae55 --- /dev/null +++ b/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogControllerTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.ui.config; + +import org.junit.jupiter.api.Test; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.client.AlarmClientNode; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.model.BasicState; +import org.phoebus.applications.alarm.model.EnabledState; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ComponentConfigDialogControllerTest { + + @Test + public void testFindAffectedPvs_5() { + AlarmTreeItem parent1 = new AlarmClientNode("/root", "parent1"); + AlarmTreeItem parent2 = new AlarmClientNode("/parent", "parent2"); + AlarmClientLeaf child1 = new AlarmClientLeaf("/paren1", "child1"); + parent2.addToParent(parent1); + child1.setEnabled(false); + child1.addToParent(parent1); + AlarmClientLeaf child2 = new AlarmClientLeaf("/parent2", "child2"); + child2.addToParent(parent2); + AlarmClientLeaf child3 = new AlarmClientLeaf("/parent2", "child3"); + child3.setEnabled(new EnabledState(LocalDateTime.now())); + child3.addToParent(parent2); + + List total = new ArrayList<>(); + List disabled = new ArrayList<>(); + List withEnableDate = new ArrayList<>(); + + ComponentConfigDialogController.findAffectedPVs(parent1, total, disabled, withEnableDate); + + assertEquals(3, total.size()); + assertEquals(2, disabled.size()); + assertEquals(1, withEnableDate.size()); + } +} diff --git a/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelperTest.java b/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelperTest.java new file mode 100644 index 0000000000..91ad5c79c1 --- /dev/null +++ b/app/alarm/ui/src/test/java/org/phoebus/applications/alarm/ui/tree/ComponentActionHelperTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.ui.tree; + +import org.junit.jupiter.api.Test; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.client.AlarmClientNode; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.model.BasicState; +import org.phoebus.applications.alarm.model.EnabledState; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ComponentActionHelperTest { + + @Test + public void testFindAffectedPvs_1() { + AlarmTreeItem parent1 = new AlarmClientNode("/root", "parent1"); + AlarmTreeItem parent2 = new AlarmClientNode("/parent", "parent2"); + AlarmClientLeaf child1 = new AlarmClientLeaf("/paren1", "child1"); + parent2.addToParent(parent1); + child1.addToParent(parent1); + AlarmClientLeaf child2 = new AlarmClientLeaf("/parent2", "child2"); + child2.addToParent(parent2); + + List disabledPvs = new ArrayList<>(); + ComponentActionHelper.findAffectedPVs(parent1, disabledPvs, true); + + assertTrue(disabledPvs.isEmpty()); + } + + @Test + public void testFindAffectedPvs_2() { + AlarmTreeItem parent1 = new AlarmClientNode("/root", "parent1"); + AlarmTreeItem parent2 = new AlarmClientNode("/parent", "parent2"); + AlarmClientLeaf child1 = new AlarmClientLeaf("/paren1", "child1"); + parent2.addToParent(parent1); + child1.addToParent(parent1); + AlarmClientLeaf child2 = new AlarmClientLeaf("/parent2", "child2"); + child2.setEnabled(new EnabledState(LocalDateTime.now())); + child2.addToParent(parent2); + + List disabledPvs = new ArrayList<>(); + ComponentActionHelper.findAffectedPVs(parent1, disabledPvs, true); + + assertEquals(1, disabledPvs.size()); + } + + @Test + public void testFindAffectedPvs_3() { + AlarmTreeItem parent1 = new AlarmClientNode("/root", "parent1"); + AlarmTreeItem parent2 = new AlarmClientNode("/parent", "parent2"); + AlarmClientLeaf child1 = new AlarmClientLeaf("/paren1", "child1"); + parent2.addToParent(parent1); + child1.addToParent(parent1); + AlarmClientLeaf child2 = new AlarmClientLeaf("/parent2", "child2"); + child2.addToParent(parent2); + AlarmClientLeaf child3 = new AlarmClientLeaf("/parent2", "child3"); + child3.setEnabled(false); + child3.addToParent(parent2); + + List disabledPvs = new ArrayList<>(); + ComponentActionHelper.findAffectedPVs(parent1, disabledPvs, true); + + assertEquals(1, disabledPvs.size()); + } +} From c3496cca23d27ed9474c7ea8999e75203e40eeed Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 27 May 2026 13:54:51 +0200 Subject: [PATCH 4/7] Documentation update --- app/alarm/ui/doc/index.rst | 76 ++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/app/alarm/ui/doc/index.rst b/app/alarm/ui/doc/index.rst index 921ec89f4d..4fa8680f00 100644 --- a/app/alarm/ui/doc/index.rst +++ b/app/alarm/ui/doc/index.rst @@ -136,29 +136,9 @@ Alarm Configuration Options Alarm configurations are imported into the Alarm Server in an XML format, the schema for which may be found `here `_. -The options for an entry in the hierarchical alarm configuration -always include guidance, display links etc. as described further below. -In addition, alarm PV entries have the following settings. -Description -^^^^^^^^^^^ -This text is displayed in the alarm table when the alarm triggers. - -The description is also used by the alarm annunciator. -By default, the annunciator will start the actual message with -the alarm severity. For example, a description of "Vacuum Problem" -will be annunciated as for example "Minor Alarm: Vacuum Problem". -The addition of the alarm severity can be disabled by starting -the description with a "\*" as in "\* Vacuum Problem". - -When there is a flurry of alarms, the annunciator will summarize -them to "There are 10 more alarms". To assert that certain alarms -are always annunciated, even if they occur within a burst of other alarms, -start the message with "!" (or "\*!"). - - -Behavior -^^^^^^^^ +Behavior - PV entries +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * Enabled: De-select to disable an alarm, i.e. to ignore the value of this alarm trigger PV. @@ -171,10 +151,14 @@ Behavior Should the alarm be annunciated (if the annunciator is running), or should it only be displayed silently? + * Disable until: + Disables an alarm and sets a date and time when the alarm + would be enabled automatically. User may select absolute or relative date/time. + * Alarm Delay: Only alarm if the trigger PV remains in alarm for at least this time, see examples below. - + * Alarm Count: Used in combination with the alarm delay. If the trigger PVs exhibits a not-OK alarm severity more than 'count' times @@ -188,9 +172,8 @@ Behavior * Enabling Filter: An optional expression that can enable the alarm based on other PVs. - - Example: `'abc' > 10` will only enable this alarm if the PV 'abc' has a value above 10. + Example: `'abc' > 10` will only enable this alarm if the PV 'abc' has a value above 10. The Alarm Delay and Count work in combination. By default, with both the alarm delay and count at zero, a non-OK PV severity is right away recognized. @@ -230,8 +213,45 @@ guidance and display links which allow the user to figure out: * What does this alarm mean? What should I do about it? * What displays allow me to see more, where can I do something about the alarm? +Behavior - component entries +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + * Enabled: + Enable or disable all PV items in the subtree. + + * Disable until: + Disables an all PV items in the subtree and set a date and time when the alarms + would be enabled automatically. User may select absolute or relative date/time. + +Note: + + * An enable date will be set even if a PV item is already disabled. + + * Ticking the Enable check box will enable all PVs in the subtree when saving the changes, + including those that have an enable date set. + + * If an enable date is set on any PV in the subtree, it is not possible to set an enable + date. + +Description - PV entries only +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This text is displayed in the alarm table when the alarm triggers. + +The description is also used by the alarm annunciator. +By default, the annunciator will start the actual message with +the alarm severity. For example, a description of "Vacuum Problem" +will be annunciated as for example "Minor Alarm: Vacuum Problem". +The addition of the alarm severity can be disabled by starting +the description with a "\*" as in "\* Vacuum Problem". + +When there is a flurry of alarms, the annunciator will summarize +them to "There are 10 more alarms". To assert that certain alarms +are always annunciated, even if they occur within a burst of other alarms, +start the message with "!" (or "\*!"). + Guidance --------- +^^^^^^^^ Each alarm should have at least one guidance message to explain the meaning of an alarm to the user, to list for example contact information for subsystem experts. @@ -249,7 +269,7 @@ parent components of the alarm hierarchy. Displays --------- +^^^^^^^^ As with Guidance, each alarm should have at least one link to a control system display that shows the actual alarm PV and the surrounding subsystem. @@ -275,7 +295,7 @@ Examples:: file:///path/to/display.bob?MACRO=Value&OTHER=42$NAME=Text+with+spaces Automated Actions ------------------ +^^^^^^^^^^^^^^^^ Automated actions are performed when the node in the alarm hierarchy enters and remains in an active alarm state for some time. From 1b260f270f2d2c07c9c7c1b3fe86d3b17d99f709 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 27 May 2026 15:20:34 +0200 Subject: [PATCH 5/7] Log location of fxml upon load failure --- .../applications/alarm/ui/config/ItemConfigDialog.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java index dfec2bbc99..0584693e92 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java @@ -37,9 +37,8 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { initModality(Modality.NONE); setTitle(Messages.configure + " " + item.getName()); getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - + final FXMLLoader fxmlLoader = new FXMLLoader(); try { - final FXMLLoader fxmlLoader = new FXMLLoader(); fxmlLoader.setResources(NLS.getMessages(Messages.class)); if (item instanceof AlarmClientLeaf){ fxmlLoader.setLocation(this.getClass().getResource("LeafConfigDialog.fxml")); @@ -67,7 +66,7 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { getDialogPane().setContent(root); } catch (Exception ex) { - throw new RuntimeException("Failed to load LeafConfigDialog.fxml", ex); + throw new RuntimeException("Failed to load " + fxmlLoader.getLocation(), ex); } setResizable(true); From 2462f26f386789fed3af315c8aa096f6b524bd6a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 1 Jun 2026 10:54:55 +0200 Subject: [PATCH 6/7] Migrate alarm config tables to fxml --- .../applications/alarm/ui/Messages.java | 4 + .../ComponentConfigDialogController.java | 9 +- .../ui/config/ConfigDialogController.java | 23 +- .../alarm/ui/config/ItemConfigDialog.java | 21 +- .../ui/config/LeafConfigDialogController.java | 8 +- .../ui/config/OptionsTablesController.java | 65 ++++ .../TitleDetailDelayTableController.java | 281 ++++++++++++++++++ .../ui/config/TitleDetailTableController.java | 208 +++++++++++++ .../config/TitleDetailToolbarController.java | 66 ++++ .../ui/config/ComponentConfigDialog.fxml | 60 +--- .../alarm/ui/config/LeafConfigDialog.fxml | 57 +--- .../alarm/ui/config/OptionsTables.fxml | 17 ++ .../ui/config/TitleDetailDelayTable.fxml | 43 +++ .../alarm/ui/config/TitleDetailTable.fxml | 38 +++ .../alarm/ui/config/TitleDetailToolbar.fxml | 88 ++++++ .../applications/alarm/ui/messages.properties | 10 + 16 files changed, 875 insertions(+), 123 deletions(-) create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java create mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/OptionsTables.fxml create mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTable.fxml create mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailTable.fxml create mode 100644 app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailToolbar.fxml diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java index df38bf1bae..4c622d57ae 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java @@ -15,6 +15,8 @@ public class Messages public static String acknowledgeFailed; public static String addComponentFailed; + public static String automatedActions; + public static String commands; public static String configure; public static String delayTooltip0; public static String delayTooltip1; @@ -23,8 +25,10 @@ public class Messages public static String disableAlarms; public static String disabled; public static String disabledUntil; + public static String displays; public static String enableAlarmFailed; public static String enableAlarms; + public static String guidance; public static String headerAlreadyDisabled; public static String headerAlreadyEnabled; public static String headerConfirmDisable; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java index 26012c28b7..8df9d1a2d6 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java @@ -131,11 +131,10 @@ public void validateAndStore() { return; } - // Next store guidance, displays... - alarmTreeItem.setGuidance(guidance.getItems()); - alarmTreeItem.setDisplays(displays.getItems()); - alarmTreeItem.setCommands(commands.getItems()); - alarmTreeItem.setActions(actions.getItems()); + alarmTreeItem.setGuidance(optionsTablesViewController.getGuidance()); + alarmTreeItem.setDisplays(optionsTablesViewController.getDisplays()); + alarmTreeItem.setCommands(optionsTablesViewController.getCommands()); + alarmTreeItem.setActions(optionsTablesViewController.getActions()); try { alarmClient.sendItemConfigurationUpdate(alarmTreeItem.getPathName(), alarmTreeItem); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java index 8f1a11b740..874b5f3ca6 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java @@ -75,10 +75,8 @@ public abstract class ConfigDialogController { @FXML private StackPane actionsPlaceholder; - protected TitleDetailTable guidance; - protected TitleDetailTable displays; - protected TitleDetailTable commands; - protected TitleDetailDelayTable actions; + @FXML + protected OptionsTablesController optionsTablesViewController; protected final AlarmClient alarmClient; protected final AlarmTreeItem alarmTreeItem; @@ -98,23 +96,6 @@ public void initialize() { path.setText(alarmTreeItem.getPathName()); - // ── Shared tables (guidance, displays, commands, actions) ───────────── - guidance = new TitleDetailTable(alarmTreeItem.getGuidance()); - guidance.setPrefHeight(100); - guidancePlaceholder.getChildren().setAll(guidance); - - displays = new TitleDetailTable(alarmTreeItem.getDisplays()); - displays.setPrefHeight(100); - displaysPlaceholder.getChildren().setAll(displays); - - commands = new TitleDetailTable(alarmTreeItem.getCommands()); - commands.setPrefHeight(100); - commandsPlaceholder.getChildren().setAll(commands); - - actions = new TitleDetailDelayTable(alarmTreeItem.getActions()); - actions.setPrefHeight(100); - actionsPlaceholder.getChildren().setAll(actions); - relativeDate.valueProperty().bindBidirectional(relativeDateProperty); enabledDatePicker.dateTimeValueProperty().bindBidirectional(enableDateProperty); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java index 0584693e92..719a171a3e 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java @@ -48,9 +48,26 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { } fxmlLoader.setControllerFactory(clazz -> { try { - return clazz.getConstructor(AlarmClient.class, AlarmTreeItem.class).newInstance(model, item); + if(clazz.isAssignableFrom(LeafConfigDialogController.class)) { + return clazz.getConstructor(AlarmClient.class, AlarmTreeItem.class).newInstance(model, item); + } + if(clazz.isAssignableFrom(ComponentConfigDialogController.class)) { + return clazz.getConstructor(AlarmClient.class, AlarmTreeItem.class).newInstance(model, item); + } + else if(clazz.isAssignableFrom(TitleDetailTableController.class)) { + return new TitleDetailTableController(); + } + else if(clazz.isAssignableFrom(TitleDetailDelayTableController.class)) { + return new TitleDetailDelayTableController(); + } + else if(clazz.isAssignableFrom(OptionsTablesController.class)) { + return clazz.getConstructor(AlarmTreeItem.class).newInstance(item); + } + else if(clazz.isAssignableFrom(TitleDetailToolbarController.class)){ + return new TitleDetailToolbarController(); + } } catch (Exception e) { - Logger.getLogger(ItemConfigDialog.class.getName()).log(Level.SEVERE, "Failed to construct ConfigDialogController", e); + Logger.getLogger(ItemConfigDialog.class.getName()).log(Level.SEVERE, "Failed to construct controller for " + clazz.getName(), e); } return null; }); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java index 5f5eed6163..59226d8275 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java @@ -184,10 +184,10 @@ public void validateAndStore() { // TODO Check filter expression pv.setFilter(enablingFilterProperty.getValue()); - pv.setGuidance(guidance.getItems()); - pv.setDisplays(displays.getItems()); - pv.setCommands(commands.getItems()); - pv.setActions(actions.getItems()); + pv.setGuidance(optionsTablesViewController.getGuidance()); + pv.setDisplays(optionsTablesViewController.getDisplays()); + pv.setCommands(optionsTablesViewController.getCommands()); + pv.setActions(optionsTablesViewController.getActions()); try { alarmClient.sendItemConfigurationUpdate(alarmTreeItem.getPathName(), pv); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java new file mode 100644 index 0000000000..58c33edf03 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.ui.config; + +import javafx.fxml.FXML; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.model.TitleDetail; +import org.phoebus.applications.alarm.model.TitleDetailDelay; +import org.phoebus.applications.alarm.ui.Messages; + +import java.util.List; + +/** + * Controller for the container of the guidance, displays... table views. + */ +public class OptionsTablesController { + + @SuppressWarnings("unused") + @FXML + private TitleDetailTableController guidanceViewController; + @SuppressWarnings("unused") + @FXML + private TitleDetailTableController displaysViewController; + @SuppressWarnings("unused") + @FXML + private TitleDetailTableController commandsViewController; + @SuppressWarnings("unused") + @FXML + private TitleDetailDelayTableController actionsViewController; + + private final AlarmTreeItem item; + + public OptionsTablesController(final AlarmTreeItem item) { + this.item = item; + } + + public void initialize(){ + guidanceViewController.setTitle(Messages.guidance); + guidanceViewController.setItems(item.getGuidance()); + displaysViewController.setTitle(Messages.displays); + displaysViewController.setItems(item.getDisplays()); + commandsViewController.setTitle(Messages.commands); + commandsViewController.setItems(item.getCommands()); + actionsViewController.setTitle(Messages.automatedActions); + actionsViewController.setItems(item.getActions()); + } + + public List getGuidance(){ + return guidanceViewController.getItems().stream().map(i -> (TitleDetail)i).toList(); + } + + public List getDisplays(){ + return displaysViewController.getItems().stream().map(i -> (TitleDetail)i).toList(); + } + + public List getCommands(){ + return commandsViewController.getItems().stream().map(i -> (TitleDetail)i).toList(); + } + + public List getActions(){ + return actionsViewController.getItems().stream().map(i -> (TitleDetailDelay)i).toList(); + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java new file mode 100644 index 0000000000..301ea59987 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java @@ -0,0 +1,281 @@ +/******************************************************************************* + * Copyright (c) 2018-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.phoebus.applications.alarm.ui.config; + +import javafx.application.Platform; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Spinner; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.ComboBoxTableCell; +import javafx.util.converter.DefaultStringConverter; +import org.phoebus.applications.alarm.model.TitleDetail; +import org.phoebus.applications.alarm.model.TitleDetailDelay; +import org.phoebus.applications.alarm.ui.tree.ValidatingTextFieldTableCell; + +import java.util.List; +import java.util.Objects; + +/** + * FXML controller for TitleDetailDelayTable.fxml. + * + *

All UI structure is declared in the FXML file. This controller wires up + * cell factories, cell-value factories, event handlers, and button icons + * (which cannot be expressed in plain FXML without a custom builder). + * + * @author Evan Smith + */ +@SuppressWarnings("nls") +public class TitleDetailDelayTableController extends TitleDetailTableController { + // ── Option enum ──────────────────────────────────────────────────────────── + + private enum ActionOption { + /** + * Send e-mail with alarm info + */ + mailto, + /** + * Execute external command + */ + cmd, + /** + * Update PV with severity + */ + sevrpv, + /** + * Update PV with alarm-info text + */ + infopv + } + + @SuppressWarnings("unused") + @FXML + private TableView table; + @SuppressWarnings("unused") + @FXML + private TableColumn optionColumn; + @SuppressWarnings("unused") + @FXML + private TableColumn infoColumn; + @SuppressWarnings("unused") + @FXML + private TableColumn delayColumn; + + private final ObservableList items = FXCollections.observableArrayList(); + + /** + * Configure table columns particular for this view. + */ + @Override + public void configureColumns() { + + optionColumn.setCellFactory(ComboBoxTableCell.forTableColumn(ActionOption.values())); + optionColumn.setCellValueFactory( + cell -> new SimpleObjectProperty<>(getOptionFromDetail(cell.getValue()))); + optionColumn.setOnEditCommit(event -> + { + final int row = event.getTablePosition().getRow(); + final TitleDetailDelay updated = setOptionToDetail(items.get(row), event.getNewValue()); + items.set(row, updated); + + if (updated.hasDelay()) + Platform.runLater(() -> + { + table.getSelectionModel().clearAndSelect(row); + table.edit(row, infoColumn); + }); + }); + + // ── Info (text-field sub-column under Detail) ────────────────────────── + infoColumn.setCellValueFactory(cell -> new SimpleStringProperty(getInfoFromDetail(cell.getValue()))); + infoColumn.setCellFactory(ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); + infoColumn.setOnEditCommit(event -> + { + final int row = event.getTablePosition().getRow(); + final TitleDetailDelay updated = setInfoToDetail(items.get(row), event.getNewValue()); + items.set(row, updated); + + if (updated.hasDelay()) + Platform.runLater(() -> + { + table.getSelectionModel().clearAndSelect(row); + table.edit(row, delayColumn); + }); + }); + + // ── Delay (spinner column) ───────────────────────────────────────────── + delayColumn.setCellValueFactory( + cell -> new SimpleIntegerProperty(cell.getValue().delay).asObject()); + delayColumn.setCellFactory(column -> new DelayTableCell()); + delayColumn.setOnEditCommit(event -> + { + final int row = event.getTablePosition().getRow(); + items.set(row, new TitleDetailDelay( + items.get(row).title, + items.get(row).detail, + event.getNewValue())); + }); + titleColumn.setOnEditCommit(event -> + { + final int row = event.getTablePosition().getRow(); + items.set(row, new TitleDetailDelay( + event.getNewValue(), + items.get(row).detail, + items.get(row).delay)); + + // Auto-advance to the Option sub-column + Platform.runLater(() -> + { + table.getSelectionModel().clearAndSelect(row); + table.edit(row, optionColumn); + }); + }); + } + + /** + * @return current items, with empty title+detail rows removed + */ + @Override + public List getItems() { + items.removeIf(item -> item.title.isEmpty() && item.detail.isEmpty()); + return items; + } + + + // ── Detail helpers (unchanged logic) ────────────────────────────────────── + + private ActionOption getOptionFromDetail(final TitleDetailDelay tdd) { + if (tdd == null) return null; + final int sep = tdd.detail.indexOf(':'); + if (sep < 0) return ActionOption.mailto; + try { + return ActionOption.valueOf(tdd.detail.substring(0, sep)); + } catch (Exception e) { + return ActionOption.mailto; + } + } + + private String getInfoFromDetail(final TitleDetailDelay tdd) { + if (tdd == null) return ""; + final int sep = tdd.detail.indexOf(':'); + return sep < 0 ? "" : tdd.detail.substring(sep + 1); + } + + private TitleDetailDelay setOptionToDetail(final TitleDetailDelay tdd, final ActionOption option) { + if (tdd == null || option == null) return tdd; + return new TitleDetailDelay( + tdd.title, + option + ":" + getInfoFromDetail(tdd), + tdd.delay); + } + + private TitleDetailDelay setInfoToDetail(final TitleDetailDelay tdd, final String info) { + if (tdd == null || info == null) return tdd; + return new TitleDetailDelay( + tdd.title, + getOptionFromDetail(tdd) + ":" + info.replace("\\n", "\n"), + tdd.delay); + } + + @Override + public void handleAdd() { + items.add(new TitleDetailDelay("", "", 0)); + + // Trigger editing the title of new item + Platform.runLater(() -> + { + final int row = items.size() - 1; + table.getSelectionModel().clearAndSelect(row); + table.edit(row, table.getColumns().get(0)); + }); + } + + // ── DelayTableCell (inner class – unchanged logic) ───────────────────────── + + /** + * Custom {@link TableCell} for the Delay column. + * Shows a {@link Spinner} and disables it for action types that have no delay. + */ + private static class DelayTableCell extends TableCell { + private final Spinner spinner; + + DelayTableCell() { + this.spinner = new Spinner<>(0, 10_000, 1); + this.spinner.setEditable(true); + + // Keep arrow buttons out of the tab order + spinner.lookupAll(".increment-arrow-button, .decrement-arrow-button") + .forEach(node -> node.setFocusTraversable(false)); + + spinner.valueProperty().addListener((obs, oldValue, newValue) -> + { + if (isEditing()) commitEdit(newValue); + }); + + // Validate on focus loss + spinner.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> + { + if (!isNowFocused) { + final Integer current = spinner.getValue(); + if (Objects.equals(current, getItem())) { + cancelEdit(); + return; + } + + if (!isEditing()) { + final TableView tv = getTableView(); + if (tv != null) + Platform.runLater(() -> + { + tv.getSelectionModel().clearAndSelect(getIndex()); + tv.edit(getIndex(), getTableColumn()); + commitEdit(current); + }); + else + commitEdit(current); + } else + commitEdit(current); + } + }); + } + + @Override + public void updateItem(final Integer item, final boolean empty) { + super.updateItem(item, empty); + + if (empty + || getTableRow() == null + || getTableRow().getItem() == null) { + setGraphic(null); + return; + } + + final boolean hasDelay = getTableRow().getItem().hasDelay(); + spinner.setDisable(!hasDelay); + spinner.getEditor().setStyle( + hasDelay ? "-fx-text-inner-color: black;" + : "-fx-text-inner-color: lightgray;"); + spinner.getValueFactory().setValue(item); + setGraphic(spinner); + + Platform.runLater(() -> + { + if (isEditing()) { + spinner.getEditor().requestFocus(); + spinner.getEditor().end(); + } + }); + } + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java new file mode 100644 index 0000000000..5834b90aed --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2018 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.phoebus.applications.alarm.ui.config; + +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.util.converter.DefaultStringConverter; +import org.phoebus.applications.alarm.model.TitleDetail; +import org.phoebus.applications.alarm.ui.tree.ValidatingTextFieldTableCell; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.MultiLineInputDialog; +import org.phoebus.ui.javafx.UpdateThrottle; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Controller for TitleDetailTable.fxml. + * + * @author Kay Kasemir (original), FXML refactor + */ +@SuppressWarnings("nls") +public class TitleDetailTableController { + + @SuppressWarnings("unused") + @FXML + private TableView table; + @SuppressWarnings("unused") + @FXML + protected TableColumn titleColumn; + @SuppressWarnings("unused") + @FXML + private TableColumn detailColumn; + @SuppressWarnings("unused") + @FXML + private Node optionsRoot; + @SuppressWarnings("unused") + @FXML + private Label titleLabel; + + @FXML + protected TitleDetailToolbarController titleDetailToolbarViewController; + + private final ObservableList items = FXCollections.observableArrayList(); + + public void initialize() { + + titleDetailToolbarViewController.setTitleDetailTableController(this); + + table.setItems(items); + table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + + // Title column + titleColumn.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().title)); + titleColumn.setCellFactory(ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); + + configureColumns(); + + final InvalidationListener item_selected = prop -> + titleDetailToolbarViewController.setButtonStates(table.getSelectionModel().getSelectedCells().size()); + table.getSelectionModel().selectedIndexProperty().addListener(item_selected); + // Apply initial state + item_selected.invalidated(null); + + } + + /** + * Configure table columns particular for this view. + */ + public void configureColumns(){ + + titleColumn.setOnEditCommit(event -> + { + final int row = event.getTablePosition().getRow(); + items.set(row, new TitleDetail(event.getNewValue(), items.get(row).detail)); + + // Immediately move focus to the Detail column + Platform.runLater(() -> + { + table.getSelectionModel().clearAndSelect(row); + table.edit(row, table.getColumns().get(1)); + }); + }); + + // Detail column (newlines stored as \n, displayed as \\n) + detailColumn.setCellValueFactory( + cell -> new SimpleStringProperty(cell.getValue().detail.replace("\n", "\\n"))); + detailColumn.setCellFactory( + ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); + detailColumn.setOnEditCommit(event -> + { + final int row = event.getTablePosition().getRow(); + items.set(row, new TitleDetail( + items.get(row).title, + event.getNewValue().replace("\\n", "\n"))); + }); + } + + /** + * Populate the table with an initial list of items. + * The original list is not modified. + * + * @param initial_items Items to display initially. + */ + public void setItems(final List initial_items) { + items.setAll(initial_items); + } + + /** + * @return Current items in the table (empty title+detail rows are removed). + */ + public List getItems() { + items.removeIf(item -> item.title.isEmpty() && item.detail.isEmpty()); + return items; + } + + + public void handleAdd() { + items.add(new TitleDetail("", "")); + + // Start editing the Title cell of the new row after a short delay + UpdateThrottle.TIMER.schedule(() -> + Platform.runLater(() -> + { + final int row = items.size() - 1; + table.getSelectionModel().clearAndSelect(row); + table.edit(row, table.getColumns().get(0)); + }), + 200, TimeUnit.MILLISECONDS); + } + + public void handleEdit() { + final int row = table.getSelectionModel().getSelectedIndex(); + if (row < 0) + return; + + final TitleDetail value = items.get(row); + final MultiLineInputDialog dialog = new MultiLineInputDialog(value.detail); + dialog.setTitle("Detail for '" + value.title + "'"); + DialogHelper.positionDialog(dialog, optionsRoot, -600, -100); + dialog.showAndWait().ifPresent(details -> + items.set(row, new TitleDetail(value.title, details))); + } + + public void handleUp() { + final List idx = + new ArrayList<>(table.getSelectionModel().getSelectedIndices()); + idx.sort((a, b) -> a - b); // ascending + + for (int i : idx) { + final TitleDetail item = items.remove(i); + if (i > 0) { + items.add(i - 1, item); + table.getSelectionModel().clearAndSelect(i - 1); + } else { + // Roll-around: top item wraps to bottom + items.add(item); + table.getSelectionModel().clearAndSelect(items.size() - 1); + } + } + } + + public void handleDown() { + final List idx = + new ArrayList<>(table.getSelectionModel().getSelectedIndices()); + idx.sort((a, b) -> b - a); // descending + + for (int i : idx) { + final TitleDetail item = items.remove(i); + if (i < items.size()) { + items.add(i + 1, item); + table.getSelectionModel().clearAndSelect(i + 1); + } else { + // Roll-around: bottom item wraps to top + items.add(0, item); + table.getSelectionModel().clearAndSelect(0); + } + } + } + + public void handleDelete() { + final List idx = + new ArrayList<>(table.getSelectionModel().getSelectedIndices()); + idx.sort((a, b) -> b - a); // descending — stable indices while removing + + for (int i : idx) + items.remove(i); + } + + public void setTitle(String title) { + titleLabel.setText(title); + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java new file mode 100644 index 0000000000..63b9ddccac --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.ui.config; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; + +/** + * Controller for the buttons controlling the guidance, displays... tables. Action handlers will simply + * forward to methods in a {@link TitleDetailTableController}. + */ +@SuppressWarnings("unused") +public class TitleDetailToolbarController { + + private TitleDetailTableController titleDetailTableController; + + @FXML + private Button addButton; + @FXML + private Button editButton; + @FXML + private Button upButton; + @FXML + private Button downButton; + @FXML + private Button deleteButton; + + public void setTitleDetailTableController(TitleDetailTableController titleDetailTableController) { + this.titleDetailTableController = titleDetailTableController; + } + + @FXML + private void handleAdd() { + titleDetailTableController.handleAdd(); + } + + @FXML + private void handleEdit() { + titleDetailTableController.handleEdit(); + } + + @FXML + private void handleUp() { + titleDetailTableController.handleUp(); + } + + @FXML + private void handleDown() { + titleDetailTableController.handleDown(); + } + + @FXML + private void handleDelete() { + titleDetailTableController.handleDelete(); + } + + public void setButtonStates(int numberOfSelectedItems){ + final boolean nothing = numberOfSelectedItems <= 0; + upButton.setDisable(nothing); + editButton.setDisable(numberOfSelectedItems != 1); + downButton.setDisable(nothing); + deleteButton.setDisable(nothing); + } +} diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml index ec7d0991cb..264e82c5f6 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml @@ -55,54 +55,18 @@ - - - -

+ + + + + + +
+ + diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTable.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTable.fxml new file mode 100644 index 0000000000..ac47304753 --- /dev/null +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTable.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + +
diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailTable.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailTable.fxml new file mode 100644 index 0000000000..615ab62b5d --- /dev/null +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailTable.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + +
diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailToolbar.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailToolbar.fxml new file mode 100644 index 0000000000..5d5e3cf7d8 --- /dev/null +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/TitleDetailToolbar.fxml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties index 4aee77de3b..c2c66a719d 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties @@ -29,11 +29,13 @@ behaviorTooltip=Enable alarms? See also 'Enabling Filter' commands=Commands: configure=Configure dateTimePickerTooltip=Select a date until which the alarm should be disabled +delay=Delay delayTooltip0=Alarms are indicated when they persist for at least this long.\n{0} delayTooltip1=With the current delay of 0 seconds, alarms trigger immediately delayTooltip2=With the current delay of {0}seconds, alarms trigger after {1} hours:minutes:seconds description=Description: descriptionTooltip=Alarm description, also used for annunciation +detail=Detail disabled=Disabled disableAlarmFailed=Failed to disable alarm disableAlarms=Disable Alarms @@ -52,9 +54,11 @@ headerAlreadyEnabled=All PVs in the selected section are already enabled headerConfirmDisable=Disable all PVs in the selected section of the alarm hierarchy?\nThis would disable {0} PVs. headerConfirmDisableWithEnableDate=Disable all PVs in the selected section of the alarm hierarchy until {0}?\nThis would affect {1} PVs. headerConfirmEnable=Enable all PVs in the selected section of the alarm hierarchy?\nThis would enable {0} PVs. +info=Info latch=Latch latchTooltip=Latch alarm until acknowledged? moveItemFailed=Failed to move item +option=Option partlyDisabled=Partly disabled partlyDisabled2=(Partly disabled) path=Path: @@ -64,4 +68,10 @@ relativeDateTooltip=Select a predefined duration for disabling the alarm removeComponentFailed=Failed to remove component renameItemFailed=Failed to rename item timer=Timer +title=Title +tooltipAddTableItem=Add a new table item +tooltipDeleteSelectedItems=Delete selected table items +tooltipMoveEditDetail=Edit the detail field of table item +tooltipMoveTableItemDown=Move table item down +tooltipMoveTableItemUp=Move table item up unacknowledgeFailed=Failed to unacknowledge alarm(s) From f0251d7f467b36b2f55de19e14a907cdf774056d Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 1 Jun 2026 14:48:48 +0200 Subject: [PATCH 7/7] Fix bug, convert rest of alarm config dialog to fxml --- .../ComponentConfigDialogController.java | 2 + .../ui/config/ConfigDialogController.java | 2 - .../alarm/ui/config/DateTimePicker.java | 2 +- .../ui/config/OptionsTablesController.java | 20 + .../TitleDetailDelayTableController.java | 30 +- .../ui/config/TitleDetailTableController.java | 35 +- .../config/TitleDetailToolbarController.java | 4 + .../alarm/ui/tree/TitleDetailDelayTable.java | 451 ------------------ .../alarm/ui/tree/TitleDetailTable.java | 218 --------- .../ui/config/ComponentConfigDialog.fxml | 24 +- .../alarm/ui/config/LeafConfigDialog.fxml | 20 +- .../alarm/TitleDetailTableDemo.java | 93 ++-- 12 files changed, 150 insertions(+), 751 deletions(-) delete mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java delete mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailTable.java diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java index 8df9d1a2d6..55fd9a8f36 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java @@ -103,10 +103,12 @@ public void initialize() { if(disabled.isEmpty()) { enabled.setSelected(true); + itemEnabledProperty.setValue(true); } else if (alarmClientLeaves.size() != disabled.size()) { partlyDisabledLabel.setVisible(true); enabled.setSelected(false); + itemEnabledProperty.setValue(false); } if (!withEnableDate.isEmpty()) { diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java index 874b5f3ca6..58944e389a 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java @@ -24,8 +24,6 @@ import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.ui.Messages; -import org.phoebus.applications.alarm.ui.tree.TitleDetailDelayTable; -import org.phoebus.applications.alarm.ui.tree.TitleDetailTable; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.util.time.TimeParser; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java index cfa68442b5..0bedb633b9 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DateTimePicker.java @@ -46,7 +46,7 @@ public DateTimePicker() { alignColumnCountWithFormat(); getEditor().setTextFormatter(new TextFormatter<>(new InternalConverter())); - // Syncronize changes to the underlying date value back to the dateTimeValue + // Synchronize changes to the underlying date value back to the dateTimeValue valueProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) { dateTimeValue.set(null); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java index 58c33edf03..f155847d3c 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java @@ -36,6 +36,10 @@ public OptionsTablesController(final AlarmTreeItem item) { this.item = item; } + /** + * Handles the dynamic aspects of the {@link TitleDetailTableController}s, i.e. sets a title + * and the {@link AlarmTreeItem} data. + */ public void initialize(){ guidanceViewController.setTitle(Messages.guidance); guidanceViewController.setItems(item.getGuidance()); @@ -47,18 +51,34 @@ public void initialize(){ actionsViewController.setItems(item.getActions()); } + /** + * + * @return The guidance data managed by the user in the {@link javafx.scene.control.TableView} + */ public List getGuidance(){ return guidanceViewController.getItems().stream().map(i -> (TitleDetail)i).toList(); } + /** + * + * @return The displays data managed by the user in the {@link javafx.scene.control.TableView} + */ public List getDisplays(){ return displaysViewController.getItems().stream().map(i -> (TitleDetail)i).toList(); } + /** + * + * @return The commands data managed by the user in the {@link javafx.scene.control.TableView} + */ public List getCommands(){ return commandsViewController.getItems().stream().map(i -> (TitleDetail)i).toList(); } + /** + * + * @return The automated actions data managed by the user in the {@link javafx.scene.control.TableView} + */ public List getActions(){ return actionsViewController.getItems().stream().map(i -> (TitleDetailDelay)i).toList(); } diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java index 301ea59987..0af1e8281d 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailDelayTableController.java @@ -23,9 +23,11 @@ import org.phoebus.applications.alarm.model.TitleDetail; import org.phoebus.applications.alarm.model.TitleDetailDelay; import org.phoebus.applications.alarm.ui.tree.ValidatingTextFieldTableCell; +import org.phoebus.ui.javafx.UpdateThrottle; import java.util.List; import java.util.Objects; +import java.util.concurrent.TimeUnit; /** * FXML controller for TitleDetailDelayTable.fxml. @@ -78,7 +80,9 @@ private enum ActionOption { * Configure table columns particular for this view. */ @Override - public void configureColumns() { + public void configure() { + + table.setItems(items); optionColumn.setCellFactory(ComboBoxTableCell.forTableColumn(ActionOption.values())); optionColumn.setCellValueFactory( @@ -126,6 +130,7 @@ public void configureColumns() { items.get(row).detail, event.getNewValue())); }); + titleColumn.setOnEditCommit(event -> { final int row = event.getTablePosition().getRow(); @@ -193,12 +198,23 @@ public void handleAdd() { items.add(new TitleDetailDelay("", "", 0)); // Trigger editing the title of new item - Platform.runLater(() -> - { - final int row = items.size() - 1; - table.getSelectionModel().clearAndSelect(row); - table.edit(row, table.getColumns().get(0)); - }); + UpdateThrottle.TIMER.schedule(() -> Platform.runLater(() -> + { + final int row = items.size() - 1; + table.getSelectionModel().clearAndSelect(row); + table.edit(row, table.getColumns().get(0)); + }), 200, TimeUnit.MILLISECONDS); + } + + /** + * Populate the table with an initial list of items. + * The original list is not modified. + * + * @param initial_items Items to display initially. + */ + @Override + public void setItems(final List initial_items) { + items.setAll(initial_items.stream().map(i -> (TitleDetailDelay)i).toList()); } // ── DelayTableCell (inner class – unchanged logic) ───────────────────────── diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java index 5834b90aed..63764ae948 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailTableController.java @@ -58,31 +58,34 @@ public class TitleDetailTableController { private final ObservableList items = FXCollections.observableArrayList(); + /** + * Performs initialization common for this controller and {@link TitleDetailDelayTableController}. + */ public void initialize() { titleDetailToolbarViewController.setTitleDetailTableController(this); - - table.setItems(items); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); // Title column titleColumn.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().title)); titleColumn.setCellFactory(ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); - configureColumns(); - final InvalidationListener item_selected = prop -> titleDetailToolbarViewController.setButtonStates(table.getSelectionModel().getSelectedCells().size()); table.getSelectionModel().selectedIndexProperty().addListener(item_selected); // Apply initial state item_selected.invalidated(null); + configure(); + } /** - * Configure table columns particular for this view. + * Configures view for class specific items. */ - public void configureColumns(){ + public void configure(){ + + table.setItems(items); titleColumn.setOnEditCommit(event -> { @@ -129,7 +132,9 @@ public List getItems() { return items; } - + /** + * Adds a new row in the {@link TableView} + */ public void handleAdd() { items.add(new TitleDetail("", "")); @@ -144,6 +149,9 @@ public void handleAdd() { 200, TimeUnit.MILLISECONDS); } + /** + * Prepares selected table row for edit. + */ public void handleEdit() { final int row = table.getSelectionModel().getSelectedIndex(); if (row < 0) @@ -157,6 +165,9 @@ public void handleEdit() { items.set(row, new TitleDetail(value.title, details))); } + /** + * Moves table row up + */ public void handleUp() { final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); @@ -175,6 +186,9 @@ public void handleUp() { } } + /** + * Moves table row down + */ public void handleDown() { final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); @@ -193,6 +207,9 @@ public void handleDown() { } } + /** + * Deletes a row from the {@link TableView} + */ public void handleDelete() { final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); @@ -202,6 +219,10 @@ public void handleDelete() { items.remove(i); } + /** + * Sets the title for the section (guidance, displays...) + * @param title Localized string for the title. + */ public void setTitle(String title) { titleLabel.setText(title); } diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java index 63b9ddccac..f6c44ba88e 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/TitleDetailToolbarController.java @@ -56,6 +56,10 @@ private void handleDelete() { titleDetailTableController.handleDelete(); } + /** + * Configures the button states based on the number of selected items in the {@link javafx.scene.control.TableView}. + * @param numberOfSelectedItems + */ public void setButtonStates(int numberOfSelectedItems){ final boolean nothing = numberOfSelectedItems <= 0; upButton.setDisable(nothing); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java deleted file mode 100644 index 118786ec6b..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java +++ /dev/null @@ -1,451 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2018-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.phoebus.applications.alarm.ui.tree; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import org.phoebus.applications.alarm.model.TitleDetailDelay; -import org.phoebus.applications.alarm.ui.AlarmUI; -import org.phoebus.ui.dialog.DialogHelper; -import org.phoebus.ui.dialog.MultiLineInputDialog; -import org.phoebus.ui.javafx.ImageCache; - -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.geometry.Insets; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.SelectionMode; -import javafx.scene.control.Spinner; -import javafx.scene.control.TableCell; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.control.Tooltip; -import javafx.scene.control.cell.ComboBoxTableCell; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.VBox; -import javafx.util.converter.DefaultStringConverter; - - -/** Table for editing list of {@link TitleDetailDelay} - * - *

Largely based of off {@link TitleDetailTable} - * @author Evan Smith - */ -@SuppressWarnings("nls") -public class TitleDetailDelayTable extends BorderPane -{ - private enum Option_d - { - // Send email with alarm info - mailto, - // Execute external command - cmd, - // Update PV with severity - sevrpv, - // Update PV with alarm info text - infopv - }; - - private final ObservableList items = FXCollections.observableArrayList(); - - private final TableView table = new TableView<>(items); - - private final Button add = new Button("", ImageCache.getImageView(ImageCache.class, "/icons/add.png")), - edit = new Button("", ImageCache.getImageView(AlarmUI.class, "/icons/edit.png")), - up = new Button("", ImageCache.getImageView(AlarmUI.class, "/icons/up.png")), - down = new Button("", ImageCache.getImageView(AlarmUI.class, "/icons/down.png")), - delete = new Button("", ImageCache.getImageView(ImageCache.class, "/icons/delete.png")); - - /** @param initial_items Initial items. Original list will remain unchanged */ - public TitleDetailDelayTable(final List initial_items) - { - items.setAll(initial_items); - - createTable(); - createButtons(); - - final VBox buttons = new VBox(5, add, edit, up, down, delete); - - setCenter(table); - BorderPane.setMargin(buttons, new Insets(0, 5, 0, 5)); - setRight(buttons); - } - - /** @return Items in table */ - public List getItems() - { - // Delete empty items (delay ignored) - items.removeIf(item -> item.title.isEmpty() && item.detail.isEmpty()); - return items; - } - - /** Table cell for 'delay' - * Disables for actions that don't use the delay - */ - class DelayTableCell extends TableCell - { - private final Spinner spinner; - - public DelayTableCell() - { - this.spinner = new Spinner<>(0, 10000, 1); - this.spinner.setEditable(true); - - // disable focus on buttons - spinner.lookupAll(".increment-arrow-button, .decrement-arrow-button") - .forEach(node -> node.setFocusTraversable(false)); - - this.spinner.valueProperty().addListener((obs, oldValue, newValue) -> { - if (isEditing()) { - commitEdit(newValue); - } - }); - - // validate when loosing focus - spinner.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { - if (!isNowFocused) { - Integer currentValue = spinner.getValue(); - Integer oldValue = getItem(); - - if (Objects.equals(currentValue, oldValue)) { - cancelEdit(); - return; - } - - // if not currently editing, force table to enter edit mode first - if (!isEditing()) { - TableView tv = getTableView(); - if (tv != null) { - Platform.runLater(() -> { - tv.getSelectionModel().clearAndSelect(getIndex()); - tv.edit(getIndex(), getTableColumn()); - // commit after we've asked the table to start editing - commitEdit(currentValue); - }); - } else { - // fallback: commit anyway - commitEdit(currentValue); - } - } else { - // normal case - commitEdit(currentValue); - } - } - }); - } - - @Override - public void updateItem(Integer item, boolean empty) - { - super.updateItem(item, empty); - - if (empty || - getTableRow() == null || - getTableRow().getItem() == null || - spinner == null) { - setGraphic(null); - return; - } - if (getTableRow().getItem().hasDelay()) - { - spinner.setDisable(false); - spinner.getEditor().setStyle("-fx-text-inner-color: black;"); - //spinner.getEditor().setTextFill(Color.BLACK); - } - else - { - spinner.setDisable(true); - spinner.getEditor().setStyle("-fx-text-inner-color: lightgray;"); - //spinner.getEditor().setTextFill(Color.LIGHTGRAY); - } - - this.spinner.getValueFactory().setValue(item); - setGraphic(spinner); - // force focus on the textedit not buttons - Platform.runLater(() -> { - if (isEditing()) { - spinner.getEditor().requestFocus(); - spinner.getEditor().end(); - } - }); - } - } - - private void createTable() - { - table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - table.setEditable(true); - table.setPlaceholder(new Label("none")); - - TableColumn col = new TableColumn<>("Title"); - col.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().title)); - col.setCellFactory(ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); - col.setOnEditCommit(event -> - { - final int row = event.getTablePosition().getRow(); - items.set(row, new TitleDetailDelay(event.getNewValue(), items.get(row).detail, items.get(row).delay)); - - // Trigger editing the detail - Platform.runLater(() -> { - TableColumn detailCol = table.getColumns().get(1); - TableColumn optionCol = detailCol.getColumns().get(0); - table.getSelectionModel().clearAndSelect(row); - table.edit(row, optionCol); - }); - }); - col.setSortable(false); - table.getColumns().add(col); - - col = new TableColumn<>("Detail"); - col.setSortable(false); - table.getColumns().add(col); - - // Use a combo box to specified the action - TableColumn tmpOptionCol = new TableColumn<>("Option"); - tmpOptionCol.setCellFactory(ComboBoxTableCell.forTableColumn(Option_d.values())); - tmpOptionCol - .setCellValueFactory(cell -> new SimpleObjectProperty(getOptionFromDetail(cell.getValue()))); - tmpOptionCol.setOnEditCommit(edit -> { - final int row = edit.getTablePosition().getRow(); - TitleDetailDelay tmpT = items.get(row); - Option_d option = edit.getNewValue(); - TitleDetailDelay newTitleDetailDelay = setOptionToDetail(tmpT, option); - items.set(row, newTitleDetailDelay); - // Trigger editing the delay. - if (newTitleDetailDelay.hasDelay()) - Platform.runLater(() -> { - TableColumn detailCol = table.getColumns().get(1); - TableColumn infoCol = detailCol.getColumns().get(1); - table.getSelectionModel().clearAndSelect(row); - table.edit(row, infoCol); - }); - }); - tmpOptionCol.setEditable(true); - col.getColumns().add(tmpOptionCol); - - // Use a textfield to set info for detail - TableColumn infoCol = new TableColumn<>("Info"); - infoCol.setCellValueFactory(cell -> new SimpleStringProperty(getInfoFromDetail(cell.getValue()))); - infoCol.setCellFactory(ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); - infoCol.setOnEditCommit(event -> { - final int row = event.getTablePosition().getRow(); - TitleDetailDelay tmpT = items.get(row); - String newInfo = event.getNewValue(); - TitleDetailDelay newTitleDetailDelay = setInfoToDetail(tmpT, newInfo); - items.set(row, newTitleDetailDelay); - // Trigger editing the delay. - if (newTitleDetailDelay.hasDelay()) - Platform.runLater(() -> { - table.getSelectionModel().clearAndSelect(row); - table.edit(row, table.getColumns().get(2)); - }); - }); - infoCol.setSortable(false); - col.getColumns().add(infoCol); - - TableColumn delayCol = new TableColumn<>("Delay"); - delayCol.setCellValueFactory(cell -> new SimpleIntegerProperty(cell.getValue().delay).asObject()); - delayCol.setCellFactory(column -> new DelayTableCell()); - delayCol.setOnEditCommit(event -> - { - final int row = event.getTablePosition().getRow(); - items.set(row, new TitleDetailDelay(items.get(row).title, items.get(row).detail, event.getNewValue())); - }); - delayCol.setSortable(false); - table.getColumns().add(delayCol); - } - - /** - * This function extracts the option from detail "option:info" - * - * @param titleDetailDelay - * @return enum Option_d (mailto, cmd, sevrpv) - */ - private Option_d getOptionFromDetail(final TitleDetailDelay titleDetailDelay) - { - if (titleDetailDelay == null) - return null; - - final int sep = titleDetailDelay.detail.indexOf(':'); - if (sep < 0) - return Option_d.mailto; - - try - { - return Option_d.valueOf(titleDetailDelay.detail.substring(0, sep)); - } - catch (Exception e) - { - return Option_d.mailto; - } - } - - /** - * This function extracts the info from detail "option:info" - * - * @param titleDetailDelay - * @return information eg : mail, command, PV - */ - private String getInfoFromDetail(final TitleDetailDelay titleDetailDelay) - { - if (titleDetailDelay == null) - return ""; - - final int sep = titleDetailDelay.detail.indexOf(':'); - if (sep < 0) - return ""; - - return titleDetailDelay.detail.substring(sep+1); - } - - /** - * Create a new TitleDetailDelay from a given option - * @param titleDetailDelay - * @param option - * @return new TitleDetailDelay - */ - private TitleDetailDelay setOptionToDetail(TitleDetailDelay titleDetailDelay, Option_d option) { - TitleDetailDelay newTitleDetailDelay = titleDetailDelay; - if (titleDetailDelay != null && option != null) { - String info = getInfoFromDetail(titleDetailDelay); - String detail = option.toString() + ":" + info; - newTitleDetailDelay = new TitleDetailDelay(titleDetailDelay.title, detail, titleDetailDelay.delay); - } - return newTitleDetailDelay; - } - - /** - * Create a new TitleDetailDelay from a given info - * @param titleDetailDelay - * @param option - * @return new TitleDetailDelay - */ - private TitleDetailDelay setInfoToDetail(TitleDetailDelay titleDetailDelay, String info) { - TitleDetailDelay newTitleDetailDelay = titleDetailDelay; - if (titleDetailDelay != null && info != null) { - Option_d option = getOptionFromDetail(titleDetailDelay); - String newInfo = info.replace("\\n", "\n"); - String detail = option.toString() + ":" + newInfo; - newTitleDetailDelay = new TitleDetailDelay(titleDetailDelay.title, detail, titleDetailDelay.delay); - } - return newTitleDetailDelay; - } - - private void createButtons() - { - add.setTooltip(new Tooltip("Add a new table item.")); - add.setOnAction(event -> - { - items.add(new TitleDetailDelay("", "", 0)); - - // Trigger editing the title of new item - Platform.runLater(() -> - { - final int row = items.size()-1; - table.getSelectionModel().clearAndSelect(row); - table.edit(row, table.getColumns().get(0)); - }); - }); - - edit.setTooltip(new Tooltip("Edit the detail field of table item.")); - edit.setOnAction(event -> - { - final int row = table.getSelectionModel().getSelectedIndex(); - - final TitleDetailDelay value = items.get(row); - final MultiLineInputDialog dialog = new MultiLineInputDialog(value.detail); - dialog.setTitle("Detail for '" + value.title + "'"); - DialogHelper.positionDialog(dialog, edit, -600, -100); - dialog.showAndWait().ifPresent(details -> - { - items.set(row, new TitleDetailDelay(value.title, details, value.delay)); - }); - }); - - up.setTooltip(new Tooltip("Move table item up.")); - up.setOnAction(event -> - { - final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); - idx.sort((a,b) -> a - b); - // Starting at top, move each item 'up' - for (int i : idx) - { - final TitleDetailDelay item = items.remove(i); - // Roll around, item from top moves 'up' by adding back to end - if (i > 0) - { - items.add(i-1, item); - table.getSelectionModel().clearAndSelect(i-1); - } - else - { - items.add(item); - table.getSelectionModel().clearAndSelect(items.size()-1); - } - } - }); - - down.setTooltip(new Tooltip("Move table item down.")); - down.setOnAction(event -> - { - final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); - // Descending sort - idx.sort((a,b) -> b - a); - // Starting at bottom, move each item 'down' - for (int i : idx) - { - final TitleDetailDelay item = items.remove(i); - // Roll around, item from top moves 'up' by adding back to end - if (i < items.size()) - { - items.add(i+1, item); - table.getSelectionModel().clearAndSelect(i+1); - } - else - { - items.add(0, item); - table.getSelectionModel().clearAndSelect(0); - } - } - }); - - delete.setTooltip(new Tooltip("Delete selected table items.")); - delete.setOnAction(event -> - { - final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); - // Descending sort - idx.sort((a,b) -> b - a); - // Delete from the bottom of the list so that remaining item indices remain unchanged - for (int i : idx) - items.remove(i); - }); - - // Disable options when nothing is selected to edit, move or delete - final InvalidationListener item_selected = prop -> - { - final int size = table.getSelectionModel().getSelectedCells().size(); - final boolean nothing = size <= 0; - up.setDisable(nothing); - edit.setDisable(size != 1); - down.setDisable(nothing); - delete.setDisable(nothing); - }; - table.getSelectionModel().selectedIndexProperty().addListener(item_selected); - // Initial enable/disable - item_selected.invalidated(null); - } -} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailTable.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailTable.java deleted file mode 100644 index 577cc20852..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailTable.java +++ /dev/null @@ -1,218 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2018 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.phoebus.applications.alarm.ui.tree; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.phoebus.applications.alarm.model.TitleDetail; -import org.phoebus.applications.alarm.ui.AlarmUI; -import org.phoebus.ui.dialog.DialogHelper; -import org.phoebus.ui.dialog.MultiLineInputDialog; -import org.phoebus.ui.javafx.ImageCache; -import org.phoebus.ui.javafx.UpdateThrottle; - -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.geometry.Insets; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.SelectionMode; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.control.Tooltip; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.VBox; -import javafx.util.converter.DefaultStringConverter; - -/** Table for editing list of {@link TitleDetail} - * @author Kay Kasemir - */ -@SuppressWarnings("nls") -public class TitleDetailTable extends BorderPane -{ - private final ObservableList items = FXCollections.observableArrayList(); - - private final TableView table = new TableView<>(items); - - private final Button add = new Button("", ImageCache.getImageView(ImageCache.class, "/icons/add.png")), - edit = new Button("", ImageCache.getImageView(AlarmUI.class, "/icons/edit.png")), - up = new Button("", ImageCache.getImageView(AlarmUI.class, "/icons/up.png")), - down = new Button("", ImageCache.getImageView(AlarmUI.class, "/icons/down.png")), - delete = new Button("", ImageCache.getImageView(ImageCache.class, "/icons/delete.png")); - - /** @param initial_items Initial items. Original list will remain unchanged */ - public TitleDetailTable(final List initial_items) - { - items.setAll(initial_items); - - createTable(); - createButtons(); - - final VBox buttons = new VBox(5, add, edit, up, down, delete); - - setCenter(table); - BorderPane.setMargin(buttons, new Insets(0, 5, 0, 5)); - setRight(buttons); - } - - /** @return Items in table */ - public List getItems() - { - // Delete empty items - items.removeIf(item -> item.title.isEmpty() && item.detail.isEmpty()); - return items; - } - - private void createTable() - { - table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - table.setEditable(true); - table.setPlaceholder(new Label("none")); - - TableColumn col = new TableColumn<>("Title"); - col.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().title)); - col.setCellFactory(ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); - col.setOnEditCommit(event -> - { - final int row = event.getTablePosition().getRow(); - items.set(row, new TitleDetail(event.getNewValue(), items.get(row).detail)); - - // Trigger editing the detail - Platform.runLater(() -> - { - table.getSelectionModel().clearAndSelect(row); - table.edit(row, table.getColumns().get(1)); - }); - }); - col.setSortable(false); - table.getColumns().add(col); - - col = new TableColumn<>("Detail"); - col.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().detail.replace("\n", "\\n"))); - col.setCellFactory(ValidatingTextFieldTableCell.forTableColumn(new DefaultStringConverter())); - col.setOnEditCommit(event -> - { - final int row = event.getTablePosition().getRow(); - items.set(row, new TitleDetail(items.get(row).title, event.getNewValue().replace("\\n", "\n"))); - }); - col.setSortable(false); - table.getColumns().add(col); - } - - private void createButtons() - { - add.setTooltip(new Tooltip("Add a new table item.")); - add.setOnAction(event -> - { - items.add(new TitleDetail("", "")); - - // Trigger editing the title of new item - UpdateThrottle.TIMER.schedule(() -> - Platform.runLater(() -> - { - final int row = items.size()-1; - table.getSelectionModel().clearAndSelect(row); - table.edit(row, table.getColumns().get(0)); - }), - 200, TimeUnit.MILLISECONDS); - }); - - edit.setTooltip(new Tooltip("Edit the detail field of table item.")); - edit.setOnAction(event -> - { - final int row = table.getSelectionModel().getSelectedIndex(); - - final TitleDetail value = items.get(row); - final MultiLineInputDialog dialog = new MultiLineInputDialog(value.detail); - dialog.setTitle("Detail for '" + value.title + "'"); - DialogHelper.positionDialog(dialog, edit, -600, -100); - dialog.showAndWait().ifPresent(details -> - { - items.set(row, new TitleDetail(value.title, details)); - }); - }); - - up.setTooltip(new Tooltip("Move table item up.")); - up.setOnAction(event -> - { - final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); - idx.sort((a,b) -> a - b); - // Starting at top, move each item 'up' - for (int i : idx) - { - final TitleDetail item = items.remove(i); - // Roll around, item from top moves 'up' by adding back to end - if (i > 0) - { - items.add(i-1, item); - table.getSelectionModel().clearAndSelect(i-1); - } - else - { - items.add(item); - table.getSelectionModel().clearAndSelect(items.size()-1); - } - } - }); - - down.setTooltip(new Tooltip("Move table item down.")); - down.setOnAction(event -> - { - final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); - // Descending sort - idx.sort((a,b) -> b - a); - // Starting at bottom, move each item 'down' - for (int i : idx) - { - final TitleDetail item = items.remove(i); - // Roll around, item from top moves 'up' by adding back to end - if (i < items.size()) - { - items.add(i+1, item); - table.getSelectionModel().clearAndSelect(i+1); - } - else - { - items.add(0, item); - table.getSelectionModel().clearAndSelect(0); - } - } - }); - - delete.setTooltip(new Tooltip("Delete selected table items.")); - delete.setOnAction(event -> - { - final List idx = new ArrayList<>(table.getSelectionModel().getSelectedIndices()); - // Descending sort - idx.sort((a,b) -> b - a); - // Delete from the bottom of the list so that remaining item indices remain unchanged - for (int i : idx) - items.remove(i); - }); - - // Disable options when nothing is selected to edit, move or delete - final InvalidationListener item_selected = prop -> - { - final int size = table.getSelectionModel().getSelectedCells().size(); - final boolean nothing = size <= 0; - up.setDisable(nothing); - edit.setDisable(size != 1); - down.setDisable(nothing); - delete.setDisable(nothing); - }; - table.getSelectionModel().selectedIndexProperty().addListener(item_selected); - // Initial enable/disable - item_selected.invalidated(null); - } -} diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml index 264e82c5f6..b983bec3ae 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml @@ -25,9 +25,9 @@