diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..997504b465 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/.gitignore b/.gitignore index 3d835da330..96e1d2c3e3 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ docs/source/_ext/**/*.pyc # Ignore pip installation metadata docs/phoebus_docs.egg-info/ *.pyc +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/app/alarm/ui/doc/images/configuration_editor_dialogs.png b/app/alarm/ui/doc/images/configuration_editor_dialogs.png new file mode 100644 index 0000000000..adb12ecc53 Binary files /dev/null and b/app/alarm/ui/doc/images/configuration_editor_dialogs.png differ diff --git a/app/alarm/ui/doc/images/context_menu_disable.png b/app/alarm/ui/doc/images/context_menu_disable.png new file mode 100644 index 0000000000..7f4baefff3 Binary files /dev/null and b/app/alarm/ui/doc/images/context_menu_disable.png differ diff --git a/app/alarm/ui/doc/images/context_menu_new.png b/app/alarm/ui/doc/images/context_menu_new.png new file mode 100644 index 0000000000..668cb79ea7 Binary files /dev/null and b/app/alarm/ui/doc/images/context_menu_new.png differ diff --git a/app/alarm/ui/doc/images/dialog_enable_date.png b/app/alarm/ui/doc/images/dialog_enable_date.png new file mode 100644 index 0000000000..77578fa3d2 Binary files /dev/null and b/app/alarm/ui/doc/images/dialog_enable_date.png differ diff --git a/app/alarm/ui/doc/index.rst b/app/alarm/ui/doc/index.rst index 4fa8680f00..1ee3290cd9 100644 --- a/app/alarm/ui/doc/index.rst +++ b/app/alarm/ui/doc/index.rst @@ -459,13 +459,32 @@ Context Menu In the Alarm Tree and the Alarm Table views user may right click on an alarm item to launch a context menu: -.. image:: images/context_menu.png +.. image:: images/context_menu_new.png :width: 20% The top item ("Guidance" in the screenshot) will launch a dialog showing the guidance text for the alarm item. -With the Disable Alarms menu item (Alarm Tree only) user may disable an alarm item, or all alarms in the sub-tree of a -node. The alarm(s) will stay disabled until explicitly enabled by the user. See also notes below for +.. image:: images/context_menu_disable.png + :width: 35% + +With the Disable... menu item (Alarm Tree only) user may disable an alarm item, or all alarms in the sub-tree of a +node. +When choosing to disable the alarm(s) *indefinitely* in the sub-context menu, the alarm(s) will stay disabled until +explicitly enabled by the user. + +Alarm(s) can also be disabled for a certain time choosing *with enable date*. + +.. image:: images/dialog_enable_date.png + :width: 40% + +Users can specify an either absolute or relative date or time the alarm(s) should be disabled. After that, the alarm(s) +will be enabled again automatically. + +**NOTE**: A structural component, along with all the items in its sub-tree, can **only** be disabled with an enable date +if either all of the items have the same enable date or none at all. +If this is not the case, the *with enable date* option in the sub context menu will be disabled. + +See also notes below for more information on how disabled alarms are handled upon import of the alarm configuration XML file. Configuration Editor @@ -479,8 +498,13 @@ editor dialog: In this view user may update all settings defined in the alarm configuration XML file: -.. image:: images/configuration_editor.png - :width: 50% +**NOTE:** There are different configuration files for a structural component (path), containing several components +or leaves, and a leaf itself (training:COUNTER), as their functionalities vary + + +.. image:: images/configuration_editor_dialogs.png + :width: 85% + **NOTE:** Any changes performed in the editor will be overwritten when the associated alarm configuration XML file is imported again. 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 4c622d57ae..19a8c1f2c0 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 @@ -24,6 +24,7 @@ public class Messages public static String disableAlarmFailed; public static String disableAlarms; public static String disabled; + public static String disableMenu; public static String disabledUntil; public static String displays; public static String enableAlarmFailed; @@ -34,6 +35,7 @@ public class Messages public static String headerConfirmDisable; public static String headerConfirmDisableWithEnableDate; public static String headerConfirmEnable; + public static String indefinitely; public static String moveItemFailed; public static String partlyDisabled; public static String promptTitle; @@ -42,6 +44,7 @@ public class Messages public static String renameItemFailed; public static String timer; public static String unacknowledgeFailed; + public static String withEnableDate; static { 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 deleted file mode 100644 index 55fd9a8f36..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ComponentConfigDialogController.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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); - itemEnabledProperty.setValue(true); - } - else if (alarmClientLeaves.size() != disabled.size()) { - partlyDisabledLabel.setVisible(true); - enabled.setSelected(false); - itemEnabledProperty.setValue(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; - } - - alarmTreeItem.setGuidance(optionsTablesViewController.getGuidance()); - alarmTreeItem.setDisplays(optionsTablesViewController.getDisplays()); - alarmTreeItem.setCommands(optionsTablesViewController.getCommands()); - alarmTreeItem.setActions(optionsTablesViewController.getActions()); - - 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 index 58944e389a..b0ab443b1d 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 @@ -4,35 +4,15 @@ 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.ui.dialog.DialogHelper; -import org.phoebus.util.time.TimeParser; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeParseException; -import java.time.temporal.TemporalAmount; - -public abstract class ConfigDialogController { +public class ConfigDialogController { @SuppressWarnings("unused") @FXML @@ -46,32 +26,6 @@ public abstract class ConfigDialogController { @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; @FXML protected OptionsTablesController optionsTablesViewController; @@ -79,10 +33,6 @@ public abstract class ConfigDialogController { 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; @@ -94,115 +44,22 @@ public void initialize() { path.setText(alarmTreeItem.getPathName()); - 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-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()); - } + public void validateAndStore(){ + alarmTreeItem.setGuidance(optionsTablesViewController.getGuidance()); + alarmTreeItem.setDisplays(optionsTablesViewController.getDisplays()); + alarmTreeItem.setCommands(optionsTablesViewController.getCommands()); + alarmTreeItem.setActions(optionsTablesViewController.getActions()); - /** - * 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(); + try { + alarmClient.sendItemConfigurationUpdate(alarmTreeItem.getPathName(), alarmTreeItem); + } catch (Exception ex) { + ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); + } } - - 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 0bedb633b9..c8665f2a08 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 @@ -108,6 +108,7 @@ private ObjectProperty formatProperty() { return format; } + // removed "private" here private void setFormat(String format) { this.format.set(format); alignColumnCountWithFormat(); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DisableUntilDialogController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DisableUntilDialogController.java new file mode 100644 index 0000000000..5cb37b53d3 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/DisableUntilDialogController.java @@ -0,0 +1,142 @@ +package org.phoebus.applications.alarm.ui.config; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.paint.Color; +import javafx.util.StringConverter; +import org.phoebus.applications.alarm.AlarmSystem; +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 class DisableUntilDialogController { + + @FXML + private DateTimePicker enabledDatePicker; + + @FXML + private ComboBox relativeDate; + + @FXML + private Label invalidDate; + + + protected final SimpleStringProperty relativeDateProperty = new SimpleStringProperty(null); + protected final SimpleObjectProperty enableDateProperty = new SimpleObjectProperty<>(); + protected final BooleanProperty invalidDateProperty = new SimpleBooleanProperty(); + + public void initialize(){ + invalidDate.setTextFill(Color.RED); + relativeDate.valueProperty().bindBidirectional(relativeDateProperty); + enabledDatePicker.dateTimeValueProperty().bindBidirectional(enableDateProperty); + invalidDate.visibleProperty().bind(invalidDateProperty); + enableDateProperty.addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + relativeDateProperty.setValue(null); + } + }); + + + enabledDatePicker.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue && invalidDateProperty.get()){ + invalidDateProperty.set(false); + } + }); + + relativeDate.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue && invalidDateProperty.get()){ + invalidDateProperty.set(false); + } + }); + + relativeDateProperty.addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + enableDateProperty.setValue(null); + } + }); + + enabledDatePicker.setDayCellFactory(picker -> new DateCell() { + @Override + public void updateItem(LocalDate date, boolean empty) { + super.updateItem(date, empty); + setDisable(empty || date.isBefore(LocalDate.now())); + } + }); + + 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(); + } + } + }); + + + 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); + + + } + + public void setDefaultDate(LocalDateTime date){ + enableDateProperty.set(date); + } + + + /** + * @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()); + } + + + /** + * 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. + */ + public LocalDateTime determineEnableDate() { + + if (enableDateProperty.isNotNull().get()) { + if (isEnableDateValid(enableDateProperty.get())) { + return enableDateProperty.get(); + } else { + invalidDateProperty.set(true); + 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 { + invalidDateProperty.set(true); + throw new IllegalArgumentException("Enable date invalid"); + } + } + return null; + } +} 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 719a171a3e..6ba3ea814c 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 @@ -44,16 +44,21 @@ public ItemConfigDialog(final AlarmClient model, final AlarmTreeItem item) { fxmlLoader.setLocation(this.getClass().getResource("LeafConfigDialog.fxml")); } else{ - fxmlLoader.setLocation(this.getClass().getResource("ComponentConfigDialog.fxml")); + // renames component config dialog fxml to config dialig + fxmlLoader.setLocation(this.getClass().getResource("ConfigDialog.fxml")); } fxmlLoader.setControllerFactory(clazz -> { try { if(clazz.isAssignableFrom(LeafConfigDialogController.class)) { return clazz.getConstructor(AlarmClient.class, AlarmTreeItem.class).newInstance(model, item); } - if(clazz.isAssignableFrom(ComponentConfigDialogController.class)) { + // GEORG is that ok? + if(clazz.isAssignableFrom(ConfigDialogController.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(); } 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 59226d8275..0f85f7c788 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 @@ -9,28 +9,22 @@ 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.control.*; import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; 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 @@ -61,6 +55,23 @@ public class LeafConfigDialogController extends ConfigDialogController { @FXML private TextField filter; + @FXML + protected CheckBox enabled; + + @SuppressWarnings("unused") + @FXML + private StackPane guidancePlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane displaysPlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane commandsPlaceholder; + @SuppressWarnings("unused") + @FXML + private StackPane actionsPlaceholder; + + private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(""); private final SimpleBooleanProperty latchingProperty = new SimpleBooleanProperty(); private final SimpleBooleanProperty annunciatingProperty = new SimpleBooleanProperty(); @@ -69,6 +80,9 @@ public class LeafConfigDialogController extends ConfigDialogController { private SpinnerValueFactory countValueFactory; private SpinnerValueFactory delayValueFactory; + protected final SimpleBooleanProperty itemEnabledProperty = new SimpleBooleanProperty(); + + public LeafConfigDialogController(AlarmClient alarmClient, AlarmTreeItem alarmTreeItem) { super(alarmClient, alarmTreeItem); } @@ -84,8 +98,12 @@ public void initialize() { annunciating.selectedProperty().bindBidirectional(annunciatingProperty); filter.textProperty().bindBidirectional(enablingFilterProperty); - relativeDate.disableProperty().bind(itemEnabledProperty.not()); - enabledDatePicker.disableProperty().bind(itemEnabledProperty.not()); + + + enabled.setOnAction(e -> { + itemEnabledProperty.setValue(enabled.isSelected()); + }); + AlarmClientLeaf leaf = (AlarmClientLeaf) alarmTreeItem; @@ -117,7 +135,6 @@ public void initialize() { descriptionProperty.set(leaf.getDescription()); enablingFilterProperty.set(leaf.getFilter()); - enableDateProperty.set(leaf.getEnabledDate()); // Behavior checkboxes itemEnabledProperty.setValue(leaf.isEnabled()); @@ -125,9 +142,9 @@ public void initialize() { latchingProperty.setValue(leaf.isLatching()); annunciatingProperty.setValue(leaf.isAnnunciating()); + BooleanBinding binding = Bindings.createBooleanBinding(() -> - itemEnabledProperty.not().get() || relativeDateProperty.isNotNull().get() || enableDateProperty.isNotNull().get(), - itemEnabledProperty, relativeDateProperty, enableDateProperty); + itemEnabledProperty.not().get(), itemEnabledProperty); latching.disableProperty().bind(binding); annunciating.disableProperty().bind(binding); @@ -162,21 +179,9 @@ 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.setEnabled(itemEnabledProperty.get()); pv.setLatching(latchingProperty.get()); pv.setAnnunciating(annunciatingProperty.get()); pv.setDelay(delayValueFactory.getValue()); 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 dac36bff57..f3d65c9801 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 @@ -617,8 +617,9 @@ else if (selection.size() == 1) if (selection.size() >= 1) { menu_items.add(new EnableComponentAction(tree_view, model, selection)); - menu_items.add(new DisableComponentAction(tree_view, model, selection)); + menu_items.add(new DisableAction(tree_view, model, selection)); menu_items.add(new RemoveComponentAction(tree_view, model, selection)); + } } diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/DisableAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/DisableAction.java new file mode 100644 index 0000000000..01445f4d9f --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/DisableAction.java @@ -0,0 +1,190 @@ +package org.phoebus.applications.alarm.ui.tree; + +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.*; +import javafx.scene.layout.GridPane; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.ui.AlarmUI; +import org.phoebus.applications.alarm.ui.config.DisableUntilDialogController; +import org.phoebus.framework.jobs.JobManager; +import org.phoebus.framework.nls.NLS; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.ui.javafx.ImageCache; + +import javafx.scene.Node; + +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.model.AlarmTreeItem; + +import org.phoebus.applications.alarm.ui.Messages; + +import java.io.IOException; +import java.text.MessageFormat; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +public class DisableAction extends Menu { + + private AlarmClient alarmClient; + + public DisableAction(final Node node, final AlarmClient model, final List> items) { + this.alarmClient = model; + setText(Messages.disableMenu); + setGraphic(ImageCache.getImageView(AlarmUI.class, "/icons/disabled.png")); + MenuItem disable = new DisableComponentAction(node, model, items); + disable.setText(Messages.indefinitely); + MenuItem disableUntil = new MenuItem(Messages.withEnableDate); + disableUntil.setDisable(true); + Set totalLeafItems = new HashSet<>(); + Set leafItemsWithEnableDate = new HashSet<>(); + setOnShowing(e -> { + + new Thread(() -> { + if (checkEnableDates(items, totalLeafItems, leafItemsWithEnableDate)) { + Platform.runLater(() -> disableUntil.setDisable(false)); + } + + }).start(); + + + }); + disableUntil.setOnAction(e -> { + final FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(NLS.getMessages(Messages.class)); + fxmlLoader.setLocation(DisableUntilDialogController.class.getResource("DisableUntilDialog.fxml")); + + final GridPane root; + try { + root = fxmlLoader.load(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + DisableUntilDialogController dialogController = fxmlLoader.getController(); + + + if (!leafItemsWithEnableDate.isEmpty()){ + LocalDateTime defaultDate = leafItemsWithEnableDate.iterator().next().getEnabledDate(); + dialogController.setDefaultDate(defaultDate); + } + + + final Dialog dlg = new Dialog<>(); + dlg.setTitle("Disable until"); + dlg.getDialogPane().setContent(root); + dlg.getDialogPane().getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK); + dlg.setResultConverter(button -> { + if (button.equals(ButtonType.OK)) { + return dialogController.determineEnableDate(); + } + return null; + }); + Optional localDateTime = dlg.showAndWait(); + if (localDateTime.isPresent()) { + updateEnablement(localDateTime.get(), totalLeafItems); + System.out.println(localDateTime.get()); + } + + }); + + getItems().addAll(disable, disableUntil); + } + + /** + * Divides items the user clicked on in leaf items and non leaf items + * Returns true when all leaf items of the same structure either have no enable dates or all the same + * Returns false if the enable dates differ + * + * @param items Root item + * @param totalLeafItems {@link Set} that will hold all leaf nodes + * @param leafItemsWithEnableDate {@link Set} that will hold all leaf nodes with non-null enable date + * + */ + + public static boolean checkEnableDates(final List> items, Set totalLeafItems, Set leafItemsWithEnableDate) { + Set> nonLeafItems = + items.stream().filter(i -> !(i instanceof AlarmClientLeaf)).collect(Collectors.toSet()); + Set> leafItems = + items.stream().filter(i -> (i instanceof AlarmClientLeaf)).collect(Collectors.toSet()); + nonLeafItems.forEach(i -> findAffectedPVs(i, totalLeafItems, leafItemsWithEnableDate)); + leafItems.forEach(i -> findAffectedPVs(i, totalLeafItems, leafItemsWithEnableDate)); + if (leafItemsWithEnableDate.isEmpty()) { + return true; + } else if (totalLeafItems.size() != leafItemsWithEnableDate.size()) { + return false; + } else { + LocalDateTime firstDate = leafItemsWithEnableDate.iterator().next().getEnabledDate(); + for (AlarmClientLeaf alarmClientLeaf : totalLeafItems) { + LocalDateTime currDate = alarmClientLeaf.getEnabledDate(); + if (!firstDate.equals(currDate)) { + return false; + } + } + return true; + } + } + + + /** + * Recursively counts alarm tree items in a subtree to find total number and + * number of disabled with enable date. + * + * @param item Root item + * @param total {@link Set} that will hold all leaf nodes + * @param withEnableDate {@link Set} that will hold all leaf nodes with non-null enable date + * + */ + public static void findAffectedPVs(final AlarmTreeItem item, final Set total, final Set withEnableDate) { + if (item instanceof AlarmClientLeaf) { + final AlarmClientLeaf pv = (AlarmClientLeaf) item; + total.add(pv); + if (pv.getEnabledDate() != null) { + withEnableDate.add(pv); + } + } else { + for (AlarmTreeItem sub : item.getChildren()) { + findAffectedPVs(sub, total, withEnableDate); + } + } + } + + + /** + * 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, Set totalLeafItems) { + if (totalLeafItems.isEmpty()) { + return; + } + if (totalLeafItems.size() > 1) { + final Alert dialog = new Alert(Alert.AlertType.CONFIRMATION); + dialog.setTitle(Messages.disableAlarms); + dialog.setHeaderText(MessageFormat.format(Messages.headerConfirmDisableWithEnableDate, enableDate, totalLeafItems.size())); + + if (dialog.showAndWait().get() != ButtonType.OK) { + return; + } + } + + JobManager.schedule(Messages.disableAlarms, monitor -> + { + for (AlarmClientLeaf pv : totalLeafItems) { + 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; + } + } + } + }); + } +} 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 625d3513e0..e5a8d0b96a 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 @@ -8,6 +8,7 @@ package org.phoebus.applications.alarm.ui.tree; import javafx.scene.Node; +import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.model.AlarmTreeItem; @@ -30,12 +31,12 @@ class EnableComponentAction extends MenuItem { * @param items Items to enable */ 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(Messages.disableAlarms); - setGraphic(ImageCache.getImageView(AlarmUI.class, "/icons/disabled.png")); } setOnAction(event -> ComponentActionHelper.updateEnablement(node, model, items, doEnable())); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/RemoveComponentAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/RemoveComponentAction.java index 8844ab90c7..20b82468c1 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/RemoveComponentAction.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/RemoveComponentAction.java @@ -10,6 +10,7 @@ import java.util.List; import javafx.application.Platform; +import javafx.scene.control.*; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.ui.Messages; @@ -19,11 +20,7 @@ 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 javafx.scene.control.TreeView; /** Action that deletes item from the alarm tree configuration * @author Kay Kasemir 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/ConfigDialog.fxml similarity index 53% rename from app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ComponentConfigDialog.fxml rename to app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ConfigDialog.fxml index b983bec3ae..f16d936ca5 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/ConfigDialog.fxml @@ -3,9 +3,8 @@ - - + @@ -25,33 +24,6 @@