diff --git a/app/save-and-restore/app/doc/images/configuration-editor.png b/app/save-and-restore/app/doc/images/configuration-editor.png index 2ae0b0422b..1aa47998f9 100644 Binary files a/app/save-and-restore/app/doc/images/configuration-editor.png and b/app/save-and-restore/app/doc/images/configuration-editor.png differ diff --git a/app/save-and-restore/app/doc/index.rst b/app/save-and-restore/app/doc/index.rst index b573884709..cc08a6da49 100644 --- a/app/save-and-restore/app/doc/index.rst +++ b/app/save-and-restore/app/doc/index.rst @@ -157,7 +157,7 @@ Configuration View ------------------ A new configuration is created from the context menu launched when right-clicking on a folder node in the tree view. -This will launch the configuration editor: +This screenshot shows the configuration editor: .. image:: images/configuration-editor.png :width: 80% @@ -170,6 +170,9 @@ with PVs in the order they appear. PV entries in a configuration marked as read only will be omitted whe performing a restore operation. +Compare Mode and Tolerance data is optional. This is used by the service when a client requests comparison between +stored and live values of a snapshot. More information on this feature is found in the service documentation. + To add a very large number of PVs, user should consider the import feature available via the "Import Configuration file to this folder" option in the context menu of a folder node in the tree view. diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index b7c00bd22b..660debeb9a 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -79,6 +79,7 @@ public class Messages { public static String duplicatePVNamesAdditionalItems; public static String duplicatePVNamesCheckFailed; public static String duplicatePVNamesFoundInSelection; + public static String duplicatePVNamesNotSupported; public static String Edit; public static String editFilter; public static String errorActionFailed; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigPvEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigPvEntry.java new file mode 100644 index 0000000000..8a57a6ed87 --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigPvEntry.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.ui.configuration; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import org.phoebus.applications.saveandrestore.model.Comparison; +import org.phoebus.applications.saveandrestore.model.ConfigPv; +import org.phoebus.applications.saveandrestore.model.ComparisonMode; + +import java.util.Objects; + +/** + * Wrapper around a {@link ConfigPv} instance for the purpose of facilitating + * configuration and data binding in (for instance) a {@link javafx.scene.control.TableView}. + */ +public class ConfigPvEntry implements Comparable { + + private final StringProperty pvNameProperty; + private final StringProperty readBackPvNameProperty; + private final BooleanProperty readOnlyProperty; + private final ObjectProperty comparisonModeProperty; + private final ObjectProperty toleranceProperty; + + public ConfigPvEntry(ConfigPv configPv) { + this.pvNameProperty = new SimpleStringProperty(this, "pvNameProperty", configPv.getPvName()); + this.readBackPvNameProperty = new SimpleStringProperty(configPv.getReadbackPvName()); + this.readOnlyProperty = new SimpleBooleanProperty(configPv.isReadOnly()); + this.comparisonModeProperty = new SimpleObjectProperty<>(configPv.getComparison() == null ? null : configPv.getComparison().getComparisonMode()); + this.toleranceProperty = new SimpleObjectProperty<>(configPv.getComparison() == null ? null : configPv.getComparison().getTolerance()); + } + + public StringProperty getPvNameProperty() { + return pvNameProperty; + } + + public StringProperty getReadBackPvNameProperty() { + return readBackPvNameProperty; + } + + public BooleanProperty getReadOnlyProperty() { + return readOnlyProperty; + } + + public ObjectProperty getComparisonModeProperty() { + return comparisonModeProperty; + } + + public ObjectProperty getToleranceProperty() { + return toleranceProperty; + } + + public void setPvNameProperty(String pvNameProperty) { + this.pvNameProperty.set(pvNameProperty); + } + + public void setReadBackPvNameProperty(String readBackPvNameProperty) { + this.readBackPvNameProperty.set(readBackPvNameProperty); + } + + public void setComparisonModeProperty(ComparisonMode comparisonModeProperty) { + this.comparisonModeProperty.set(comparisonModeProperty); + } + + public void setToleranceProperty(Double toleranceProperty) { + this.toleranceProperty.set(toleranceProperty ); + } + + public ConfigPv toConfigPv() { + ConfigPv configPv = ConfigPv.builder() + .pvName(pvNameProperty.get()) + .readbackPvName(readBackPvNameProperty.get()) + .readOnly(readOnlyProperty.get()) + .build(); + if(comparisonModeProperty.isNotNull().get() && toleranceProperty.isNotNull().get()){ + configPv.setComparison(new Comparison(comparisonModeProperty.get(), toleranceProperty.get())); + } + return configPv; + } + + @Override + public boolean equals(Object other) { + if (other instanceof ConfigPvEntry otherConfigPv) { + return Objects.equals(pvNameProperty, otherConfigPv.getPvNameProperty()) && + Objects.equals(readBackPvNameProperty, otherConfigPv.getReadBackPvNameProperty()) && + Objects.equals(readOnlyProperty.get(), otherConfigPv.getReadOnlyProperty().get()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(pvNameProperty, readBackPvNameProperty, readOnlyProperty); + } + + @Override + public int compareTo(ConfigPvEntry other) { + return pvNameProperty.get().compareTo(other.getPvNameProperty().get()); + } +} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java index 602a4c8c79..d5d9dbb831 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationController.java @@ -21,6 +21,7 @@ import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; @@ -28,6 +29,7 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; @@ -37,16 +39,18 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.SelectionMode; import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; -import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.control.cell.CheckBoxTableCell; +import javafx.scene.control.cell.ComboBoxTableCell; +import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; -import javafx.util.Callback; +import javafx.util.StringConverter; +import javafx.util.converter.DoubleStringConverter; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.SaveAndRestoreApplication; import org.phoebus.applications.saveandrestore.model.ConfigPv; @@ -54,10 +58,12 @@ import org.phoebus.applications.saveandrestore.model.ConfigurationData; import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.applications.saveandrestore.model.ComparisonMode; import org.phoebus.applications.saveandrestore.ui.NodeChangedListener; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.core.types.ProcessVariable; +import org.phoebus.framework.nls.NLS; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -65,11 +71,14 @@ import org.phoebus.ui.javafx.ImageCache; import org.phoebus.util.time.TimestampFormats; +import java.io.IOException; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.ResourceBundle; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; @@ -78,30 +87,55 @@ public class ConfigurationController extends SaveAndRestoreBaseController implements NodeChangedListener { @FXML + @SuppressWarnings("unused") private BorderPane root; @FXML - private TableColumn pvNameColumn; + @SuppressWarnings("unused") + private TableColumn pvNameColumn; @FXML - private TableView pvTable; + @SuppressWarnings("unused") + private TableColumn readbackPvNameColumn; @FXML + @SuppressWarnings("unused") + private TableColumn comparisonModeColumn; + + @FXML + @SuppressWarnings("unused") + private TableColumn toleranceColumn; + + @FXML + @SuppressWarnings("unused") + private TableColumn readOnlyColumn; + + @FXML + @SuppressWarnings("unused") + private TableView pvTable; + + @FXML + @SuppressWarnings("unused") private TextArea descriptionTextArea; @FXML + @SuppressWarnings("unused") private Button saveButton; @FXML + @SuppressWarnings("unused") private TextField pvNameField; @FXML + @SuppressWarnings("unused") private TextField readbackPvNameField; @FXML + @SuppressWarnings("unused") private Button addPvButton; @FXML + @SuppressWarnings("unused") private CheckBox readOnlyCheckBox; @FXML @@ -113,25 +147,28 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private final SimpleBooleanProperty readOnlyProperty = new SimpleBooleanProperty(false); @FXML + @SuppressWarnings("unused") private TextField configurationNameField; @FXML + @SuppressWarnings("unused") private Label configurationCreatedDateField; @FXML + @SuppressWarnings("unused") private Label configurationLastModifiedDateField; @FXML + @SuppressWarnings("unused") private Label createdByField; @FXML + @SuppressWarnings("unused") private Pane addPVsPane; private SaveAndRestoreService saveAndRestoreService; private static final Executor UI_EXECUTOR = Platform::runLater; - private final SimpleBooleanProperty dirty = new SimpleBooleanProperty(false); - - private final ObservableList configurationEntries = FXCollections.observableArrayList(); + private final ObservableList configurationEntries = FXCollections.observableArrayList(); private final SimpleBooleanProperty selectionEmpty = new SimpleBooleanProperty(false); private final SimpleStringProperty configurationDescriptionProperty = new SimpleStringProperty(); @@ -146,6 +183,9 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem private final Logger logger = Logger.getLogger(ConfigurationController.class.getName()); + private final BooleanProperty loadInProgress = new SimpleBooleanProperty(); + private final BooleanProperty dirty = new SimpleBooleanProperty(); + public ConfigurationController(ConfigurationTab configurationTab) { this.configurationTab = configurationTab; } @@ -155,16 +195,17 @@ public void initialize() { saveAndRestoreService = SaveAndRestoreService.getInstance(); + dirty.addListener((obs, o, n) -> configurationTab.annotateDirty(n)); + + pvTable.editableProperty().bind(userIdentity.isNull().not()); pvTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); pvTable.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> selectionEmpty.set(nv == null)); - ContextMenu pvNameContextMenu = new ContextMenu(); - MenuItem deleteMenuItem = new MenuItem(Messages.menuItemDeleteSelectedPVs, new ImageView(ImageCache.getImage(ConfigurationController.class, "/icons/delete.png"))); deleteMenuItem.setOnAction(ae -> { configurationEntries.removeAll(pvTable.getSelectionModel().getSelectedItems()); - configurationTab.annotateDirty(true); + //configurationTab.annotateDirty(true); pvTable.refresh(); }); @@ -172,50 +213,114 @@ public void initialize() { || userIdentity.isNull().get(), pvTable.getSelectionModel().getSelectedItems(), userIdentity)); - pvNameColumn.setEditable(true); - pvNameColumn.setCellValueFactory(new PropertyValueFactory<>("pvName")); + ContextMenu contextMenu = new ContextMenu(); + pvTable.setOnContextMenuRequested(event -> { + contextMenu.getItems().clear(); + contextMenu.getItems().addAll(deleteMenuItem); + contextMenu.getItems().add(new SeparatorMenuItem()); + ObservableList selectedPVs = pvTable.getSelectionModel().getSelectedItems(); + if (!selectedPVs.isEmpty()) { + List selectedPVList = selectedPVs.stream() + .map(tableEntry -> new ProcessVariable(tableEntry.getPvNameProperty().get())) + .collect(Collectors.toList()); + SelectionService.getInstance().setSelection(SaveAndRestoreApplication.NAME, selectedPVList); + ContextMenuHelper.addSupportedEntries(FocusUtil.setFocusOn(pvTable), contextMenu); + } + }); - pvNameColumn.setCellFactory(new Callback<>() { - @Override - public TableCell call(TableColumn param) { - final TableCell cell = new TableCell<>() { - @Override - public void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - selectionEmpty.set(empty); - if (empty) { - setText(null); - } else { - if (isEditing()) { - setText(null); - } else { - setText(getItem()); - setGraphic(null); - } - } + pvTable.setContextMenu(contextMenu); + + pvNameColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + pvNameColumn.setCellValueFactory(cell -> cell.getValue().getPvNameProperty()); + pvNameColumn.setOnEditCommit(t -> { + t.getTableView().getItems().get(t.getTablePosition().getRow()).setPvNameProperty(t.getNewValue()); + setDirty(true); + }); + + readbackPvNameColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + readbackPvNameColumn.setCellValueFactory(cell -> cell.getValue().getReadBackPvNameProperty()); + readbackPvNameColumn.setOnEditCommit(t -> { + t.getTableView().getItems().get(t.getTablePosition().getRow()).setReadBackPvNameProperty(t.getNewValue()); + setDirty(true); + }); + + readOnlyColumn.setCellFactory(CheckBoxTableCell.forTableColumn(readOnlyColumn)); + readOnlyColumn.setCellValueFactory(cell -> { + BooleanProperty readOnly = cell.getValue().getReadOnlyProperty(); + readOnly.addListener((obs, o, n) -> setDirty(true)); + return readOnly; + }); + + comparisonModeColumn.setCellValueFactory(cell -> cell.getValue().getComparisonModeProperty()); + comparisonModeColumn.setCellFactory(callback -> { + ObservableList values = FXCollections.observableArrayList(Arrays.stream(ComparisonMode.values()).toList()); + values.add(0, null); + ComboBoxTableCell tableCell = new ComboBoxTableCell<>(values) { + + @Override + public void commitEdit(ComparisonMode comparisonMode) { + getTableView().getItems().get(getIndex()).setComparisonModeProperty(comparisonMode); + if (comparisonMode == null) { + getTableView().getItems().get(getIndex()).setToleranceProperty(null); } - }; - cell.setOnContextMenuRequested(event -> { - pvNameContextMenu.hide(); - pvNameContextMenu.getItems().clear(); - pvNameContextMenu.getItems().addAll(deleteMenuItem); - pvNameContextMenu.getItems().add(new SeparatorMenuItem()); - ObservableList selectedPVs = pvTable.getSelectionModel().getSelectedItems(); - if (!selectedPVs.isEmpty()) { - List selectedPVList = selectedPVs.stream() - .map(tableEntry -> new ProcessVariable(tableEntry.getPvName())) - .collect(Collectors.toList()); - SelectionService.getInstance().setSelection(SaveAndRestoreApplication.NAME, selectedPVList); - - ContextMenuHelper.addSupportedEntries(FocusUtil.setFocusOn(cell), pvNameContextMenu); + setDirty(true); + super.commitEdit(comparisonMode); + } + }; + + StringConverter converter = new StringConverter<>() { + @Override + public String toString(ComparisonMode object) { + if (object == null) { + return ""; } - pvNameContextMenu.show(cell, event.getScreenX(), event.getScreenY()); - }); - cell.setContextMenu(pvNameContextMenu); + return object.toString(); + } + + @Override + public ComparisonMode fromString(String string) { + if (string == null) { + return null; + } + return ComparisonMode.valueOf(string); + } + }; + tableCell.setConverter(converter); + return tableCell; + }); + + toleranceColumn.setCellFactory(callback -> new TextFieldTableCell<>(new DoubleStringConverter() { + @Override + public String toString(Double value) { + return value == null ? null : value.toString(); + } - return cell; + @Override + public Double fromString(String string) { + try { + double value = Double.parseDouble(string); + if(value >= 0){ + // Tolerance must be >= 0. + return value; + } + return null; + } catch (Exception e) { + // No logging needed: user has entered text that cannot be parsed as double. + return null; + } + } + }) { + @Override + public void commitEdit(Double value) { + if (value == null) { + return; + } + getTableView().getItems().get(getIndex()).setToleranceProperty(value); + setDirty(true); + super.commitEdit(value); } }); + toleranceColumn.setCellValueFactory(cell -> cell.getValue().getToleranceProperty()); pvNameField.textProperty().bindBidirectional(pvNameProperty); readbackPvNameField.textProperty().bindBidirectional(readbackPvNameProperty); @@ -224,17 +329,17 @@ public void updateItem(String item, boolean empty) { descriptionTextArea.textProperty().bindBidirectional(configurationDescriptionProperty); descriptionTextArea.disableProperty().bind(userIdentity.isNull()); - configurationEntries.addListener((ListChangeListener) change -> { + configurationEntries.addListener((ListChangeListener) change -> { while (change.next()) { if (change.wasAdded() || change.wasRemoved()) { FXCollections.sort(configurationEntries); - dirty.set(true); + setDirty(true); } } }); - configurationNameProperty.addListener((observableValue, oldValue, newValue) -> dirty.set(!newValue.equals(configurationNode.getName()))); - configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> dirty.set(!newValue.equals(configurationNode.get().getDescription()))); + configurationNameProperty.addListener((observableValue, oldValue, newValue) -> setDirty(!newValue.equals(configurationNode.getName()))); + configurationDescriptionProperty.addListener((observable, oldValue, newValue) -> setDirty(!newValue.equals(configurationNode.get().getDescription()))); saveButton.disableProperty().bind(Bindings.createBooleanBinding(() -> dirty.not().get() || configurationDescriptionProperty.isEmpty().get() || @@ -242,7 +347,10 @@ public void updateItem(String item, boolean empty) { userIdentity.isNull().get(), dirty, configurationDescriptionProperty, configurationNameProperty, userIdentity)); - addPvButton.disableProperty().bind(pvNameField.textProperty().isEmpty()); + addPvButton.disableProperty().bind(Bindings.createBooleanBinding(() -> + pvNameField.textProperty().isEmpty().get() && + readbackPvNameField.textProperty().isEmpty().get(), + pvNameField.textProperty(), readbackPvNameField.textProperty())); readOnlyCheckBox.selectedProperty().bindBidirectional(readOnlyProperty); @@ -268,12 +376,13 @@ public void updateItem(String item, boolean empty) { } @FXML + @SuppressWarnings("unused") public void saveConfiguration() { UI_EXECUTOR.execute(() -> { try { configurationNode.get().setName(configurationNameProperty.get()); configurationNode.get().setDescription(configurationDescriptionProperty.get()); - configurationData.setPvList(configurationEntries); + configurationData.setPvList(configurationEntries.stream().map(ConfigPvEntry::toConfigPv).toList()); Configuration configuration = new Configuration(); configuration.setConfigurationNode(configurationNode.get()); configuration.setConfigurationData(configurationData); @@ -281,12 +390,10 @@ public void saveConfiguration() { configuration = saveAndRestoreService.createConfiguration(configurationNodeParent, configuration); configurationTab.setId(configuration.getConfigurationNode().getUniqueId()); - configurationTab.updateTabTitle(configuration.getConfigurationNode().getName()); } else { configuration = saveAndRestoreService.updateConfiguration(configuration); } configurationData = configuration.getConfigurationData(); - dirty.set(false); loadConfiguration(configuration.getConfigurationNode()); } catch (Exception e1) { ExceptionDetailsErrorDialog.openError(pvTable, @@ -298,6 +405,7 @@ public void saveConfiguration() { } @FXML + @SuppressWarnings("unused") public void addPv() { UI_EXECUTOR.execute(() -> { @@ -305,27 +413,44 @@ public void addPv() { String[] pvNames = pvNameProperty.get().trim().split("[\\s;]+"); String[] readbackPvNames = readbackPvNameProperty.get().trim().split("[\\s;]+"); - ArrayList configPVs = new ArrayList<>(); + if(!checkForDuplicatePvNames(pvNames)){ + return; + } + + ArrayList configPVs = new ArrayList<>(); for (int i = 0; i < pvNames.length; i++) { - // Disallow duplicate PV names as in a restore operation this would mean that a PV is written - // multiple times, possibly with different values. String pvName = pvNames[i].trim(); - if (configurationEntries.stream().anyMatch(s -> s.getPvName().equals(pvName))) { - continue; - } String readbackPV = i >= readbackPvNames.length ? null : readbackPvNames[i] == null || readbackPvNames[i].isEmpty() ? null : readbackPvNames[i].trim(); ConfigPv configPV = ConfigPv.builder() .pvName(pvName) .readOnly(readOnlyProperty.get()) .readbackPvName(readbackPV) .build(); - configPVs.add(configPV); + configPVs.add(new ConfigPvEntry(configPV)); } configurationEntries.addAll(configPVs); - configurationTab.annotateDirty(true); resetAddPv(); }); + } + /** + * Checks that added PV names are not added multiple times + * @param addedPvNames New PV names added in the UI + * @return true if no duplicates are detected, otherwise false + */ + private boolean checkForDuplicatePvNames(String[] addedPvNames){ + List pvNamesAsList = new ArrayList<>(); + pvNamesAsList.addAll(Arrays.asList(addedPvNames)); + pvTable.itemsProperty().get().forEach(i -> pvNamesAsList.add(i.getPvNameProperty().get())); + List duplicatePvNames = pvNamesAsList.stream().filter(n -> Collections.frequency(pvNamesAsList, n) > 1).toList(); + + if(duplicatePvNames.size() > 0){ + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText(Messages.duplicatePVNamesNotSupported); + alert.showAndWait(); + return false; + } + return true; } private void resetAddPv() { @@ -348,7 +473,7 @@ public void newConfiguration(Node parentNode) { configurationData = new ConfigurationData(); pvTable.setItems(configurationEntries); UI_EXECUTOR.execute(() -> configurationNameField.requestFocus()); - dirty.set(false); + setDirty(false); } /** @@ -363,6 +488,7 @@ public void loadConfiguration(final Node node) { ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e); return; } + loadInProgress.set(true); // Create a cloned Node object to avoid changes in the Node object contained in the tree view. configurationNode.set(Node.builder().uniqueId(node.getUniqueId()) .name(node.getName()) @@ -372,16 +498,16 @@ public void loadConfiguration(final Node node) { .created(node.getCreated()) .lastModified(node.getLastModified()) .build()); - loadConfigurationData(); + loadConfigurationData(() -> loadInProgress.set(false)); } - private void loadConfigurationData() { + private void loadConfigurationData(Runnable completion) { UI_EXECUTOR.execute(() -> { try { Collections.sort(configurationData.getPvList()); - configurationEntries.setAll(configurationData.getPvList()); + configurationEntries.setAll(configurationData.getPvList().stream().map(ConfigPvEntry::new).toList()); pvTable.setItems(configurationEntries); - dirty.set(false); + completion.run(); } catch (Exception e) { logger.log(Level.WARNING, "Unable to load existing configuration"); } @@ -412,4 +538,8 @@ public void nodeChanged(Node node) { .build()); } } + + private void setDirty(boolean dirty) { + this.dirty.set(dirty && !loadInProgress.get()); + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java index ad4d54cba9..572a7f4a2c 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationTab.java @@ -95,7 +95,7 @@ public void configureForNewConfiguration(Node parentNode) { @Override public void nodeChanged(Node node) { if (node.getUniqueId().equals(getId())) { - textProperty().set(node.getName()); + Platform.runLater(() -> textProperty().set(node.getName())); } } @@ -104,15 +104,21 @@ public void nodeChanged(Node node) { * * @param tabTitle The wanted tab title. */ - public void updateTabTitle(String tabTitle) { + private void updateTabTitle(String tabTitle) { Platform.runLater(() -> textProperty().set(tabTitle)); } + /** + * Updates the tab to indicate if the data is dirty and needs to be saved. + * @param dirty If true, an asterisk is prepended, otherwise + * only the name {@link org.phoebus.applications.saveandrestore.model.Configuration} + * is rendered. + */ public void annotateDirty(boolean dirty) { String tabTitle = textProperty().get(); - if (dirty && !tabTitle.contains("*")) { + if (dirty && !tabTitle.startsWith("*")) { updateTabTitle("* " + tabTitle); - } else if (!dirty) { + } else if (!dirty && tabTitle.startsWith("*")) { updateTabTitle(tabTitle.substring(2)); } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 0b54021a05..4936a88f4c 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -19,6 +19,8 @@ closeConfigurationWarning=Configuration modified, but not saved. Do you wish to closeCompositeSnapshotWarning=Composite snapshot modified, but not saved. Do you wish to continue? closeTabPrompt=Close tab? comment=Comment +comparisonMode=Comparison Mode +comparisonTolerance=Tolerance compositeSnapshotConsistencyCheckFailed=Failed to check consistency for composite snapshot copy=Copy createdDate=Created @@ -67,8 +69,9 @@ deleteFilterFailed=Failed to delete filter description=Description descriptionHint=Specify non-empty description duplicatePVNamesAdditionalItems={0} additional PV names -duplicatePVNamesFoundInSelection=Cannot add selected snapshots, duplicate PV names found duplicatePVNamesCheckFailed=Cannot check if snapshots may be added, remote service off-line? +duplicatePVNamesFoundInSelection=Cannot add selected snapshots, duplicate PV names found +duplicatePVNamesNotSupported=Duplicate PV names detected. This is not supported. editFilter=Edit filter errorActionFailed=Action failed errorAddTagFailed=Failed to add tag diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml index 0080db9c83..70ac3d2d65 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/configuration/ConfigurationEditor.fxml @@ -5,7 +5,8 @@ - +
@@ -53,25 +54,15 @@