diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java index 34f5969cd6..a5fff2a790 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java @@ -307,6 +307,11 @@ private void handleUpdate(final ConsumerRecord record) { for (final AlarmClientListener listener : listeners) listener.itemRemoved(node); } + } else if (JsonModelReader.isConfigDeletion(json)) { + // Ignore config deletion messages; these can cause spurious updates to configs and hence + // spurious calls to itemUpdated + logger.log(Level.FINE, () -> "Ignoring tombstone config message: " + record.key() + " " + node_config); + return; } else { // Configuration update if (JsonModelReader.isStateUpdate(json)) logger.log(Level.WARNING, "Got config update with state content: " + record.key() + " " + node_config); diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientLeaf.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientLeaf.java index 2c0260bfe9..13f09c5e09 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientLeaf.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientLeaf.java @@ -129,10 +129,11 @@ public boolean setEnabled(final EnabledState enabled_state) public boolean setEnabledDate(final LocalDateTime enabled_date) { final EnabledState new_enabled_state = new EnabledState(enabled_date); - if ((enabled.equals(new_enabled_state)) || enabled_date.isBefore(LocalDateTime.now()) || enabled_date.isEqual(LocalDateTime.now())) { + if ((enabled.equals(new_enabled_state)) + || enabled_date != null && enabled_date.isBefore(LocalDateTime.now()) + || enabled_date != null && enabled_date.isEqual(LocalDateTime.now())) { return false; } - setEnabled(false); enabled = new_enabled_state; return true; } diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientNode.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientNode.java index e5ec601250..592ea56170 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientNode.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClientNode.java @@ -7,6 +7,7 @@ *******************************************************************************/ package org.phoebus.applications.alarm.client; +import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; import org.phoebus.applications.alarm.model.AlarmTreeItemWithState; @@ -27,4 +28,17 @@ public AlarmClientNode(final String parent_path, final String name) super(parent_path, name, new CopyOnWriteArrayList<>()); state = new BasicState(SeverityLevel.OK); } + + /** @return {@link AlarmClientNode} with same configuration, but no parent or children */ + public AlarmClientNode createDetachedCopy() + { + final AlarmClientNode node = new AlarmClientNode(null, getName()); + + node.setGuidance(new ArrayList<>(getGuidance())); + node.setDisplays(new ArrayList<>(getDisplays())); + node.setCommands(new ArrayList<>(getCommands())); + node.setActions(new ArrayList<>(getActions())); + + return node; + } } diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/AlarmTreeItem.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/AlarmTreeItem.java index 31648c2f1b..21868f25d7 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/AlarmTreeItem.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/AlarmTreeItem.java @@ -11,9 +11,12 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.client.AlarmClientNode; import org.phoebus.util.text.CompareNatural; /** Base class for all nodes in the alarm tree @@ -232,4 +235,32 @@ public String toString() { return getName(); } + + // TODO is there a nicer way to do this? + /** Compares the configurations (but not the state) of two alarm tree items */ + public static boolean configsEqual(final AlarmTreeItem item1, final AlarmTreeItem item2) { + if (item1 instanceof final AlarmClientLeaf leaf1 && item2 instanceof final AlarmClientLeaf leaf2) { + return Objects.equals(leaf1.getName(), leaf2.getName()) + && Objects.equals(leaf1.getDescription(), leaf2.getDescription()) + && Objects.equals(leaf1.getEnabled(), leaf2.getEnabled()) + && Objects.equals(leaf1.getEnabledDate(), leaf2.getEnabledDate()) + && Objects.equals(leaf1.getFilter(), leaf2.getFilter()) + && leaf1.isLatching() == leaf2.isLatching() + && leaf1.isAnnunciating() == leaf2.isAnnunciating() + && leaf1.getCount() == leaf2.getCount() + && leaf1.getDelay() == leaf2.getDelay() + && Objects.equals(leaf1.getGuidance(), leaf2.getGuidance()) + && Objects.equals(leaf1.getDisplays(), leaf2.getDisplays()) + && Objects.equals(leaf1.getCommands(), leaf2.getCommands()) + && Objects.equals(leaf1.getActions(), leaf2.getActions()); + } + if (item1 instanceof final AlarmClientNode node1 && item2 instanceof final AlarmClientNode node2) { + return Objects.equals(node1.getName(), node2.getName()) + && Objects.equals(node1.getGuidance(), node2.getGuidance()) + && Objects.equals(node1.getDisplays(), node2.getDisplays()) + && Objects.equals(node1.getCommands(), node2.getCommands()) + && Objects.equals(node1.getActions(), node2.getActions()); + } + return false; + } } diff --git a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/json/JsonModelReader.java b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/json/JsonModelReader.java index 263887a15c..8e0f7bf72b 100644 --- a/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/json/JsonModelReader.java +++ b/app/alarm/model/src/main/java/org/phoebus/applications/alarm/model/json/JsonModelReader.java @@ -230,7 +230,7 @@ private static boolean updateAlarmLeafConfig(final AlarmTreeLeaf node, final Jso } } else { - node.setEnabled(true); + changed |= node.setEnabled(true); } 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 1a8d67bb5e..cbd7f6388b 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,16 +15,38 @@ public class Messages public static String acknowledgeFailed; public static String addComponentFailed; - public static String automatedActions; public static String AlarmAreaPanel; + public static String actionsHeaderAlarm; + public static String actionsHeaderNode; + public static String actionsLabel; + public static String actionsLabelAlarmTooltip; + public static String actionsLabelNodeTooltip; + public static String actionsNone; + public static String actionsPlural; + public static String actionsSingular; + public static String alarmProperties; + public static String annunciatingLabel; + public static String annunciatingLabelTooltip; public static String AlarmTable; public static String AlarmTree; public static String Annunciator; - public static String commands; - public static String configure; - public static String delayTooltip0; - public static String delayTooltip1; - public static String delayTooltip2; + public static String behavior; + public static String commandsHeaderAlarm; + public static String commandsHeaderNode; + public static String commandsLabel; + public static String commandsLabelAlarmTooltip; + public static String commandsLabelNodeTooltip; + public static String commandsNone; + public static String commandsPlural; + public static String commandsSingular; + public static String nodeProperties; + public static String configChangedOnServer; + public static String countLabel; + public static String countLabelTooltip; + public static String delayLabel; + public static String delayLabelTooltip; + public static String descriptionLabel; + public static String descriptionLabelTooltip; public static String disableAlarmFailed; public static String disableAlarms; public static String disabled; @@ -33,23 +55,59 @@ public class Messages public static String disabledUntil; public static String disabledCommonEnableDate; public static String disabledVaryingEnableDate; - public static String displays; + public static String discardChanges; + public static String discardChangesPrompt; + public static String displaysHeaderAlarm; + public static String displaysHeaderNode; + public static String displaysLabel; + public static String displaysLabelAlarmTooltip; + public static String displaysLabelNodeTooltip; + public static String displaysNone; + public static String displaysPlural; + public static String displaysSingular; + public static String enabled; + public static String enabledLabel; + public static String enabledLabelTooltip; + public static String enableDatetimeLabel; + public static String enableDatetimeLabelTooltip; + public static String enablingFilterLabel; + public static String enablingFilterLabelTooltip; public static String enableAlarmFailed; public static String enableAlarms; - public static String guidance; + public static String guidanceHeaderAlarm; + public static String guidanceHeaderNode; + public static String guidanceLabel; + public static String guidanceLabelAlarmTooltip; + public static String guidanceLabelNodeTooltip; + public static String guidanceNone; + public static String guidancePlural; + public static String guidanceSingular; public static String headerAlreadyDisabled; public static String headerAlreadyEnabled; public static String headerConfirmDisable; public static String headerConfirmDisableWithEnableDate; public static String headerConfirmEnable; public static String indefinitely; + public static String info; + public static String itemDeletedOnServer; + public static String latchingLabel; + public static String latchingLabelTooltip; + public static String loading; public static String MenuPath; public static String moveItemFailed; - public static String promptTitle; - public static String removeComponentFailed; + public static String nameLabel; + public static String nameLabelTooltip; + public static String pathLabel; + public static String pathLabelAlarmTooltip; + public static String pathLabelNodeTooltip; + public static String pvLabel; + public static String pvLabelTooltip; + public static String removeNodeFailed; public static String renameItemFailed; + public static String saveChanges; public static String totalPVs; public static String unacknowledgeFailed; + public static String updateAlarmItemConfig; public static String withEnableDate; static diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java index 63f5762453..809ad3d2e1 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java @@ -75,8 +75,11 @@ public class AlarmAreaView extends StackPane implements AlarmClientListener */ private final AtomicReference> item_changes = new AtomicReference<>(); - /** Set of items that have updated, with iteration order based on when they updated */ - private final Set items_to_update = new LinkedHashSet<>(); + /** Set of items that have updated, with iteration order based on when they updated + * + * SYNC on access + */ + private final Set item_paths_to_update = new LinkedHashSet<>(); /** Throttle [5Hz] used for updates of existing items */ private final UpdateThrottle update_throttle = new UpdateThrottle(200, TimeUnit.MILLISECONDS, this::updateItems); @@ -165,9 +168,9 @@ public void itemUpdated(final AlarmTreeItem item) if (null == item_name) return; - synchronized (items_to_update) + synchronized (item_paths_to_update) { - items_to_update.add(item_name); + item_paths_to_update.add(item_name); } update_throttle.trigger(); } @@ -178,10 +181,10 @@ private void updateItems() final List changed_items = item_changes.getAndSet(null); final String[] update_array; - synchronized (items_to_update) + synchronized (item_paths_to_update) { - update_array = items_to_update.toArray(new String[items_to_update.size()]); - items_to_update.clear(); + update_array = item_paths_to_update.toArray(new String[item_paths_to_update.size()]); + item_paths_to_update.clear(); } Platform.runLater(() -> { diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeAlarmConfigPanel.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeAlarmConfigPanel.java new file mode 100644 index 0000000000..b99476e770 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeAlarmConfigPanel.java @@ -0,0 +1,127 @@ +package org.phoebus.applications.alarm.ui.config; + +import javafx.geometry.HPos; +import javafx.scene.control.CheckBox; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; + + +public class AlarmTreeAlarmConfigPanel extends AlarmTreeConfigPanel +{ + /** The path of the node that this panel is for. Does not account for newName */ + private final String pathName; + + /** Detatched node storing the item's configuration when the panel was created */ + private final AlarmClientLeaf initialConfig; + + /** Detached node storing the item's configuration including the changes the user has made */ + private final AlarmClientLeaf newConfig; + + public AlarmTreeAlarmConfigPanel(final AlarmClientLeaf alarm) + { + super(); + + pathName = alarm.getPathName(); + initialConfig = alarm.createDetachedCopy(); + newConfig = alarm.createDetachedCopy(); + + getColumnConstraints().add(new ColumnConstraints(32, USE_COMPUTED_SIZE, Integer.MAX_VALUE, Priority.ALWAYS, HPos.LEFT, true)); + getColumnConstraints().add(new ColumnConstraints(32, USE_COMPUTED_SIZE, Integer.MAX_VALUE, Priority.ALWAYS, HPos.LEFT, true)); + getColumnConstraints().get(0).setPercentWidth(40); + + addCategory(Messages.alarmProperties); + addDisabledTextRow(Messages.pathLabel, Messages.pathLabelAlarmTooltip, alarm.getPathName()); + addDisabledTextRow(Messages.pvLabel, Messages.pvLabelTooltip, initialConfig.getName()); + + addSubcategory(Messages.info); + addTextRow(Messages.descriptionLabel, Messages.descriptionLabelTooltip, initialConfig.getDescription(), newConfig::setDescription); + addTitleDetailListBox( + Messages.guidanceLabel, + Messages.guidanceLabelAlarmTooltip, + Messages.guidanceHeaderAlarm, + Messages.guidanceNone, + Messages.guidanceSingular, + Messages.guidancePlural, + initialConfig.getGuidance(), + newConfig::getGuidance, + newConfig::setGuidance + ); + addTitleDetailListBox( + Messages.displaysLabel, + Messages.displaysLabelAlarmTooltip, + Messages.displaysHeaderAlarm, + Messages.displaysNone, + Messages.displaysSingular, + Messages.displaysPlural, + initialConfig.getDisplays(), + newConfig::getDisplays, + newConfig::setDisplays + ); + addTitleDetailListBox( + Messages.commandsLabel, + Messages.commandsLabelAlarmTooltip, + Messages.commandsHeaderAlarm, + Messages.commandsNone, + Messages.commandsSingular, + Messages.commandsPlural, + initialConfig.getCommands(), + newConfig::getCommands, + newConfig::setCommands + ); + + addSubcategory(Messages.enabled); + final CheckBox enabledCheckBox = addCheckboxRow(Messages.enabledLabel, Messages.enabledLabelTooltip, initialConfig.isEnabled(), newConfig::setEnabled); + final DateTimePicker enableDateTimePicker = addDateTimeRow(Messages.enableDatetimeLabel, Messages.enableDatetimeLabelTooltip, initialConfig.getEnabledDate(), newConfig::setEnabledDate); + + enableDateTimePicker.setDisable(enabledCheckBox.isSelected()); + enabledCheckBox.selectedProperty().addListener(_ -> { + enableDateTimePicker.setDisable(enabledCheckBox.isSelected()); + if (enabledCheckBox.isSelected()) { + enableDateTimePicker.setDateTimeValue(null); + } + }); + + addTextRow(Messages.enablingFilterLabel, Messages.enablingFilterLabelTooltip, initialConfig.getFilter(), newConfig::setFilter); + + addSubcategory(Messages.behavior); + addCheckboxRow(Messages.latchingLabel, Messages.latchingLabelTooltip, initialConfig.isLatching(), newConfig::setLatching); + addCheckboxRow(Messages.annunciatingLabel, Messages.annunciatingLabelTooltip, initialConfig.isAnnunciating(), newConfig::setAnnunciating); + addIntSpinnerRow(Messages.delayLabel, Messages.delayLabelTooltip, initialConfig.getDelay(), newConfig::setDelay); + addIntSpinnerRow(Messages.countLabel, Messages.countLabelTooltip, initialConfig.getCount(), newConfig::setCount); + addTitleDetailDelayListBox( + Messages.actionsLabel, + Messages.actionsLabelAlarmTooltip, + Messages.actionsHeaderAlarm, + Messages.actionsNone, + Messages.actionsSingular, + Messages.actionsPlural, + initialConfig.getActions(), + newConfig::getActions, + newConfig::setActions + ); + + add(new HBox(saveButton, discardButton), 0, getRowCount() + 1, 2, 1); + } + + @Override + public String getPathName() + { + return pathName; + } + + @Override + public AlarmTreeItem getInitialConfig() + { + return initialConfig; + } + + @Override + public AlarmTreeItem getNewConfig() + { + return newConfig; + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeConfigPanel.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeConfigPanel.java new file mode 100644 index 0000000000..bf89daed93 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeConfigPanel.java @@ -0,0 +1,261 @@ +package org.phoebus.applications.alarm.ui.config; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +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 org.phoebus.applications.alarm.ui.tree.TitleDetailDelayDialog; +import org.phoebus.applications.alarm.ui.tree.TitleDetailDialog; +import org.phoebus.ui.dialog.DialogHelper; + +import java.text.MessageFormat; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + + +public abstract class AlarmTreeConfigPanel extends GridPane +{ + + public final Button saveButton = new Button(Messages.saveChanges); + public final Button discardButton = new Button(Messages.discardChanges); + + protected AlarmTreeConfigPanel() + { + super(); + + HBox.setMargin(saveButton, new Insets(3, 3, 3, 3)); + HBox.setMargin(discardButton, new Insets(3, 3, 3, 3)); + + saveButton.setDisable(true); + discardButton.setDisable(true); + } + + public abstract String getPathName(); + public abstract AlarmTreeItem getInitialConfig(); + public abstract AlarmTreeItem getNewConfig(); + + protected void addCategory(final String name) + { + final Label header = new Label(name); + header.getStyleClass().add("heading"); + header.setMaxWidth(Double.MAX_VALUE); + final int row = getRowCount() + 1; + add(header, 0, row, 2, 1); + } + + protected void addSubcategory(final String name) + { + final Label header = new Label(name); + header.getStyleClass().add("subheading"); + header.setMaxWidth(Double.MAX_VALUE); + final int row = getRowCount() + 1; + add(header, 0, row, 2, 1); + } + + protected void addDisabledTextRow(final String label, final String tooltip, final String pathValue) { + final TextField path = new TextField(pathValue.replace("\\", "")); + path.setMaxWidth(Double.MAX_VALUE); + path.setDisable(true); + addRow(label, tooltip, path); + } + + protected void addTextRow(final String label, final String tooltip, final String initialValue, final Consumer onChanged) { + final TextField textField = new TextField(initialValue); + + textField.setMaxWidth(Double.MAX_VALUE); + textField.textProperty().addListener(_ -> + { + onChanged.accept(textField.getText()); + userMadeChange(); + updateModifiedStyle(textField, Objects.equals(textField.getText(), initialValue)); + }); + + addRow(label, tooltip, textField); + } + + protected CheckBox addCheckboxRow(final String label, final String tooltip, final boolean initialValue, final Consumer onChanged) { + final CheckBox checkBox = new CheckBox(); + + checkBox.setSelected(initialValue); + checkBox.selectedProperty().addListener(_ -> + { + onChanged.accept(checkBox.isSelected()); + userMadeChange(); + updateModifiedStyle(checkBox, checkBox.isSelected() == initialValue); + }); + + addRow(label, tooltip, checkBox); + return checkBox; + } + + protected DateTimePicker addDateTimeRow(final String label, final String tooltip, final LocalDateTime initialValue, final Consumer onChanged) { + final DateTimePicker dateTimePicker = new DateTimePicker(); + + dateTimePicker.setDateTimeValue(initialValue); + dateTimePicker.dateTimeProperty().addListener(_ -> + { + final LocalDateTime value = dateTimePicker.getDateTimeValue(); + if (value != null && value.isBefore(LocalDateTime.now())) { + dateTimePicker.setDateTimeValue(null); + } + onChanged.accept(dateTimePicker.getDateTimeValue()); + userMadeChange(); + updateModifiedStyle(dateTimePicker, Objects.equals(dateTimePicker.getDateTimeValue(), initialValue)); + }); + + addRow(label, tooltip, dateTimePicker); + return dateTimePicker; + } + + protected void addIntSpinnerRow(final String label, final String tooltip, final int initialValue, final Consumer onChanged) { + final Spinner spinner = new Spinner<>(0, Integer.MAX_VALUE, initialValue); + + spinner.setEditable(true); + spinner.valueProperty().addListener(_ -> + { + onChanged.accept(spinner.getValue()); + userMadeChange(); + updateModifiedStyle(spinner, spinner.getValue() == initialValue); + }); + + addRow(label, tooltip, spinner); + } + + protected void addTitleDetailListBox( + final String label, + final String tooltip, + final String header, + final String none, + final String singular, + final String plural, + final List initialValue, + final Supplier> currentValue, + final Consumer> onChanged + ) { + final Button button = new Button(); + + final Runnable updateButton = () -> { + final List value = currentValue.get(); + if (value.isEmpty()) { + button.setText(none); + } else if (value.size() == 1) { + button.setText(singular); + } else { + button.setText(MessageFormat.format(plural, value.size())); + } + updateModifiedStyle(button, currentValue.get().equals(initialValue)); + }; + + updateButton.run(); + + button.setOnAction(_ -> { + final TitleDetailDialog dialog = new TitleDetailDialog(label, header, currentValue.get()); + dialog.resultProperty().addListener(_ -> { + onChanged.accept(dialog.getResult()); + updateButton.run(); + userMadeChange(); + }); + DialogHelper.positionDialog(dialog, button,-300, -100); + dialog.show(); + }); + + addRow(label, tooltip, button); + } + + protected void addTitleDetailDelayListBox( + final String label, + final String tooltip, + final String header, + final String none, + final String singular, + final String plural, + final List initialValue, + final Supplier> currentValue, + final Consumer> onChanged + ) { + final Button button = new Button(); + + final Runnable updateButton = () -> { + final List value = currentValue.get(); + if (value.isEmpty()) { + button.setText(none); + } else if (value.size() == 1) { + button.setText(singular); + } else { + button.setText(MessageFormat.format(plural, value.size())); + } + updateModifiedStyle(button, currentValue.get().equals(initialValue)); + }; + + updateButton.run(); + + button.setOnAction(_ -> { + final TitleDetailDelayDialog dialog = new TitleDetailDelayDialog(label, header, currentValue.get()); + dialog.resultProperty().addListener(_ -> { + onChanged.accept(dialog.getResult()); + updateButton.run(); + userMadeChange(); + }); + DialogHelper.positionDialog(dialog, button,-300, -100); + dialog.show(); + }); + + addRow(label, tooltip, button); + } + + private void addRow(final String key, final String tooltip, final Node value) + { + final Label label = new Label(key); + label.setTooltip(new Tooltip(tooltip)); + label.setMaxWidth(Double.MAX_VALUE); + label.setPadding(new Insets(0, 5, 0, 5)); + final int row = getRowCount() + 1; + add(label, 0, row); + + final HBox box = new HBox(value); + box.setMinHeight(28); + box.setAlignment(Pos.CENTER_LEFT); + box.setPadding(new Insets(0, 5, 0, 0)); + HBox.setHgrow(value, Priority.ALWAYS); + add(box, 1, row); + + final Separator separator = new Separator(); + add(separator, 0, row + 1, 2, 1); + } + + private static void updateModifiedStyle(final Node node, final boolean modified) + { + if (modified) { + node.getStyleClass().remove("modified"); + } else if (!node.getStyleClass().contains("modified")) { + node.getStyleClass().add("modified"); + } + } + + private void userMadeChange() + { + final boolean changed = isConfigChanged(); + saveButton.setDisable(!changed); + discardButton.setDisable(!changed); + } + + public boolean isConfigChanged() { + return !AlarmTreeItem.configsEqual(getInitialConfig(), getNewConfig()); + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeNodeConfigPanel.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeNodeConfigPanel.java new file mode 100644 index 0000000000..33f1ca8f02 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/AlarmTreeNodeConfigPanel.java @@ -0,0 +1,107 @@ +package org.phoebus.applications.alarm.ui.config; + +import javafx.geometry.HPos; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.phoebus.applications.alarm.client.AlarmClientNode; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.Messages; + + +public class AlarmTreeNodeConfigPanel extends AlarmTreeConfigPanel +{ + /** The path of the node that this panel is for. Does not account for newName */ + private final String pathName; + + /** Detatched node storing the item's configuration when the panel was created */ + private final AlarmClientNode initialConfig; + + /** Detached node storing the item's configuration including the changes the user has made */ + private final AlarmClientNode newConfig; + + public AlarmTreeNodeConfigPanel(final AlarmClientNode component) + { + super(); + + pathName = component.getPathName(); + initialConfig = component.createDetachedCopy(); + newConfig = component.createDetachedCopy(); + + getColumnConstraints().add(new ColumnConstraints(32, USE_COMPUTED_SIZE, Integer.MAX_VALUE, Priority.ALWAYS, HPos.LEFT, true)); + getColumnConstraints().add(new ColumnConstraints(32, USE_COMPUTED_SIZE, Integer.MAX_VALUE, Priority.ALWAYS, HPos.LEFT, true)); + getColumnConstraints().get(0).setPercentWidth(40); + + addCategory(Messages.nodeProperties); + addDisabledTextRow(Messages.pathLabel, Messages.pathLabelNodeTooltip, component.getPathName()); + addDisabledTextRow(Messages.nameLabel, Messages.nameLabelTooltip, initialConfig.getName()); + + addSubcategory(Messages.info); + addTitleDetailListBox( + Messages.guidanceLabel, + Messages.guidanceLabelNodeTooltip, + Messages.guidanceHeaderNode, + Messages.guidanceNone, + Messages.guidanceSingular, + Messages.guidancePlural, + initialConfig.getGuidance(), + newConfig::getGuidance, + newConfig::setGuidance + ); + addTitleDetailListBox( + Messages.displaysLabel, + Messages.displaysLabelNodeTooltip, + Messages.displaysHeaderNode, + Messages.displaysNone, + Messages.displaysSingular, + Messages.displaysPlural, + initialConfig.getDisplays(), + newConfig::getDisplays, + newConfig::setDisplays + ); + addTitleDetailListBox( + Messages.commandsLabel, + Messages.commandsLabelNodeTooltip, + Messages.commandsHeaderNode, + Messages.commandsNone, + Messages.commandsSingular, + Messages.commandsPlural, + initialConfig.getCommands(), + newConfig::getCommands, + newConfig::setCommands + ); + + addSubcategory(Messages.behavior); + addTitleDetailDelayListBox( + Messages.actionsLabel, + Messages.actionsLabelNodeTooltip, + Messages.actionsHeaderNode, + Messages.actionsNone, + Messages.actionsSingular, + Messages.actionsPlural, + initialConfig.getActions(), + newConfig::getActions, + newConfig::setActions + ); + + add(new HBox(saveButton, discardButton), 0, getRowCount() + 1, 2, 1); + } + + @Override + public String getPathName() + { + return pathName; + } + + @Override + public AlarmTreeItem getInitialConfig() + { + return initialConfig; + } + + @Override + public AlarmTreeItem getNewConfig() + { + return newConfig; + } +} 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 deleted file mode 100644 index b0ab443b1d..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ConfigDialogController.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2025 European Spallation Source ERIC. - */ - -package org.phoebus.applications.alarm.ui.config; - -import javafx.fxml.FXML; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.TextField; -import javafx.scene.layout.GridPane; -import org.phoebus.applications.alarm.client.AlarmClient; -import org.phoebus.applications.alarm.model.AlarmTreeItem; -import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; - -public class ConfigDialogController { - - @SuppressWarnings("unused") - @FXML - private ScrollPane scroll; - @SuppressWarnings("unused") - @FXML - private GridPane layout; - - - @SuppressWarnings("unused") - @FXML - private TextField path; - - - @FXML - protected OptionsTablesController optionsTablesViewController; - - protected final AlarmClient alarmClient; - protected final AlarmTreeItem alarmTreeItem; - - - public ConfigDialogController(AlarmClient alarmClient, AlarmTreeItem alarmTreeItem) { - this.alarmClient = alarmClient; - this.alarmTreeItem = alarmTreeItem; - } - - @FXML - public void initialize() { - - path.setText(alarmTreeItem.getPathName()); - -// // ── Scroll-pane width listener ──────────────────────────────────────── - scroll.widthProperty().addListener((p, old, width) -> - layout.setPrefWidth(Math.max(width.doubleValue() - 40, 450))); - } - - - public void validateAndStore(){ - 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); - } - } -} 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 c8665f2a08..3203bed04c 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 @@ -86,6 +86,10 @@ private void simulateEnterPressed() { getEditor().commitValue(); } + public ObjectProperty dateTimeProperty() { + return dateTimeValue; + } + /** @return Time */ public LocalDateTime getDateTimeValue() { return dateTimeValue.get(); 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 deleted file mode 100644 index 6ba3ea814c..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/ItemConfigDialog.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.client.AlarmClientLeaf; -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 LeafConfigDialog.fxml}. - * Runtime wiring (model data, bindings, event handlers) is handled by - * {@link LeafConfigDialogController}. - * - *

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); - final FXMLLoader fxmlLoader = new FXMLLoader(); - try { - fxmlLoader.setResources(NLS.getMessages(Messages.class)); - if (item instanceof AlarmClientLeaf){ - fxmlLoader.setLocation(this.getClass().getResource("LeafConfigDialog.fxml")); - } - else{ - // 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); - } - // 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(); - } - 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 controller for " + clazz.getName(), 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 -> ((ConfigDialogController)fxmlLoader.getController()).validateAndStore()); - - getDialogPane().setContent(root); - - } catch (Exception ex) { - throw new RuntimeException("Failed to load " + fxmlLoader.getLocation(), ex); - } - - setResizable(true); - - setResultConverter(button -> button == ButtonType.OK); - } -} 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 deleted file mode 100644 index 0f85f7c788..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/LeafConfigDialogController.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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.*; -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.util.function.UnaryOperator; - - -/** - * 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; - - @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(); - private final SimpleStringProperty enablingFilterProperty = new SimpleStringProperty(""); - - private SpinnerValueFactory countValueFactory; - private SpinnerValueFactory delayValueFactory; - - protected final SimpleBooleanProperty itemEnabledProperty = new SimpleBooleanProperty(); - - - 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); - - - - enabled.setOnAction(e -> { - itemEnabledProperty.setValue(enabled.isSelected()); - }); - - - 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()); - - // 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(), itemEnabledProperty); - - 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()); - - - pv.setDescription(descriptionProperty.get()); - pv.setEnabled(itemEnabledProperty.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(optionsTablesViewController.getGuidance()); - pv.setDisplays(optionsTablesViewController.getDisplays()); - pv.setCommands(optionsTablesViewController.getCommands()); - pv.setActions(optionsTablesViewController.getActions()); - - 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/config/OptionsTablesController.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java deleted file mode 100644 index f155847d3c..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/config/OptionsTablesController.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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; - } - - /** - * 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()); - displaysViewController.setTitle(Messages.displays); - displaysViewController.setItems(item.getDisplays()); - commandsViewController.setTitle(Messages.commands); - commandsViewController.setItems(item.getCommands()); - actionsViewController.setTitle(Messages.automatedActions); - 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/table/AlarmTableUI.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableUI.java index 98ae4461bc..3d1708d871 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableUI.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/table/AlarmTableUI.java @@ -29,7 +29,6 @@ import org.phoebus.applications.alarm.model.SeverityLevel; import org.phoebus.applications.alarm.ui.AlarmContextMenuHelper; import org.phoebus.applications.alarm.ui.AlarmUI; -import org.phoebus.applications.alarm.ui.tree.ConfigureComponentAction; import org.phoebus.framework.jobs.Job; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.persistence.Memento; @@ -563,11 +562,6 @@ private void createContextMenu(final TableView table, final boolea if (new AlarmContextMenuHelper().addSupportedEntries(table, client, menu, selection)) menu_items.add(new SeparatorMenuItem()); - if (AlarmUI.mayConfigure(client) && selection.size() == 1) - { - menu_items.add(new ConfigureComponentAction(table, client, selection.get(0))); - menu_items.add(new SeparatorMenuItem()); - } menu_items.add(new PrintAction(this)); menu_items.add(new SaveSnapshotAction(table)); 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 f3d65c9801..20203d4620 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 @@ -12,10 +12,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -24,6 +25,11 @@ import java.util.stream.Collectors; import javafx.collections.FXCollections; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SplitPane; +import javafx.stage.Modality; import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.client.AlarmClientLeaf; @@ -33,12 +39,17 @@ 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.applications.alarm.ui.Messages; +import org.phoebus.applications.alarm.ui.config.AlarmTreeAlarmConfigPanel; +import org.phoebus.applications.alarm.ui.config.AlarmTreeNodeConfigPanel; +import org.phoebus.applications.alarm.ui.config.AlarmTreeConfigPanel; +import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.Selection; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuService; import org.phoebus.ui.application.SaveSnapshotAction; import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.docking.DockPane; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.javafx.PrintAction; @@ -80,6 +91,14 @@ * as generic JavaFX Node, only calling public methods * defined on this class. * + *

The model (AlarmClient) and the AlarmTreeView are kept in + * sync by a lockstep between model and view updates. When the + * model is changed, it calls item[removed|added]. The AlarmClient + * then blocks until the method returns (i.e. until the UI has + * been updated) and then can move on to the next update. The + * exception to this is itemUpdated, which adds to a queue of + * items to be updated which is then regularly processed. + * * @author Kay Kasemir */ @SuppressWarnings("nls") @@ -87,6 +106,18 @@ public class AlarmTreeView extends BorderPane implements AlarmClientListener { private final Label no_server = AlarmUI.createNoServerLabel(); private final TreeView> tree_view = new TreeView<>(); + private final ScrollPane config_view = new ScrollPane(); + private AlarmTreeConfigPanel config_panel = null; + + /** Unfortunate hack-on-top-of-a-hack - performUpdates has to rebuild tree cells which + * clears the selection, so performUpdates saves the selection beforehand and then + * manually re-selects every item. This causes the selection listener to fire many + * times when the end result is actually the same. So when restoring the selection, we + * set this flag to true to indicate that fired selection listeners should be ignored + * + * This is also used to prevent a recursive loop when the selection is being updated + * from within a selection listener */ + private boolean updating_selected_items = false; /** Model with current alarm tree, sends updates */ private final AlarmClient model; @@ -112,19 +143,19 @@ public class AlarmTreeView extends BorderPane implements AlarmClientListener private final CountDownLatch block_item_changes = new CountDownLatch(1); /** Map from alarm tree path to view's TreeItem */ - private final ConcurrentHashMap>> path2view = new ConcurrentHashMap<>(); + private final Map>> path2view = new HashMap<>(); /** Items to update, ordered by time of original update request * - * SYNC on access + * SYNC on access */ - private final Set>> items_to_update = new LinkedHashSet<>(); + private final Set item_paths_to_update = new LinkedHashSet<>(); /** Throttle [5Hz] used for updates of existing items */ private final UpdateThrottle throttle = new UpdateThrottle(200, TimeUnit.MILLISECONDS, this::performUpdates); /** Cursor change doesn't work on Mac, so add indicator to toolbar */ - private final Label changing = new Label("Loading..."); + private final Label changing = new Label(Messages.loading); /** Is change indicator shown, and future been submitted to clear it? */ private final AtomicReference> ongoing_change = new AtomicReference<>(); @@ -140,6 +171,8 @@ public class AlarmTreeView extends BorderPane implements AlarmClientListener items.remove(changing); }); + private Alert discard_changes_alert; + // Javadoc for TreeItem shows example for overriding isLeaf() and getChildren() // to dynamically create TreeItem as TreeView requests information. // @@ -161,7 +194,7 @@ public AlarmTreeView(final AlarmClient model) { * @param model Model to represent. Must not be running, yet * @param itemName item name that may be expanded or given focus */ - public AlarmTreeView(final AlarmClient model, String itemName) + public AlarmTreeView(final AlarmClient model, final String itemName) { if (model.isRunning()) throw new IllegalStateException(); @@ -176,11 +209,21 @@ public AlarmTreeView(final AlarmClient model, String itemName) tree_view.setCellFactory(view -> new AlarmTreeViewCell()); tree_view.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + config_view.getStylesheets().add(AlarmUI.class.getResource("alarm_node_config_editor.css").toExternalForm()); + config_view.setFitToWidth(true); + setTop(createToolbar()); - setCenter(tree_view); + + SplitPane split_pane = new SplitPane(); + split_pane.getItems().add(tree_view); + // TODO change to isEditing listener once that is in + if (AlarmUI.mayConfigure(model)) { + split_pane.getItems().add(config_view); + } + setCenter(split_pane); createContextMenu(); - addClickSupport(); + addSelectSupport(); addDragSupport(); if (AlarmSystem.alarm_tree_startup_ms <= 0) @@ -387,37 +430,34 @@ private void blockItemChanges() public void itemAdded(final AlarmTreeItem item) { blockItemChanges(); - // System.out.println(Thread.currentThread() + " Add " + item.getPathName()); - - // Parent must already exist - final AlarmTreeItem model_parent = item.getParent(); - final TreeItem> view_parent = path2view.get(model_parent.getPathName()); - if (view_parent == null) - { - dumpTree(tree_view.getRoot()); - throw new IllegalStateException("Missing parent view item for " + item.getPathName()); - } - // Create view item ASAP so that following updates will find it.. - final TreeItem> view_item = createViewItem(item); - - // .. but defer showing it on screen to UI thread final CountDownLatch done = new CountDownLatch(1); + Platform.runLater(() -> { indicateChange(); + + // Parent must already exist + final AlarmTreeItem model_parent = item.getParent(); + final TreeItem> view_parent = path2view.get(model_parent.getPathName()); + + if (view_parent == null) { + dumpTree(tree_view.getRoot()); + throw new IllegalStateException("Missing parent view item for " + item.getPathName()); + } + final TreeItem> view_item = createViewItem(item); + // Keep sorted by inserting at appropriate index final List>> items = view_parent.getChildren(); final int index = Collections.binarySearch(items, view_item, - (a, b) -> CompareNatural.compareTo(a.getValue().getName(), - b.getValue().getName())); + (a, b) -> CompareNatural.compareTo(a.getValue().getName(), + b.getValue().getName())); if (index < 0) - items.add(-index-1, view_item); + items.add(-index - 1, view_item); else items.add(index, view_item); done.countDown(); }); - updateStats(); // Waiting on the UI thread throttles the model's updates // to a rate that the UI can handle. @@ -438,18 +478,35 @@ public void itemAdded(final AlarmTreeItem item) public void itemRemoved(final AlarmTreeItem item) { blockItemChanges(); - // System.out.println(Thread.currentThread() + " Removed " + item.getPathName()); - - // Remove item and all sub-items from model2ui - final TreeItem> view_item = removeViewItems(item); - if (view_item == null) - throw new IllegalStateException("No view item for " + item.getPathName()); - // Remove the corresponding view final CountDownLatch done = new CountDownLatch(1); Platform.runLater(() -> { indicateChange(); + + // Remove item and all sub-items from model2ui + final TreeItem> view_item = removeViewItems(item); + if (view_item == null) + throw new IllegalStateException("No view item for " + item.getPathName()); + + // Close panel if this item is being edited + if (config_panel != null && config_panel.getPathName().equals(item.getPathName())) { + if (config_panel.isConfigChanged()) { + // Only show alert if the user has made changes + final Alert alert = new Alert(Alert.AlertType.ERROR, "", ButtonType.OK); + DialogHelper.positionDialog(alert, tree_view, 0, 0); + alert.setHeaderText(Messages.itemDeletedOnServer); + alert.initModality(Modality.WINDOW_MODAL); + alert.show(); + } + + config_panel = null; + config_view.setContent(null); + tree_view.getSelectionModel().clearSelection(); + } + + indicateChange(); + // Can only locate the parent view item on UI thread, // because item might just have been created by itemAdded() event // and won't be on the screen until UI thread runs. @@ -459,7 +516,6 @@ public void itemRemoved(final AlarmTreeItem item) view_parent.getChildren().remove(view_item); done.countDown(); }); - updateStats(); // Waiting on the UI thread throttles the model's updates // to a rate that the UI can handle. @@ -493,87 +549,114 @@ private TreeItem> removeViewItems(final AlarmTreeItem item) public void itemUpdated(final AlarmTreeItem item) { blockItemChanges(); - // System.out.println(Thread.currentThread() + " Updated " + item.getPathName()); - final TreeItem> view_item = path2view.get(item.getPathName()); - if (view_item == null) - { - System.out.println("Unknown view for " + item.getPathName()); - path2view.keySet().stream().forEach(System.out::println); - throw new IllegalStateException("No view item for " + item.getPathName()); - } // UI update of existing item, i.e. // Platform.runLater(() -> TreeHelper.triggerTreeItemRefresh(view_item)); // is throttled. // If several items update, they're all redrawn in one Platform call, // and rapid updates of the same item are merged into just one final update - synchronized (items_to_update) + // We push the path instead of the view item in case the view item is removed + // between now and the update being handled. + synchronized (item_paths_to_update) { - items_to_update.add(view_item); + item_paths_to_update.add(item.getPathName()); } throttle.trigger(); - updateStats(); } /** Called by throttle to perform accumulated updates */ - @SuppressWarnings("unchecked") private void performUpdates() { - final TreeItem>[] view_items; - synchronized (items_to_update) + + // Drain items to update in one go; we will process it later on the main thread + final String[] view_item_paths; + synchronized (item_paths_to_update) { - // Creating a direct copy, i.e. another new LinkedHashSet<>(items_to_update), - // would be expensive, since we only need a _list_ of what's to update. - // Could use type-safe - // new ArrayList>>(items_to_update) - // but that calls toArray() internally, so doing that directly - view_items = items_to_update.toArray(new TreeItem[items_to_update.size()]); - items_to_update.clear(); + view_item_paths = item_paths_to_update.toArray(new String[item_paths_to_update.size()]); + item_paths_to_update.clear(); } - // Remember selection - final ObservableList>> updatedSelectedItems = - FXCollections.observableArrayList(tree_view.getSelectionModel().getSelectedItems()); - - // How to update alarm tree cells when data changed? - // `setValue()` with a truly new value (not 'equal') should suffice, - // but there are two problems: - // Since we're currently using the alarm tree model item as a value, - // the value as seen by the TreeView remains the same. - // We could use a model item wrapper class as the cell value - // and replace it (while still holding the same model item!) - // for the TreeView to see a different wrapper value, but - // as shown in org.phoebus.applications.alarm.TreeItemUpdateDemo, - // replacing a tree cell value fails to trigger refreshes - // for certain hidden items. - // Only replacing the TreeItem gives reliable refreshes. - for (final TreeItem> view_item : view_items) - // Top-level item has no parent, and is not visible, so we keep it - if (view_item.getParent() != null) - { - // Locate item in tree parent - final TreeItem> parent = view_item.getParent(); - final int index = parent.getChildren().indexOf(view_item); - - // Create new TreeItem for that value - final AlarmTreeItem value = view_item.getValue(); - final TreeItem> update = new TreeItem<>(value); - if (updatedSelectedItems.contains(view_item)) { - updatedSelectedItems.remove(view_item); - updatedSelectedItems.add(update); + Platform.runLater(() -> { + // Remember selection + final ObservableList>> updatedSelectedItems = + FXCollections.observableArrayList(tree_view.getSelectionModel().getSelectedItems()); + + // How to update alarm tree cells when data changed? + // `setValue()` with a truly new value (not 'equal') should suffice, + // but there are two problems: + // Since we're currently using the alarm tree model item as a value, + // the value as seen by the TreeView remains the same. + // We could use a model item wrapper class as the cell value + // and replace it (while still holding the same model item!) + // for the TreeView to see a different wrapper value, but + // as shown in org.phoebus.applications.alarm.TreeItemUpdateDemo, + // replacing a tree cell value fails to trigger refreshes + // for certain hidden items. + // Only replacing the TreeItem gives reliable refreshes. + updating_selected_items = true; // run here because modifying the tree cells can modify the selection... + for (final String view_item_path : view_item_paths) { + final TreeItem> view_item = path2view.get(view_item_path); + if (view_item == null) { + // Possible if the item has been removed since it was added to the update queue + continue; + } + + // Top-level item has no parent, and is not visible, so we keep it + if (view_item.getParent() != null) { + // Locate item in tree parent + final TreeItem> parent = view_item.getParent(); + final int index = parent.getChildren().indexOf(view_item); + + // Create new TreeItem for that value + final AlarmTreeItem value = view_item.getValue(); + final TreeItem> update = new TreeItem<>(value); + if (updatedSelectedItems.contains(view_item)) { + updatedSelectedItems.remove(view_item); + updatedSelectedItems.add(update); + } + // Move child links to new item + final ArrayList>> children = new ArrayList<>(view_item.getChildren()); + view_item.getChildren().clear(); + update.getChildren().addAll(children); + update.setExpanded(view_item.isExpanded()); + + path2view.put(value.getPathName(), update); + parent.getChildren().set(index, update); } - // Move child links to new item - final ArrayList>> children = new ArrayList<>(view_item.getChildren()); - view_item.getChildren().clear(); - update.getChildren().addAll(children); - update.setExpanded(view_item.isExpanded()); - - path2view.put(value.getPathName(), update); - parent.getChildren().set(index, update); } - // Restore selection - tree_view.getSelectionModel().clearSelection(); - updatedSelectedItems.forEach(item -> tree_view.getSelectionModel().select(item)); + + // Restore selection + tree_view.getSelectionModel().clearSelection(); + updatedSelectedItems.forEach(item -> tree_view.getSelectionModel().select(item)); + updating_selected_items = false; + + // Handle config panel UI changes if config_panel refers to an item whose config has just been updated + if (config_panel != null && Arrays.asList(view_item_paths).contains(config_panel.getPathName())) { + final TreeItem> config_panel_view_item = path2view.get(config_panel.getPathName()); + if (!AlarmTreeItem.configsEqual(config_panel_view_item.getValue(), config_panel.getInitialConfig())) { + if (!config_panel.isConfigChanged()) + // Config has not been edited on our end; just update the UI + refreshConfigPanel(); + else if (AlarmTreeItem.configsEqual(config_panel_view_item.getValue(), config_panel.getNewConfig())) + // Config has been edited but is the same as the updated config; just update the UI + refreshConfigPanel(); + else if (discard_changes_alert == null) { + // Config has been edited and there is now a conflict; prompt the user + final Alert alert = new Alert(Alert.AlertType.WARNING, "", ButtonType.NO, ButtonType.YES); + DialogHelper.positionDialog(alert, config_panel, -100, -50); + alert.setHeaderText(Messages.configChangedOnServer); + alert.initModality(Modality.WINDOW_MODAL); + alert.resultProperty().addListener((_, _, button) -> { + if (button == ButtonType.YES) + refreshConfigPanel(); + discard_changes_alert = null; + }); + alert.show(); + discard_changes_alert = alert; + } + } + } + }); } /** Context menu, details depend on selected items */ @@ -601,8 +684,6 @@ private void createContextMenu() else if (selection.size() == 1) { final AlarmTreeItem item = selection.get(0); - menu_items.add(new ConfigureComponentAction(tree_view, model, item)); - menu_items.add(new SeparatorMenuItem()); if (item instanceof AlarmClientNode) menu_items.add(new AddComponentAction(tree_view, model, item)); @@ -618,7 +699,7 @@ else if (selection.size() == 1) { menu_items.add(new EnableComponentAction(tree_view, model, selection)); menu_items.add(new DisableAction(tree_view, model, selection)); - menu_items.add(new RemoveComponentAction(tree_view, model, selection)); + menu_items.add(new RemoveComponentAction(tree_view, model, selection, this)); } } @@ -652,24 +733,78 @@ else if (selection.size() == 1) }); } - /** Double-click on item opens configuration dialog */ - private void addClickSupport() + private void addSelectSupport() { - tree_view.setOnMouseClicked(event -> + tree_view.getSelectionModel().selectedItemProperty().addListener((_, oldSelection, _) -> { - if (!AlarmUI.mayConfigure(model) || - event.getClickCount() != 2 || - tree_view.getSelectionModel().getSelectedItems().size() != 1) + if (updating_selected_items) // see comment on updating_selected_items return; - final AlarmTreeItem item = tree_view.getSelectionModel().getSelectedItems().get(0).getValue(); - final ItemConfigDialog dialog = new ItemConfigDialog(model, item); - DialogHelper.positionDialog(dialog, tree_view, -150, -300); - // Show dialog, not waiting for it to close with OK or Cancel - dialog.show(); + if (config_panel != null && config_panel.isConfigChanged()) + { + if (discard_changes_alert == null) { + final Alert alert = new Alert(Alert.AlertType.WARNING, "", ButtonType.NO, ButtonType.YES); + DialogHelper.positionDialog(alert, config_panel, -100, -50); + alert.setHeaderText(Messages.discardChangesPrompt); + alert.initModality(Modality.WINDOW_MODAL); + alert.resultProperty().addListener((_, _, button) -> + { + discard_changes_alert = null; + if (button == ButtonType.YES) + refreshConfigPanel(); + else { + // Selection has changed, since the user clicked 'no' we need to preserve the previous selection + updating_selected_items = true; + tree_view.getSelectionModel().clearSelection(); + tree_view.getSelectionModel().select(oldSelection); + updating_selected_items = false; + } + }); + alert.show(); + discard_changes_alert = alert; + } + } + else + { + refreshConfigPanel(); + } }); } + /** Creates and displays a config panel for the selected item */ + private void refreshConfigPanel() + { + final ObservableList>> items = tree_view.getSelectionModel().getSelectedItems(); + if (items.size() != 1) { + config_panel = null; + config_view.setContent(null); + } else { + final AlarmTreeItem selected = items.getFirst().getValue(); + + if (selected instanceof final AlarmClientNode node) { + config_panel = new AlarmTreeNodeConfigPanel(node); + } else if (selected instanceof final AlarmClientLeaf leaf) { + config_panel = new AlarmTreeAlarmConfigPanel(leaf); + } + + if (config_panel != null) { + config_panel.discardButton.setOnAction(_ -> refreshConfigPanel()); + config_panel.saveButton.setOnAction(_ -> { + JobManager.schedule(Messages.updateAlarmItemConfig, monitor -> + { + try { + model.sendItemConfigurationUpdate(config_panel.getPathName(), config_panel.getNewConfig()); + } catch (final Exception ex) { + ExceptionDetailsErrorDialog.openError("Error", "Cannot update item", ex); + } + }); + }); + } + + config_view.setContent(config_panel); + } + } + /** For leaf nodes, drag PV name */ private void addDragSupport() { @@ -689,25 +824,14 @@ private void addDragSupport() }); } -// private long next_stats = 0; -// private final AtomicInteger update_count = new AtomicInteger(); -// private volatile double updates_per_sec = 0.0; - - private void updateStats() - { -// final long time = System.currentTimeMillis(); -// if (time > next_stats) -// { -// final int updates = update_count.getAndSet(0); -// updates_per_sec = updates_per_sec * 0.9 + updates * 0.1; -// next_stats = time + 1000; -// System.out.format("%.2f updates/sec\n", updates_per_sec); -// } -// else -// update_count.incrementAndGet(); + public void closePanel() { + if (config_panel != null) { + config_view.setContent(null); + config_panel = null; + } } - private void dumpTree(TreeItem> item) + private static void dumpTree(final TreeItem> item) { final ObservableList>> children = item.getChildren(); System.out.printf("item: %s , has %d children.\n", item.getValue().getName(), children.size()); 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 deleted file mode 100644 index febedc90cd..0000000000 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ConfigureComponentAction.java +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2018-2022 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 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; - -import javafx.application.Platform; -import javafx.scene.Node; -import javafx.scene.control.MenuItem; - -/** Action for editing configuration of an item - * @author Kay Kasemir - */ -@SuppressWarnings("nls") -public class ConfigureComponentAction extends MenuItem -{ - /** @param node Node to position dialog - * @param model Model where new component is added - * @param item Parent item in alarm tree - */ - public ConfigureComponentAction(final Node node, final AlarmClient model, final AlarmTreeItem item) - { - super("Configure Item", ImageCache.getImageView(AlarmSystem.class, "/icons/configure.png")); - setOnAction(event -> Platform.runLater(() -> - { - final ItemConfigDialog dialog = new ItemConfigDialog(model, item); - DialogHelper.positionDialog(dialog, node, -150, -300); - // Show dialog, not waiting for it to close with OK or Cancel - dialog.show(); - })); - } -} 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 20b82468c1..6d7877dddd 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 @@ -9,7 +9,6 @@ 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; @@ -32,7 +31,7 @@ class RemoveComponentAction extends MenuItem * @param model Model where components are to be removed * @param items Items to remove from alarm tree */ - public RemoveComponentAction(final Node node, final AlarmClient model, final List> items) + public RemoveComponentAction(final Node node, final AlarmClient model, final List> items, final AlarmTreeView alarmTreeView) { super("Remove Selected Items", ImageCache.getImageView(ImageCache.class, "/icons/delete.png")); @@ -61,6 +60,13 @@ public RemoveComponentAction(final Node node, final AlarmClient model, final Lis if (dialog.showAndWait().get() != ButtonType.OK) return; + // HACK: JavaFX's selection model does not enjoy the way that alarm editing works. Consider a scenario + // where the user selects an alarm, edits it, and then right clicks and removes it. This will trigger a + // deselect and also remove the item from the server, popping up an alert box for the user because there + // is no way for AlarmTreeView to distinguish this client deleting items from another client deleting items. + // So before deselecting the item, we need to notify alarm tree view that the changes are fine to discard + alarmTreeView.closePanel(); + // Tree view keeps the selection indices, which will point to wrong content // after those items have been removed if (node instanceof TreeView) @@ -73,7 +79,7 @@ public RemoveComponentAction(final Node node, final AlarmClient model, final Lis model.removeComponent(item); } catch (Exception e) { ExceptionDetailsErrorDialog.openError(Messages.error, - Messages.removeComponentFailed, + Messages.removeNodeFailed, e); // Breaking under the assumption that if one fails, all will fail break; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayDialog.java new file mode 100644 index 0000000000..7c6d81ee75 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayDialog.java @@ -0,0 +1,44 @@ +package org.phoebus.applications.alarm.ui.tree; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import org.phoebus.applications.alarm.model.TitleDetailDelay; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.applications.alarm.ui.config.TitleDetailDelayTableController; +import org.phoebus.framework.nls.NLS; + +import java.util.List; + + +public class TitleDetailDelayDialog extends Dialog> { + + public TitleDetailDelayDialog(final String name, final String headerText, final List initialValue) { + super(); + + setTitle(name); + setHeaderText(headerText); + setResizable(true); + getDialogPane().getButtonTypes().add(ButtonType.OK); + + final FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(NLS.getMessages(Messages.class)); + fxmlLoader.setLocation(TitleDetailDelayTableController.class.getResource("TitleDetailDelayTable.fxml")); + + final Node node; + try { + node = fxmlLoader.load(); + } catch (final Exception ex) { + throw new RuntimeException("Failed to load " + fxmlLoader.getLocation(), ex); + } + + final TitleDetailDelayTableController controller = fxmlLoader.getController(); + controller.setTitle(name); + controller.setItems(initialValue); + + getDialogPane().setContent(node); + + setResultConverter(_ -> controller.getItems().stream().map(x -> (TitleDetailDelay) x).toList()); + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDialog.java new file mode 100644 index 0000000000..65c263c0e4 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDialog.java @@ -0,0 +1,44 @@ +package org.phoebus.applications.alarm.ui.tree; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import org.phoebus.applications.alarm.model.TitleDetail; +import org.phoebus.applications.alarm.ui.Messages; +import org.phoebus.applications.alarm.ui.config.TitleDetailTableController; +import org.phoebus.framework.nls.NLS; + +import java.util.List; + + +public class TitleDetailDialog extends Dialog> { + + public TitleDetailDialog(final String name, final String headerText, final List initialValue) { + super(); + + setTitle(name); + setHeaderText(headerText); + setResizable(true); + getDialogPane().getButtonTypes().add(ButtonType.OK); + + final FXMLLoader fxmlLoader = new FXMLLoader(); + fxmlLoader.setResources(NLS.getMessages(Messages.class)); + fxmlLoader.setLocation(TitleDetailTableController.class.getResource("TitleDetailTable.fxml")); + + final Node node; + try { + node = fxmlLoader.load(); + } catch (final Exception ex) { + throw new RuntimeException("Failed to load " + fxmlLoader.getLocation(), ex); + } + + final TitleDetailTableController controller = fxmlLoader.getController(); + controller.setTitle(name); + controller.setItems(initialValue); + + getDialogPane().setContent(node); + + setResultConverter(_ -> controller.getItems().stream().map(x -> (TitleDetail) x).toList()); + } +} diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/alarm_node_config_editor.css b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/alarm_node_config_editor.css new file mode 100644 index 0000000000..e6301ce2b5 --- /dev/null +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/alarm_node_config_editor.css @@ -0,0 +1,33 @@ +.separator:horizontal .line +{ + -fx-background-color: null; + -fx-padding: 0.0em 0.0em 0.0em 0.0em; /* 1 0 0 0 */ + -fx-border-color: derive(-fx-background,-2%); +} + +.subheading +{ + -fx-font-weight: bold; + -fx-font-size: 10px; + -fx-background-color: moccasin; + -fx-padding: 2 5 2 5; + -fx-background-radius: 0; +} + +.heading +{ + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-background-color: darkslateblue ; + -fx-padding: 2 5 2 5; + -fx-background-radius: 0; +} + +.text-field.modified, +.button.modified, +.check-box.modified > .box, +.spinner.modified > .text-field, +.date-picker.modified > .text-field, +.date-picker.modified > .arrow-button { + -fx-background-color: #abc9e1; +} \ No newline at end of file diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ConfigDialog.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ConfigDialog.fxml deleted file mode 100644 index f16d936ca5..0000000000 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/ConfigDialog.fxml +++ /dev/null @@ -1,38 +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 deleted file mode 100644 index 1d3c54b08e..0000000000 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/LeafConfigDialog.fxml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/OptionsTables.fxml b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/OptionsTables.fxml deleted file mode 100644 index ba132ffbec..0000000000 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/config/OptionsTables.fxml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - -

- - - - - - -
- - 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 index ac47304753..002c737dfc 100644 --- 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 @@ -5,7 +5,7 @@ - +