From ff4be7275e8b1fc89bafb1006e9a742837443783 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 8 Jul 2025 14:37:07 +0200 Subject: [PATCH 1/6] Refactoring/cleanup --- .../ui/snapshot/SaveAndRestorePV.java | 13 +-- .../ui/snapshot/SnapshotController.java | 83 ++++++++++++------- .../ui/snapshot/TableEntry.java | 32 ++++++- 3 files changed, 90 insertions(+), 38 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java index 1c3c00c910..429a4b6706 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java @@ -45,7 +45,7 @@ public class SaveAndRestorePV { private PV readbackPv; private VType pvValue = VDisconnectedData.INSTANCE; private VType readbackValue = VDisconnectedData.INSTANCE; - private TableEntry snapshotTableEntry; + //private TableEntry snapshotTableEntry; /** * The time between updates of dynamic data in the table, in ms. @@ -53,7 +53,7 @@ public class SaveAndRestorePV { private static final long TABLE_UPDATE_INTERVAL = 500; protected SaveAndRestorePV(TableEntry snapshotTableEntry) { - this.snapshotTableEntry = snapshotTableEntry; + //this.snapshotTableEntry = snapshotTableEntry; this.pvName = patchPvName(snapshotTableEntry.pvNameProperty().get()); this.readbackPvName = patchPvName(snapshotTableEntry.readbackNameProperty().get()); @@ -61,7 +61,7 @@ protected SaveAndRestorePV(TableEntry snapshotTableEntry) { pv = PVPool.getPV(pvName); pv.onValueEvent().throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS).subscribe(value -> { pvValue = org.phoebus.pv.PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value; - this.snapshotTableEntry.setLiveValue(pvValue); + snapshotTableEntry.setLiveValue(pvValue); }); if (readbackPvName != null && !readbackPvName.isEmpty()) { @@ -70,11 +70,11 @@ protected SaveAndRestorePV(TableEntry snapshotTableEntry) { .throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) .subscribe(value -> { this.readbackValue = org.phoebus.pv.PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value; - this.snapshotTableEntry.setReadbackValue(this.readbackValue); + snapshotTableEntry.setReadbackValue(this.readbackValue); }); } else { // If configuration does not define read-back PV, then UI should show "no data" rather than "disconnected" - this.snapshotTableEntry.setReadbackValue(VNoData.INSTANCE); + snapshotTableEntry.setReadbackValue(VNoData.INSTANCE); } } catch (Exception e) { Logger.getLogger(SaveAndRestorePV.class.getName()).log(Level.INFO, "Error connecting to PV", e); @@ -99,11 +99,14 @@ public void countDown() { this.countDownLatch.countDown(); } + /* public void setSnapshotTableEntry(TableEntry snapshotTableEntry) { this.snapshotTableEntry = snapshotTableEntry; this.snapshotTableEntry.setLiveValue(pv.read()); } + */ + void dispose() { if (pv != null) { PVPool.releasePV(pv); diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index b36d4b372b..16bb616aab 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -280,7 +280,7 @@ public class SnapshotController extends SaveAndRestoreBaseController implements @FXML protected VBox progressIndicator; - protected final Map pvs = new HashMap<>(); + //protected final Map pvs = new HashMap<>(); protected Node configurationNode; @@ -347,6 +347,7 @@ public class SnapshotController extends SaveAndRestoreBaseController implements * determine which elements in the {@link Map} to actually represent. */ protected final Map tableEntryItems = new LinkedHashMap<>(); + //protected final List tableEntries = new ArrayList<>(); public SnapshotController(SnapshotTab snapshotTab) { snapshotTab.textProperty().bind(tabTitleProperty); @@ -484,10 +485,17 @@ public void initialize() { this.showDeltaPercentage.set(n)); hideEqualItemsButton.setGraphic(new ImageView(new Image(getClass().getResourceAsStream("/icons/hide_show_equal_items.png")))); + hideEqualItemsButton.selectedProperty().bindBidirectional(hideEqualItemsProperty); + hideEqualItemsProperty.addListener((obs, o, n) -> updateTable()); + /* hideEqualItemsProperty.bind(hideEqualItemsButton.selectedProperty()); hideEqualItemsButton.selectedProperty() .addListener((a, o, n) -> + + hideEqualItems()); + + */ logAction.selectedProperty().bindBidirectional(logActionProperty); @@ -876,7 +884,8 @@ public boolean handleTabClosed() { * Releases PV resources. */ private void dispose() { - pvs.values().forEach(SaveAndRestorePV::dispose); + //pvs.values().forEach(SaveAndRestorePV::dispose); + tableEntryItems.values().forEach(tableEntry -> tableEntry.getSaveAndRestorePV().dispose()); } private void showLoggingError(String cause) { @@ -1265,11 +1274,14 @@ private void applyPreserveSelection(boolean preserve) { } } + /* private void hideEqualItems() { ArrayList arrayList = new ArrayList<>(tableEntryItems.values()); Platform.runLater(() -> updateTable(arrayList)); } + */ + /** * Restores a snapshot from client or service. * @@ -1418,29 +1430,28 @@ private void addSnapshot(Snapshot snapshot) { for (int s = 1; s < snapshots.size(); s++) { Node snapshotNode = snapshots.get(s).getSnapshotNode(); String snapshotName = snapshotNode.getName(); - List entries = snapshot.getSnapshotData().getSnapshotItems(); String nodeName; TableEntry tableEntry; // Base snapshot data List baseSnapshotTableEntries = new ArrayList<>(tableEntryItems.values()); - SnapshotItem entry; + SnapshotItem snpshotItem; for (int i = 0; i < entries.size(); i++) { - entry = entries.get(i); - nodeName = entry.getConfigPv().getPvName(); + snpshotItem = entries.get(i); + nodeName = snpshotItem.getConfigPv().getPvName(); tableEntry = tableEntryItems.get(nodeName); // tableEntry is null if the added snapshot has more items than the base snapshot. if (tableEntry == null) { - tableEntry = new TableEntry(); + tableEntry = new TableEntry(snpshotItem); tableEntry.idProperty().setValue(tableEntryItems.size() + i + 1); - tableEntry.pvNameProperty().setValue(nodeName); - tableEntry.setConfigPv(entry.getConfigPv()); + //tableEntry.pvNameProperty().setValue(nodeName); + //tableEntry.setConfigPv(snpshotItem.getConfigPv()); tableEntryItems.put(nodeName, tableEntry); - tableEntry.readbackNameProperty().set(entry.getConfigPv().getReadbackPvName()); + //tableEntry.readbackNameProperty().set(snpshotItem.getConfigPv().getReadbackPvName()); } - tableEntry.setSnapshotValue(entry.getValue(), snapshots.size()); - tableEntry.setStoredReadbackValue(entry.getReadbackValue(), snapshots.size()); - tableEntry.readOnlyProperty().set(entry.getConfigPv().isReadOnly()); + tableEntry.setSnapshotValue(snpshotItem.getValue(), snapshots.size()); + tableEntry.setStoredReadbackValue(snpshotItem.getReadbackValue(), snapshots.size()); + //tableEntry.readOnlyProperty().set(snpshotItem.getConfigPv().isReadOnly()); baseSnapshotTableEntries.remove(tableEntry); } // If added snapshot has more items than base snapshot, the base snapshot's values for those @@ -1487,8 +1498,8 @@ private void addSnapshot(Snapshot snapshot) { snapshotTableView.getColumns().addAll(columns); - connectPVs(); - updateTable(null); + //connectPVs(); + updateTable(); } private void showSnapshotInTable() { @@ -1498,36 +1509,43 @@ private void showSnapshotInTable() { snapshots.set(0, snapshot); } AtomicInteger counter = new AtomicInteger(0); - snapshot.getSnapshotData().getSnapshotItems().forEach(entry -> { - TableEntry tableEntry = new TableEntry(); - String name = entry.getConfigPv().getPvName(); + snapshot.getSnapshotData().getSnapshotItems().forEach(snapshotItem -> { + TableEntry tableEntry = new TableEntry(snapshotItem); + String name = snapshotItem.getConfigPv().getPvName(); tableEntry.idProperty().setValue(counter.incrementAndGet()); - tableEntry.pvNameProperty().setValue(name); - tableEntry.setConfigPv(entry.getConfigPv()); - tableEntry.setSnapshotValue(entry.getValue(), 0); - tableEntry.setStoredReadbackValue(entry.getReadbackValue(), 0); - tableEntry.setReadbackValue(entry.getReadbackValue()); - if (entry.getValue() == null || entry.getValue().equals(VDisconnectedData.INSTANCE)) { + //tableEntry.pvNameProperty().setValue(name); + //tableEntry.setConfigPv(snapshotItem.getConfigPv()); + tableEntry.setSnapshotValue(snapshotItem.getValue(), 0); + tableEntry.setStoredReadbackValue(snapshotItem.getReadbackValue(), 0); + //tableEntry.setReadbackValue(snapshotItem.getReadbackValue()); + /* + if (snapshotItem.getValue() == null || snapshotItem.getValue().equals(VDisconnectedData.INSTANCE)) { tableEntry.setActionResult(ActionResult.FAILED); } else { tableEntry.setActionResult(ActionResult.OK); } - if (entry.getConfigPv().getReadbackPvName() != null){ - if(entry.getReadbackValue() == null || entry.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { + if (snapshotItem.getConfigPv().getReadbackPvName() != null){ + if(snapshotItem.getReadbackValue() == null || snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { tableEntry.setActionResultReadback(ActionResult.FAILED); } else{ tableEntry.setActionResultReadback(ActionResult.OK); } } - tableEntry.readbackNameProperty().set(entry.getConfigPv().getReadbackPvName()); - tableEntry.readOnlyProperty().set(entry.getConfigPv().isReadOnly()); + + */ + //tableEntry.readbackNameProperty().set(snapshotItem.getConfigPv().getReadbackPvName()); + //tableEntry.readOnlyProperty().set(snapshotItem.getConfigPv().isReadOnly()); tableEntryItems.put(name, tableEntry); }); - updateTable(null); - connectPVs(); + updateTable(); + //connectPVs(); + } + + private void updateTable(){ + updateTable(tableEntryItems.values().stream().toList()); } /** @@ -1540,7 +1558,7 @@ private void updateTable(List entries) { final boolean notHide = hideEqualItemsProperty.not().get(); Platform.runLater(() -> { items.clear(); - tableEntryItems.forEach((key, value) -> { + entries.forEach(value -> { // there is no harm if this is executed more than once, because only one line is allowed for these // two properties (see SingleListenerBooleanProperty for more details) value.liveStoredEqualProperty().addListener((a, o, n) -> { @@ -1579,6 +1597,7 @@ private int measureStringWidth(String text, Font font) { * Attempts to connect to all the PVs of the configuration/snapshot and binds the created {@link SaveAndRestorePV} objects * to the {@link TableEntry} objects matched on PV name. */ + /* private void connectPVs() { JobManager.schedule("Connect PVs", monitor -> tableEntryItems.values().forEach(e -> { SaveAndRestorePV pv = pvs.get(e.getConfigPv().getPvName()); @@ -1590,6 +1609,8 @@ private void connectPVs() { })); } + */ + /** * * @param configurationData {@link ConfigurationData} obejct of a {@link org.phoebus.applications.saveandrestore.model.Configuration} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java index 8687d40c90..d81eb352c3 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java @@ -28,6 +28,7 @@ import org.epics.vtype.VNumberArray; import org.epics.vtype.VType; import org.phoebus.applications.saveandrestore.model.ConfigPv; +import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.applications.saveandrestore.ui.SingleListenerBooleanProperty; import org.phoebus.saveandrestore.util.Threshold; import org.phoebus.saveandrestore.util.Utilities; @@ -101,13 +102,13 @@ public class TableEntry { */ private final ObjectProperty actionResultReadback = new SimpleObjectProperty<>(this, "actionResultReadback", ActionResult.PENDING); - + private SaveAndRestorePV saveAndRestorePV; private ConfigPv configPv; /** * Construct a new table entry. */ - public TableEntry() { + public TableEntry(SnapshotItem snapshotItem) { //when read only is set to true, unselect this PV readOnly.addListener((a, o, n) -> { if (n) { @@ -120,14 +121,41 @@ public TableEntry() { selected.set(false); } }); + readOnlyProperty().setValue(snapshotItem.getConfigPv().isReadOnly()); + pvNameProperty().set(snapshotItem.getConfigPv().getPvName()); + readbackNameProperty().set(snapshotItem.getConfigPv().getReadbackPvName()); + setReadbackValue(snapshotItem.getReadbackValue()); + if (snapshotItem.getValue() == null || snapshotItem.getValue().equals(VDisconnectedData.INSTANCE)) { + setActionResult(ActionResult.FAILED); + } + else { + setActionResult(ActionResult.OK); + } + if (snapshotItem.getConfigPv().getReadbackPvName() != null){ + if(snapshotItem.getReadbackValue() == null || snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { + setActionResultReadback(ActionResult.FAILED); + } + else{ + setActionResultReadback(ActionResult.OK); + } + } + this.configPv = snapshotItem.getConfigPv(); + this.saveAndRestorePV = new SaveAndRestorePV(this); + } + + public SaveAndRestorePV getSaveAndRestorePV(){ + return saveAndRestorePV; } + /* public void setConfigPv(ConfigPv configPv) { this.configPv = configPv; pvName.setValue(configPv.getPvName()); readbackName.setValue(configPv.getReadbackPvName()); } + */ + public ConfigPv getConfigPv() { return configPv; } From b41a47755f13773641cfcc8684922d4028c72686 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 11 Jul 2025 15:06:46 +0200 Subject: [PATCH 2/6] Refactoring to improve code readability in large class --- .../ui/snapshot/SaveAndRestorePV.java | 138 ------ .../ui/snapshot/SnapshotController.java | 417 +++++++----------- .../ui/snapshot/TableEntry.java | 59 ++- 3 files changed, 207 insertions(+), 407 deletions(-) delete mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java deleted file mode 100644 index 429a4b6706..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SaveAndRestorePV.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2020 European Spallation Source ERIC. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - * - */ - -package org.phoebus.applications.saveandrestore.ui.snapshot; - -import org.epics.vtype.VType; -import org.phoebus.saveandrestore.util.VNoData; -import org.phoebus.core.vtypes.VDisconnectedData; -import org.phoebus.pv.PV; -import org.phoebus.pv.PVPool; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Utility class binding the snapshot table views to a {@link PV} for each row such - * that the view can show the live values. A hardcoded throttling time of 500ms keeps - * update rate of PV values under control. - */ -public class SaveAndRestorePV { - - private final String pvName; - - private final String readbackPvName; - private CountDownLatch countDownLatch; - private PV pv; - private PV readbackPv; - private VType pvValue = VDisconnectedData.INSTANCE; - private VType readbackValue = VDisconnectedData.INSTANCE; - //private TableEntry snapshotTableEntry; - - /** - * The time between updates of dynamic data in the table, in ms. - */ - private static final long TABLE_UPDATE_INTERVAL = 500; - - protected SaveAndRestorePV(TableEntry snapshotTableEntry) { - //this.snapshotTableEntry = snapshotTableEntry; - this.pvName = patchPvName(snapshotTableEntry.pvNameProperty().get()); - this.readbackPvName = patchPvName(snapshotTableEntry.readbackNameProperty().get()); - - try { - pv = PVPool.getPV(pvName); - pv.onValueEvent().throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS).subscribe(value -> { - pvValue = org.phoebus.pv.PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value; - snapshotTableEntry.setLiveValue(pvValue); - }); - - if (readbackPvName != null && !readbackPvName.isEmpty()) { - readbackPv = PVPool.getPV(readbackPvName); - readbackPv.onValueEvent() - .throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) - .subscribe(value -> { - this.readbackValue = org.phoebus.pv.PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value; - snapshotTableEntry.setReadbackValue(this.readbackValue); - }); - } else { - // If configuration does not define read-back PV, then UI should show "no data" rather than "disconnected" - snapshotTableEntry.setReadbackValue(VNoData.INSTANCE); - } - } catch (Exception e) { - Logger.getLogger(SaveAndRestorePV.class.getName()).log(Level.INFO, "Error connecting to PV", e); - } - } - - private String patchPvName(String pvName) { - if (pvName == null || pvName.isEmpty()) { - return null; - } else if (pvName.startsWith("ca://") || pvName.startsWith("pva://")) { - return pvName.substring(pvName.lastIndexOf('/') + 1); - } else { - return pvName; - } - } - - public void setCountDownLatch(CountDownLatch countDownLatch) { - this.countDownLatch = countDownLatch; - } - - public void countDown() { - this.countDownLatch.countDown(); - } - - /* - public void setSnapshotTableEntry(TableEntry snapshotTableEntry) { - this.snapshotTableEntry = snapshotTableEntry; - this.snapshotTableEntry.setLiveValue(pv.read()); - } - - */ - - void dispose() { - if (pv != null) { - PVPool.releasePV(pv); - } - if (readbackPv != null) { - PVPool.releasePV(readbackPv); - } - } - - public PV getPv() { - return pv; - } - - public PV getReadbackPv() { - return readbackPv; - } - - public String getPvName() { - return pvName; - } - - public String getReadbackPvName() { - return readbackPvName; - } - - public VType getReadbackValue() { - return readbackValue; - } -} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index 16bb616aab..3998927938 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -5,11 +5,13 @@ import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -105,10 +107,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; import java.util.concurrent.atomic.AtomicBoolean; @@ -280,8 +279,6 @@ public class SnapshotController extends SaveAndRestoreBaseController implements @FXML protected VBox progressIndicator; - //protected final Map pvs = new HashMap<>(); - protected Node configurationNode; public static final Logger LOGGER = Logger.getLogger(SnapshotController.class.getName()); @@ -292,7 +289,6 @@ public class SnapshotController extends SaveAndRestoreBaseController implements private final SimpleStringProperty tabIdProperty = new SimpleStringProperty(); private final SimpleObjectProperty tabGraphicImageProperty = new SimpleObjectProperty<>(); - private List> regexPatterns = new ArrayList<>(); protected final SimpleStringProperty snapshotNameProperty = new SimpleStringProperty(); private final SimpleStringProperty snapshotCommentProperty = new SimpleStringProperty(); @@ -312,10 +308,9 @@ public class SnapshotController extends SaveAndRestoreBaseController implements private final SimpleObjectProperty restoreModeProperty = new SimpleObjectProperty<>(RestoreMode.CLIENT_RESTORE); /** - * List of snapshots used managed in this controller. Index 0 is always the base snapshot, - * all others are snapshots added in the compare use-case. + * List of snapshots added when user chooses to compare base snapshot with other snapshots. */ - protected List snapshots = new ArrayList<>(); + protected List additionalSnapshots = new ArrayList<>(); private final SimpleBooleanProperty selectionInverted = new SimpleBooleanProperty(false); private final SimpleBooleanProperty showReadbacks = new SimpleBooleanProperty(false); @@ -326,6 +321,16 @@ public class SnapshotController extends SaveAndRestoreBaseController implements */ private final SimpleObjectProperty nodeTypeProperty = new SimpleObjectProperty<>(NodeType.SNAPSHOT); + /** + * {@link StringProperty} holding the text of the filter {@link TextField}. + */ + private final StringProperty filterTextProperty = new SimpleStringProperty(""); + + /** + * {@link BooleanProperty} holding state of the Preserve selection checkbox + */ + private final BooleanProperty preserveSelectionProperty = new SimpleBooleanProperty(false); + private SnapshotUtil snapshotUtil; /** * Used to disable portions of the UI when long-lasting operations are in progress, e.g. @@ -334,20 +339,22 @@ public class SnapshotController extends SaveAndRestoreBaseController implements protected final SimpleBooleanProperty disabledUi = new SimpleBooleanProperty(false); /** - * The final {@link Snapshot} object holding the data for this controller. When user performs operations (loading, - * taking and saving snapshots), the fields of this object are updated accordingly. + * The {@link Snapshot} object holding the data for this controller. */ - private final Snapshot snapshot = new Snapshot(); - + private Snapshot snapshot; /** - * {@link Map} of {@link TableEntry} items corresponding to the snapshot data, i.e. - * one per PV as defined in the snapshot's configuration. This map is used to + * {@link List} of {@link TableEntry} items corresponding to the snapshot data, i.e. + * one per PV as defined in the snapshot's configuration. This {@link List} is used to * populate the {@link TableView}, but other parameters (e.g. hideEqualItems) may - * determine which elements in the {@link Map} to actually represent. + * determine which elements in the {@link List} to actually represent. + * + *

+ * Note that the list is cleared and recreated whenever snapshot data has changed, i.e. + * when retrieved from service or when taking a snapshot. + *

*/ - protected final Map tableEntryItems = new LinkedHashMap<>(); - //protected final List tableEntries = new ArrayList<>(); + protected final List tableEntryItems = new ArrayList<>(); public SnapshotController(SnapshotTab snapshotTab) { snapshotTab.textProperty().bind(tabTitleProperty); @@ -357,14 +364,9 @@ public SnapshotController(SnapshotTab snapshotTab) { snapshotTab.setGraphic(imageView); } - - @FXML public void initialize() { - Node snapshotNode = Node.builder().nodeType(NodeType.SNAPSHOT).build(); - snapshot.setSnapshotNode(snapshotNode); - // Locate registered SaveAndRestoreEventReceivers eventReceivers = ServiceLoader.load(SaveAndRestoreEventReceiver.class); progressIndicator.visibleProperty().bind(disabledUi); @@ -397,7 +399,7 @@ public void initialize() { saveSnapshotButton.disableProperty().bind(Bindings.createBooleanBinding(() -> // TODO: support save (=update) a composite snapshot from the snapshot view. In the meanwhile, disable save button. - snapshotDataDirty.not().get() || + snapshotDataDirty.not().get() || snapshotNameProperty.isEmpty().get() || snapshotCommentProperty.isEmpty().get() || userIdentity.isNull().get(), @@ -444,32 +446,13 @@ public void initialize() { String filterShortcutName = (new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)).getDisplayText(); filterTextField.setPromptText("* for all matching and , as or separator, & as and separator. Start with / for regex. All if empty. (" + filterShortcutName + ")"); - - filterTextField.addEventHandler(KeyEvent.ANY, event -> { - String filterText = filterTextField.getText().trim(); - - List filters = Arrays.asList(filterText.split(",")); - regexPatterns = filters.stream() - .map(item -> { - if (item.startsWith("/")) { - return List.of(Pattern.compile(item.substring(1, item.length() - 1).trim())); - } else { - return Arrays.stream(item.split("&")) - .map(andItem -> andItem.replaceAll("\\*", ".*")) - .map(andItem -> Pattern.compile(andItem.trim())) - .collect(Collectors.toList()); - } - }).collect(Collectors.toList()); - - applyFilter(filterText, preserveSelectionCheckBox.isSelected(), regexPatterns); - }); - - preserveSelectionCheckBox.selectedProperty() - .addListener((observableValue, aBoolean, isSelected) -> applyPreserveSelection(isSelected)); + filterTextField.textProperty().bindBidirectional(filterTextProperty); + filterTextProperty.addListener((obs, o, n) -> applyFilter()); + preserveSelectionCheckBox.selectedProperty().bindBidirectional(preserveSelectionProperty); showLiveReadbackButton.setGraphic(new ImageView(new Image(getClass().getResourceAsStream("/icons/show_live_readback_column.png")))); showLiveReadbackButton.selectedProperty() - .addListener((a, o, n) ->{ + .addListener((a, o, n) -> { this.showReadbacks.set(n); actionResultReadbackColumn.visibleProperty().setValue(actionResultReadbackColumn.getGraphic() != null); }); @@ -487,15 +470,6 @@ public void initialize() { hideEqualItemsButton.setGraphic(new ImageView(new Image(getClass().getResourceAsStream("/icons/hide_show_equal_items.png")))); hideEqualItemsButton.selectedProperty().bindBidirectional(hideEqualItemsProperty); hideEqualItemsProperty.addListener((obs, o, n) -> updateTable()); - /* - hideEqualItemsProperty.bind(hideEqualItemsButton.selectedProperty()); - hideEqualItemsButton.selectedProperty() - .addListener((a, o, n) -> - - - hideEqualItems()); - - */ logAction.selectedProperty().bindBidirectional(logActionProperty); @@ -739,6 +713,7 @@ public void initializeViewForNewSnapshot(Node configurationNode) { List configPvs = configurationData.getPvList(); SnapshotData snapshotData = new SnapshotData(); snapshotData.setSnapshotItems(configurationToSnapshotItems(configPvs)); + this.snapshot = new Snapshot(); this.snapshot.setSnapshotData(snapshotData); updateUi(); Platform.runLater(() -> actionResultReadbackColumn.visibleProperty().setValue(false)); @@ -760,8 +735,7 @@ public void takeSnapshot() { actionResultColumn.visibleProperty().set(true); actionResultReadbackColumn.visibleProperty().set(true); }); - this.snapshot.setSnapshotNode(snapshot.get().getSnapshotNode()); - this.snapshot.setSnapshotData(snapshot.get().getSnapshotData()); + this.snapshot = snapshot.get(); updateUi(); } }); @@ -771,7 +745,7 @@ public void takeSnapshot() { * Restores snapshot meta-data properties to indicate that the UI * is not showing persisted {@link Snapshot} data. */ - private void resetMetaData(){ + private void resetMetaData() { tabTitleProperty.setValue(Messages.unnamedSnapshot); snapshotNameProperty.setValue(null); snapshotCommentProperty.setValue(null); @@ -884,8 +858,7 @@ public boolean handleTabClosed() { * Releases PV resources. */ private void dispose() { - //pvs.values().forEach(SaveAndRestorePV::dispose); - tableEntryItems.values().forEach(tableEntry -> tableEntry.getSaveAndRestorePV().dispose()); + tableEntryItems.forEach(tableEntry -> tableEntry.dispose()); } private void showLoggingError(String cause) { @@ -909,16 +882,13 @@ public void loadSnapshot(Node snapshotNode) { disabledUi.set(true); JobManager.schedule("Load snapshot items", monitor -> { try { - Snapshot snapshot = getSnapshotFromService(snapshotNode); - this.snapshot.setSnapshotNode(snapshot.getSnapshotNode()); - this.snapshot.setSnapshotData(snapshot.getSnapshotData()); + this.snapshot = getSnapshotFromService(snapshotNode); boolean configurationHasReadbacks = configurationHasReadbackPvs(snapshot.getSnapshotData()); Platform.runLater(() -> { nodeTypeProperty.set(snapshot.getSnapshotNode().getNodeType()); showLiveReadbackButton.setSelected(configurationHasReadbacks); actionResultColumn.visibleProperty().setValue(false); actionResultReadbackColumn.visibleProperty().setValue(false); - snapshotRestorableProperty.set(true); selectedColumn.visibleProperty().set(true); tabTitleProperty.setValue(snapshotNode.getName()); tabIdProperty.setValue(snapshotNode.getUniqueId()); @@ -935,6 +905,7 @@ public void loadSnapshot(Node snapshotNode) { @FXML public void restore() { disabledUi.setValue(true); + tableEntryItems.forEach(tableEntry -> tableEntry.setActionResult(ActionResult.PENDING)); restore(restoreModeProperty.get(), restoreResultList -> { disabledUi.setValue(false); if (logActionProperty.get()) { @@ -1089,7 +1060,7 @@ private void takeSnapshotFromArchiver(Consumer> consumer) { private void takeSnapshotReadPVs(Consumer> consumer) { JobManager.schedule("Take snapshot", monitor -> { // Clear snapshots array - snapshots.clear(); + additionalSnapshots.clear(); List snapshotItems; try { snapshotItems = SaveAndRestoreService.getInstance().takeSnapshot(configurationNode.getUniqueId()); @@ -1150,7 +1121,7 @@ private void showTakeSnapshotResult(List snapshotItems) { break; } } - for(SnapshotItem snapshotItem : snapshotItems){ + for (SnapshotItem snapshotItem : snapshotItems) { if (snapshotItem.getConfigPv().getReadbackPvName() != null && snapshotItem.getReadbackValue() != null && snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { disconnectedReadbackPvEncountered.set(true); @@ -1164,33 +1135,26 @@ private void showTakeSnapshotResult(List snapshotItems) { if (!disconnectedPvEncountered.get()) { actionResultColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/ok.png"))); } - if(!disconnectedReadbackPvEncountered.get()){ + if (!disconnectedReadbackPvEncountered.get()) { actionResultReadbackColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/ok.png"))); } }); } + /** + * Computes thresholds on scalar data types. The threshold is used to indicate that a delta value within threshold + * should not decorate the delta column, i.e. consider saved and live values equal. + * + * @param threshold Threshold in percent + */ private void updateThreshold(double threshold) { - snapshot.getSnapshotData().getSnapshotItems().forEach(item -> { - VType vtype = item.getValue(); - VNumber diffVType; - - double ratio = threshold / 100; - - TableEntry tableEntry = tableEntryItems.get(item.getConfigPv().getPvName()); - if (tableEntry == null) { - tableEntry = tableEntryItems.get(item.getConfigPv().getPvName()); - } - - if (!item.getConfigPv().equals(tableEntry.getConfigPv())) { - return; - } - + double ratio = threshold / 100; + tableEntryItems.forEach(tableEntry -> { + VType vtype = tableEntry.getSnapshotVal().get(); + // Only scalars considered if (vtype instanceof VNumber) { - diffVType = SafeMultiply.multiply((VNumber) vtype, ratio); - VNumber vNumber = diffVType; + VNumber vNumber = SafeMultiply.multiply((VNumber) vtype, ratio); boolean isNegative = vNumber.getValue().doubleValue() < 0; - tableEntry.setThreshold(Optional.of(new Threshold<>(isNegative ? SafeMultiply.multiply(vNumber.getValue(), -1.0) : vNumber.getValue()))); } }); @@ -1202,86 +1166,77 @@ private void updateThreshold(double threshold) { * @param multiplier The (double) factor used to change the snapshot set-points used in restore operation. */ private void updateSnapshotValues(double multiplier) { - snapshot.getSnapshotData().getSnapshotItems() - .forEach(item -> { - TableEntry tableEntry = tableEntryItems.get(item.getConfigPv().getPvName()); - VType vtype = tableEntry.storedSnapshotValue().get(); - VType newVType; - - if (vtype instanceof VNumber) { - newVType = SafeMultiply.multiply((VNumber) vtype, multiplier); - } else if (vtype instanceof VNumberArray) { - newVType = SafeMultiply.multiply((VNumberArray) vtype, multiplier); - } else { - return; - } + tableEntryItems.forEach(tableEntry -> { + VType vtype = tableEntry.storedSnapshotValue().get(); + VType newVType; - item.setValue(newVType); + if (vtype instanceof VNumber) { + newVType = SafeMultiply.multiply((VNumber) vtype, multiplier); + } else if (vtype instanceof VNumberArray) { + newVType = SafeMultiply.multiply((VNumberArray) vtype, multiplier); + } else { + return; + } - tableEntry.snapshotValProperty().set(newVType); + tableEntry.getSnapshotItem().setValue(newVType); + tableEntry.snapshotValProperty().set(newVType); - ObjectProperty value = tableEntry.valueProperty(); - value.setValue(new VTypePair(value.get().base, newVType, value.get().threshold)); - }); + ObjectProperty value = tableEntry.valueProperty(); + value.setValue(new VTypePair(value.get().base, newVType, value.get().threshold)); + }); } - private void applyFilter(String filterText, boolean preserveSelection, List> regexPatterns) { - if (filterText.isEmpty()) { - List arrayList = tableEntryItems.values().stream() + /** + * Applies the filter pattern, if any, to compute which entries to hide/show in the table. + * PV names matching user specified patterns (comma separated) will be maintained in the view. + * Only entries in the view will be subject to restore. + * If however user has ticked the Preserve selection... checkbox, non-matching entries will be hidden, + * but still considered as selected and hence subject to restore. + */ + private void applyFilter() { + if (filterTextProperty.isEmpty().get() && preserveSelectionProperty.not().get()) { + List arrayList = tableEntryItems.stream() .peek(item -> { - if (!preserveSelection) { - if (!item.readOnlyProperty().get()) { - item.selectedProperty().set(true); - } + if (!item.readOnlyProperty().get()) { + item.selectedProperty().set(true); } }).collect(Collectors.toList()); - Platform.runLater(() -> updateTable(arrayList)); - return; - } - - List filteredEntries = tableEntryItems.values().stream() - .filter(item -> { - boolean matchEither = false; - for (List andPatternList : regexPatterns) { - boolean matchAnd = true; - for (Pattern pattern : andPatternList) { - matchAnd &= pattern.matcher(item.pvNameProperty().get()).find(); + } else { + List filters = Arrays.asList(filterTextProperty.get().split(",")); + List> regexPatterns = filters.stream() + .map(item -> { + if (item.startsWith("/")) { + return List.of(Pattern.compile(item.substring(1, item.length() - 1).trim())); + } else { + return Arrays.stream(item.split("&")) + .map(andItem -> andItem.replaceAll("\\*", ".*")) + .map(andItem -> Pattern.compile(andItem.trim())) + .collect(Collectors.toList()); + } + }).collect(Collectors.toList()); + List filteredEntries = tableEntryItems.stream() + .filter(item -> { + boolean matchEither = false; + for (List andPatternList : regexPatterns) { + boolean matchAnd = true; + for (Pattern pattern : andPatternList) { + matchAnd &= pattern.matcher(item.pvNameProperty().get()).find(); + } + matchEither |= matchAnd; } - matchEither |= matchAnd; - } - - if (!preserveSelection) { - item.selectedProperty().setValue(matchEither); - } else { - matchEither |= item.selectedProperty().get(); - } - - return matchEither; - }).collect(Collectors.toList()); + if (preserveSelectionProperty.not().get()) { + item.selectedProperty().setValue(matchEither); + } - Platform.runLater(() -> updateTable(filteredEntries)); - } + return matchEither; + }).collect(Collectors.toList()); - private void applyPreserveSelection(boolean preserve) { - if (preserve) { - boolean allSelected = tableEntryItems.values().stream().allMatch(item -> item.selectedProperty().get()); - if (allSelected) { - tableEntryItems.values() - .forEach(item -> item.selectedProperty().set(false)); - } + Platform.runLater(() -> updateTable(filteredEntries)); } } - /* - private void hideEqualItems() { - ArrayList arrayList = new ArrayList<>(tableEntryItems.values()); - Platform.runLater(() -> updateTable(arrayList)); - } - - */ - /** * Restores a snapshot from client or service. * @@ -1295,10 +1250,9 @@ private void restore(RestoreMode restoreMode, Consumer> comp List restoreResultList = null; try { switch (restoreMode) { - case CLIENT_RESTORE -> - restoreResultList = snapshotUtil.restore(getSnapshotItemsToRestore(snapshot)); + case CLIENT_RESTORE -> restoreResultList = snapshotUtil.restore(getSnapshotItemsToRestore()); case SERVICE_RESTORE -> - restoreResultList = SaveAndRestoreService.getInstance().restore(getSnapshotItemsToRestore(snapshot)); + restoreResultList = SaveAndRestoreService.getInstance().restore(getSnapshotItemsToRestore()); } } catch (Exception e) { Platform.runLater(() -> { @@ -1325,9 +1279,8 @@ private void restore(RestoreMode restoreMode, Consumer> comp * @param restoreResultList Data created through a restore operation. */ private void showRestoreResult(List restoreResultList) { - List tableEntries = snapshotTableView.getItems(); AtomicBoolean disconnectedPvEncountered = new AtomicBoolean(false); - for (TableEntry tableEntry : tableEntries) { + for (TableEntry tableEntry : tableEntryItems) { Optional tableEntryOptional = restoreResultList.stream().filter(r -> r.getSnapshotItem().getConfigPv().getPvName().equals(tableEntry.getConfigPv().getPvName())).findFirst(); if (tableEntryOptional.isPresent()) { disconnectedPvEncountered.set(true); @@ -1341,8 +1294,7 @@ private void showRestoreResult(List restoreResultList) { Platform.runLater(() -> { if (!disconnectedPvEncountered.get()) { actionResultColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/ok.png"))); - } - else{ + } else { actionResultColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png"))); } }); @@ -1350,31 +1302,26 @@ private void showRestoreResult(List restoreResultList) { /** * Compiles a list of {@link SnapshotItem}s based on the snapshot's PVs (and potential read-only property setting) - * as well as user's choice to exclude items in the UI. + * as well as user's choice to exclude items in the UI using a filter * - * @param snapshot {@link Snapshot} contents. * @return A list of {@link SnapshotItem}s to be subject to a restore operation. */ - private List getSnapshotItemsToRestore(Snapshot snapshot) { + private List getSnapshotItemsToRestore() { List itemsToRestore = new ArrayList<>(); - - for (SnapshotItem entry : snapshot.getSnapshotData().getSnapshotItems()) { - TableEntry e = tableEntryItems.get(entry.getConfigPv().getPvName()); - - boolean restorable = e.selectedProperty().get() && - !e.readOnlyProperty().get() && - entry.getValue() != null && - !entry.getValue().equals(VNoData.INSTANCE); - + tableEntryItems.forEach(tableEntry -> { + boolean restorable = tableEntry.selectedProperty().get() && + tableEntry.readOnlyProperty().not().get() && + tableEntry.getSnapshotVal().get() != null && + !tableEntry.getSnapshotVal().get().equals(VNoData.INSTANCE); if (restorable) { - itemsToRestore.add(entry); + itemsToRestore.add(tableEntry.getSnapshotItem()); } - } + }); return itemsToRestore; } private void addSnapshot(Snapshot snapshot) { - snapshots.add(snapshot); + additionalSnapshots.add(snapshot); snapshotTableView.getColumns().clear(); @@ -1391,10 +1338,10 @@ private void addSnapshot(Snapshot snapshot) { compareColumn = new TableColumn<>(Messages.storedValues); compareColumn.getStyleClass().add("snapshot-table-centered"); - String baseSnapshotTimeStamp = snapshots.get(0).getSnapshotNode().getCreated() == null ? + String baseSnapshotTimeStamp = this.snapshot.getSnapshotNode().getCreated() == null ? "" : - " (" + TimestampFormats.SECONDS_FORMAT.format(snapshots.get(0).getSnapshotNode().getCreated().toInstant()) + ")"; - String snapshotName = snapshots.get(0).getSnapshotNode().getName() + baseSnapshotTimeStamp; + " (" + TimestampFormats.SECONDS_FORMAT.format(this.snapshot.getSnapshotNode().getCreated().toInstant()) + ")"; + String snapshotName = this.snapshot.getSnapshotNode().getName() + baseSnapshotTimeStamp; baseSnapshotColumn = new TableColumn<>(snapshotName); baseSnapshotColumn.getStyleClass().add("snapshot-table-centered"); @@ -1409,8 +1356,8 @@ private void addSnapshot(Snapshot snapshot) { ObjectProperty value = e.getRowValue().valueProperty(); value.setValue(new VTypePair(value.get().base, updatedValue, value.get().threshold)); updateLoadedSnapshot(e.getRowValue(), updatedValue); - for (int i = 1; i < snapshots.size(); i++) { - ObjectProperty compareValue = e.getRowValue().compareValueProperty(i); + for (int i = 0; i < additionalSnapshots.size(); i++) { + ObjectProperty compareValue = e.getRowValue().compareValueProperty(i + 1); compareValue.setValue(new VTypePair(updatedValue, compareValue.get().value, compareValue.get().threshold)); } }); @@ -1427,37 +1374,35 @@ private void addSnapshot(Snapshot snapshot) { compareColumn.getColumns().add(0, baseSnapshotColumn); - for (int s = 1; s < snapshots.size(); s++) { - Node snapshotNode = snapshots.get(s).getSnapshotNode(); + for (int s = 0; s < additionalSnapshots.size(); s++) { + Node snapshotNode = additionalSnapshots.get(s).getSnapshotNode(); String snapshotName = snapshotNode.getName(); List entries = snapshot.getSnapshotData().getSnapshotItems(); - String nodeName; - TableEntry tableEntry; - // Base snapshot data - List baseSnapshotTableEntries = new ArrayList<>(tableEntryItems.values()); + // Base snapshot data. Create a copy as tableEntryItems should always contain full list. + List baseSnapshotTableEntries = new ArrayList<>(tableEntryItems); SnapshotItem snpshotItem; for (int i = 0; i < entries.size(); i++) { snpshotItem = entries.get(i); - nodeName = snpshotItem.getConfigPv().getPvName(); - tableEntry = tableEntryItems.get(nodeName); + String pvName = snpshotItem.getConfigPv().getPvName(); + Optional tableEntryOptional = + tableEntryItems.stream().filter(t -> t.getConfigPv().getPvName().equals(pvName)).findFirst(); // tableEntry is null if the added snapshot has more items than the base snapshot. - if (tableEntry == null) { + TableEntry tableEntry; + if (tableEntryOptional.isEmpty()) { tableEntry = new TableEntry(snpshotItem); tableEntry.idProperty().setValue(tableEntryItems.size() + i + 1); - //tableEntry.pvNameProperty().setValue(nodeName); - //tableEntry.setConfigPv(snpshotItem.getConfigPv()); - tableEntryItems.put(nodeName, tableEntry); - //tableEntry.readbackNameProperty().set(snpshotItem.getConfigPv().getReadbackPvName()); + tableEntryItems.add(tableEntry); + } else { + tableEntry = tableEntryOptional.get(); } - tableEntry.setSnapshotValue(snpshotItem.getValue(), snapshots.size()); - tableEntry.setStoredReadbackValue(snpshotItem.getReadbackValue(), snapshots.size()); - //tableEntry.readOnlyProperty().set(snpshotItem.getConfigPv().isReadOnly()); + tableEntry.setSnapshotValue(snpshotItem.getValue(), additionalSnapshots.size()); + tableEntry.setStoredReadbackValue(snpshotItem.getReadbackValue(), additionalSnapshots.size()); baseSnapshotTableEntries.remove(tableEntry); } // If added snapshot has more items than base snapshot, the base snapshot's values for those // table rows need to be set to DISCONNECTED. for (TableEntry te : baseSnapshotTableEntries) { - te.setSnapshotValue(VDisconnectedData.INSTANCE, snapshots.size()); + te.setSnapshotValue(VDisconnectedData.INSTANCE, additionalSnapshots.size()); } TableColumn headerColumn = new TableColumn<>(snapshotName + " (" + @@ -1468,7 +1413,7 @@ private void addSnapshot(Snapshot snapshot) { Messages.setpoint, Messages.toolTipTableColumnSetpointPVValue, minWidth); - setpointValueCol.setCellValueFactory(e -> e.getValue().compareValueProperty(snapshots.size())); + setpointValueCol.setCellValueFactory(e -> e.getValue().compareValueProperty(additionalSnapshots.size())); setpointValueCol.setCellFactory(e -> new VTypeCellEditor<>()); setpointValueCol.setEditable(false); setpointValueCol.setSortable(false); @@ -1477,7 +1422,7 @@ private void addSnapshot(Snapshot snapshot) { TooltipTableColumn deltaCol = new TooltipTableColumn<>( Utilities.DELTA_CHAR + " " + Messages.baseSetpoint, "", minWidth); - deltaCol.setCellValueFactory(e -> e.getValue().compareValueProperty(snapshots.size())); + deltaCol.setCellValueFactory(e -> e.getValue().compareValueProperty(additionalSnapshots.size())); deltaCol.setCellFactory(e -> { VDeltaCellEditor vDeltaCellEditor = new VDeltaCellEditor<>(); vDeltaCellEditor.setShowDeltaPercentage(showDeltaPercentage.get()); @@ -1489,7 +1434,7 @@ private void addSnapshot(Snapshot snapshot) { headerColumn.getColumns().addAll(setpointValueCol, deltaCol, new DividerTableColumn()); - compareColumn.getColumns().add(s, headerColumn); + compareColumn.getColumns().add(s + 1, headerColumn); } columns.add(compareColumn); @@ -1498,60 +1443,42 @@ private void addSnapshot(Snapshot snapshot) { snapshotTableView.getColumns().addAll(columns); - //connectPVs(); updateTable(); } + /** + * This clears the list of {@link TableEntry}s in the view and creates new objects based + * on the contents of the current {@link Snapshot}. + */ private void showSnapshotInTable() { - if (snapshots.isEmpty()) { - snapshots.add(snapshot); - } else { - snapshots.set(0, snapshot); - } AtomicInteger counter = new AtomicInteger(0); + tableEntryItems.forEach(tableEntry -> tableEntry.dispose()); + tableEntryItems.clear(); snapshot.getSnapshotData().getSnapshotItems().forEach(snapshotItem -> { TableEntry tableEntry = new TableEntry(snapshotItem); - String name = snapshotItem.getConfigPv().getPvName(); tableEntry.idProperty().setValue(counter.incrementAndGet()); - //tableEntry.pvNameProperty().setValue(name); - //tableEntry.setConfigPv(snapshotItem.getConfigPv()); tableEntry.setSnapshotValue(snapshotItem.getValue(), 0); tableEntry.setStoredReadbackValue(snapshotItem.getReadbackValue(), 0); - //tableEntry.setReadbackValue(snapshotItem.getReadbackValue()); - /* - if (snapshotItem.getValue() == null || snapshotItem.getValue().equals(VDisconnectedData.INSTANCE)) { - tableEntry.setActionResult(ActionResult.FAILED); - } - else { - tableEntry.setActionResult(ActionResult.OK); - } - if (snapshotItem.getConfigPv().getReadbackPvName() != null){ - if(snapshotItem.getReadbackValue() == null || snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { - tableEntry.setActionResultReadback(ActionResult.FAILED); - } - else{ - tableEntry.setActionResultReadback(ActionResult.OK); - } - } - - */ - //tableEntry.readbackNameProperty().set(snapshotItem.getConfigPv().getReadbackPvName()); - //tableEntry.readOnlyProperty().set(snapshotItem.getConfigPv().isReadOnly()); - tableEntryItems.put(name, tableEntry); + tableEntryItems.add(tableEntry); }); updateTable(); - //connectPVs(); } - private void updateTable(){ - updateTable(tableEntryItems.values().stream().toList()); + /** + * Updates the {@link TableView} with the full list of {@link TableEntry} objects as created from + * the {@link Snapshot} data. + */ + private void updateTable() { + updateTable(tableEntryItems); } /** - * Sets new table entries for this table, but do not change the structure of the table. + * Updates the table showing the {@link TableEntry}s. Note though that while the full list of {@link TableEntry}s + * associated with a snapshot is maintained in this class, the supplied {@link List} of {@link TableEntry}s + * may be a subset, e.g. if user selects to filter or hide items where store setpoint and live value are equal. * - * @param entries the entries to set + * @param entries The entries to show in the table. */ private void updateTable(List entries) { final ObservableList items = snapshotTableView.getItems(); @@ -1594,40 +1521,20 @@ private int measureStringWidth(String text, Font font) { } /** - * Attempts to connect to all the PVs of the configuration/snapshot and binds the created {@link SaveAndRestorePV} objects - * to the {@link TableEntry} objects matched on PV name. - */ - /* - private void connectPVs() { - JobManager.schedule("Connect PVs", monitor -> tableEntryItems.values().forEach(e -> { - SaveAndRestorePV pv = pvs.get(e.getConfigPv().getPvName()); - if (pv == null) { - pvs.put(e.getConfigPv().getPvName(), new SaveAndRestorePV(e)); - } else { - pv.setSnapshotTableEntry(e); - } - })); - } - - */ - - /** - * * @param configurationData {@link ConfigurationData} obejct of a {@link org.phoebus.applications.saveandrestore.model.Configuration} * @return true if any if the {@link ConfigPv} items in {@link ConfigurationData#getPvList()} defines a non-null read-back * PV name, otherwise false. */ - private boolean configurationHasReadbackPvs(ConfigurationData configurationData){ + private boolean configurationHasReadbackPvs(ConfigurationData configurationData) { return configurationData.getPvList().stream().anyMatch(cp -> cp.getReadbackPvName() != null); } /** - * * @param snapshotData {@link SnapshotData} obejct of a {@link org.phoebus.applications.saveandrestore.model.Snapshot} * @return true if any if the {@link ConfigPv} items in {@link SnapshotData#getSnapshotItems()} defines a non-null read-back * PV name, otherwise false. */ - private boolean configurationHasReadbackPvs(SnapshotData snapshotData){ + private boolean configurationHasReadbackPvs(SnapshotData snapshotData) { return snapshotData.getSnapshotItems().stream().anyMatch(si -> si.getConfigPv().getReadbackPvName() != null); } @@ -1666,7 +1573,7 @@ protected void updateItem(Instant item, boolean empty) { /** * {@link TableCell} implementation for the action result columns. */ - private class ActionResultTableCell extends TableCell { + private static class ActionResultTableCell extends TableCell { @Override public void updateItem(org.phoebus.applications.saveandrestore.ui.snapshot.ActionResult actionResult, boolean empty) { if (empty) { diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java index d81eb352c3..f72b8b4f22 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java @@ -30,6 +30,8 @@ import org.phoebus.applications.saveandrestore.model.ConfigPv; import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.applications.saveandrestore.ui.SingleListenerBooleanProperty; +import org.phoebus.pv.PV; +import org.phoebus.pv.PVPool; import org.phoebus.saveandrestore.util.Threshold; import org.phoebus.saveandrestore.util.Utilities; import org.phoebus.saveandrestore.util.VNoData; @@ -40,6 +42,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; /** * TableEntry represents a single line in the snapshot viewer table. It provides values for all columns in @@ -102,13 +107,21 @@ public class TableEntry { */ private final ObjectProperty actionResultReadback = new SimpleObjectProperty<>(this, "actionResultReadback", ActionResult.PENDING); - private SaveAndRestorePV saveAndRestorePV; - private ConfigPv configPv; + private final ConfigPv configPv; + private final SnapshotItem snapshotItem; + + private PV pv; + private PV readbackPv; + /** + * The time between updates of dynamic data in the table, in ms. + */ + private static final long TABLE_UPDATE_INTERVAL = 500; /** * Construct a new table entry. */ public TableEntry(SnapshotItem snapshotItem) { + this.snapshotItem = snapshotItem; //when read only is set to true, unselect this PV readOnly.addListener((a, o, n) -> { if (n) { @@ -140,22 +153,13 @@ public TableEntry(SnapshotItem snapshotItem) { } } this.configPv = snapshotItem.getConfigPv(); - this.saveAndRestorePV = new SaveAndRestorePV(this); - } - - public SaveAndRestorePV getSaveAndRestorePV(){ - return saveAndRestorePV; + connect(); } - /* - public void setConfigPv(ConfigPv configPv) { - this.configPv = configPv; - pvName.setValue(configPv.getPvName()); - readbackName.setValue(configPv.getReadbackPvName()); + public SnapshotItem getSnapshotItem(){ + return snapshotItem; } - */ - public ConfigPv getConfigPv() { return configPv; } @@ -481,5 +485,32 @@ public void setActionResultReadback(ActionResult actionResult){ this.actionResultReadback.set(actionResult); } + private void connect(){ + try { + pv = PVPool.getPV(pvNameProperty().get()); + pv.onValueEvent().throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) + .subscribe(value -> setLiveValue(PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value)); + if (readbackName.isNotNull().get() && !readbackName.get().isEmpty()) { + readbackPv = PVPool.getPV(readbackName.get()); + readbackPv.onValueEvent() + .throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) + .subscribe(value -> setReadbackValue(PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value)); + } else { + // If configuration does not define read-back PV, then UI should show "no data" rather than "disconnected" + setReadbackValue(VNoData.INSTANCE); + } + } catch (Exception e) { + Logger.getLogger(TableEntry.class.getName()).log(Level.INFO, "Error connecting to PV", e); + } + } + + void dispose() { + if (pv != null) { + PVPool.releasePV(pv); + } + if (readbackPv != null) { + PVPool.releasePV(readbackPv); + } + } } From be103c75a51a3548c3d07d2dfc16d9696a65a33b Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 11 Jul 2025 15:22:10 +0200 Subject: [PATCH 3/6] Documentation if snapshot view filtering --- .../app/doc/images/filter-pv-items.png | Bin 0 -> 24306 bytes app/save-and-restore/app/doc/index.rst | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 app/save-and-restore/app/doc/images/filter-pv-items.png diff --git a/app/save-and-restore/app/doc/images/filter-pv-items.png b/app/save-and-restore/app/doc/images/filter-pv-items.png new file mode 100644 index 0000000000000000000000000000000000000000..6adec2d567b095bf3c1b888925a9152366ef0d0a GIT binary patch literal 24306 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 z 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 XXww%=MbF6L_DwtMOIB4DUa|iNP%|ve literal 0 HcmV?d00001 diff --git a/app/save-and-restore/app/doc/index.rst b/app/save-and-restore/app/doc/index.rst index eab505e600..f9174acccc 100644 --- a/app/save-and-restore/app/doc/index.rst +++ b/app/save-and-restore/app/doc/index.rst @@ -324,12 +324,25 @@ Prior to restore user has the option to: Restoring from a composite snapshot works in the same manner as the restore operation from a single-snapshot. +Filter PV items in list +^^^^^^^^^^^^^^^^^^^^^^^ +The list of items in the snapshot view can be filtered based on the PV name. See screenshot for highlighted UI element +where user may specify a string pattern to match PV names. Non-matching items will be hidden from the list view and +**also excluded from a restore operation**. + +To filter the view without excluding PV items from a restore operation, user needs to tick the “Preserve selection…” checkbox. + +.. image:: images/filter-pv-items.png + :width: 80% + + Restore from context menu ^^^^^^^^^^^^^^^^^^^^^^^^^ User may invoke a restore operation (from client or from service) from context menu items in the tree -view or in the search-and-filer view. In this case user will not have the possibility to unselect specific PVs. -However, PV items configured as read-only will not be restored. +view or in the search-and-filer view. In this case user will not have the possibility to deselect specific PVs. +Filtering/exclusion based on PV name will also not be possible. +However, PV items configured as read-only will always be excluded from a restore operation. Restore result ^^^^^^^^^^^^^^ From c8201fcadbc9e88e1b35929e0939db87b6d31821 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 13 Aug 2025 11:00:39 +0200 Subject: [PATCH 4/6] Improve close checks in save&restore --- .../applications/saveandrestore/Messages.java | 6 ++- .../SaveAndRestoreInstance.java | 52 ++++++++++++++++--- .../ui/SaveAndRestoreBaseController.java | 12 +++-- .../ui/SaveAndRestoreController.java | 16 ++++-- .../saveandrestore/ui/SaveAndRestoreTab.java | 26 +++++++--- .../ConfigurationController.java | 21 +++++--- .../ui/search/SearchAndFilterTab.java | 8 --- .../search/SearchAndFilterViewController.java | 10 ++-- .../SearchResultTableViewController.java | 7 ++- .../snapshot/CompositeSnapshotController.java | 14 +++-- .../ui/snapshot/SnapshotController.java | 41 ++++++++------- .../ui/snapshot/TableEntry.java | 39 +++++++------- .../saveandrestore/messages.properties | 8 +-- .../ui/configuration/ConfigurationEditor.fxml | 2 +- 14 files changed, 174 insertions(+), 88 deletions(-) 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 e5ec5bf940..db0047186a 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 @@ -34,7 +34,10 @@ public class Messages { public static String cannotCompareTitle; public static String closeConfigurationWarning; public static String closeCompositeSnapshotWarning; - public static String closeTabPrompt; + public static String closeSnapshotWarning; + public static String closeCompositeSnapshotTabPrompt; + public static String closeConfigurationTabPrompt; + public static String closeSnapshotTabPrompt; public static String compositeSnapshotConsistencyCheckFailed; public static String contextMenuAddTag; @Deprecated @@ -120,7 +123,6 @@ public class Messages { public static String overwrite; public static String paste; - public static String promptCloseSnapshotTabContent; public static String promptDeleteSelectedTitle; public static String promptDeleteSelectedHeader; public static String promptDeleteSelectedContent; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java index e763e3e75b..7ebc22b015 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java @@ -18,6 +18,7 @@ package org.phoebus.applications.saveandrestore; +import javafx.application.Platform; import javafx.fxml.FXMLLoader; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.framework.nls.NLS; @@ -25,7 +26,7 @@ import org.phoebus.framework.spi.AppDescriptor; import org.phoebus.framework.spi.AppInstance; import org.phoebus.security.tokens.ScopedAuthenticationToken; -import org.phoebus.ui.docking.DockItem; +import org.phoebus.ui.docking.DockItemWithInput; import org.phoebus.ui.docking.DockPane; import java.net.URI; @@ -37,9 +38,10 @@ public class SaveAndRestoreInstance implements AppInstance { + private final AppDescriptor appDescriptor; private final SaveAndRestoreController saveAndRestoreController; - private DockItem dockItem; + private DockItemWithInput dockItem; public static SaveAndRestoreInstance INSTANCE; @@ -53,21 +55,59 @@ public SaveAndRestoreInstance(AppDescriptor appDescriptor) { ResourceBundle resourceBundle = NLS.getMessages(Messages.class); loader.setResources(resourceBundle); loader.setLocation(SaveAndRestoreApplication.class.getResource("ui/SaveAndRestoreUI.fxml")); - dockItem = new DockItem(this, loader.load()); + dockItem = new DockItemWithInput(this, loader.load(), null, null, null); } catch (Exception e) { Logger.getLogger(SaveAndRestoreInstance.class.getName()).log(Level.SEVERE, "Failed loading fxml", e); } saveAndRestoreController = loader.getController(); + + // When user closes Phoebus... dockItem.addCloseCheck(() -> { - saveAndRestoreController.handleTabClosed(); - INSTANCE = null; - return CompletableFuture.completedFuture(true); + boolean mayClose = mayClose(); + if (mayClose) { + saveAndRestoreController.handleTabClosed(); + INSTANCE = null; + } + return CompletableFuture.completedFuture(mayClose); + }); + + // When user closes save&restore app... + dockItem.setOnCloseRequest(event -> { + if (mayClose()) { + saveAndRestoreController.handleTabClosed(); + INSTANCE = null; + } else { + event.consume(); + } }); DockPane.getActiveDockPane().addTab(dockItem); } + /** + * Checks with each tab if it may be closed. + * + * @return false if any of the tabs contains unsaved data, otherwise true. + */ + private boolean mayClose() { + if (Platform.isFxApplicationThread()) { + return saveAndRestoreController.doCloseCheck(); + } else { + CompletableFuture completableFuture = new CompletableFuture<>(); + Platform.runLater(() -> { + boolean okToClose = saveAndRestoreController.doCloseCheck(); + completableFuture.complete(okToClose); + }); + try { + return completableFuture.get(); + } catch (Exception e) { + Logger.getLogger(SaveAndRestoreInstance.class.getName()).log(Level.WARNING, "Got exception when waiting for close check evaluation", e); + return true; + } + } + } + @Override public AppDescriptor getAppDescriptor() { return appDescriptor; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java index fc30652bcf..9b992cf579 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreBaseController.java @@ -76,8 +76,14 @@ public SimpleStringProperty getUserIdentity() { protected void handleWebSocketMessage(SaveAndRestoreWebSocketMessage webSocketMessage){ } + /** + * Performs suitable cleanup, e.g. close web socket and PVs (where applicable). + */ + public abstract void handleTabClosed(); - protected boolean handleTabClosed(){ - return true; - } + /** + * Checks if the tab may be closed, e.g. if data managed in the UI has been saved. + * @return false if tab contains unsaved data, otherwise true + */ + public abstract boolean doCloseCheck(); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java index ce510528f7..db4b76af71 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java @@ -937,11 +937,11 @@ public void saveLocalState() { } @Override - public boolean handleTabClosed() { + public void handleTabClosed() { + tabPane.getTabs().forEach(t -> ((SaveAndRestoreTab)t).handleTabClosed()); saveLocalState(); webSocketClientService.closeWebSocket(); filterActivators.forEach(FilterActivator::stop); - return true; } /** @@ -1436,7 +1436,7 @@ private void addOptionalLoggingMenuItem() { } @Override - public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { + public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { switch (saveAndRestoreWebSocketMessage.messageType()) { case NODE_ADDED -> nodeAdded((String) saveAndRestoreWebSocketMessage.payload()); case NODE_REMOVED -> nodeRemoved((String) saveAndRestoreWebSocketMessage.payload()); @@ -1496,4 +1496,14 @@ private void handleWebSocketDisconnected() { serviceConnected.setValue(false); saveLocalState(); } + + @Override + public boolean doCloseCheck(){ + for(Tab tab : tabPane.getTabs()){ + if(!((SaveAndRestoreTab)tab).doCloseCheck()){ + return false; + } + } + return true; + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java index 195532a589..7edab3004d 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreTab.java @@ -38,7 +38,6 @@ public abstract class SaveAndRestoreTab extends Tab implements WebSocketMessageHandler { protected SaveAndRestoreBaseController controller; - protected WebSocketClientService webSocketClientService; public SaveAndRestoreTab() { ContextMenu contextMenu = new ContextMenu(); @@ -63,17 +62,13 @@ public SaveAndRestoreTab() { contextMenu.getItems().addAll(closeAll, closeOthers); setContextMenu(contextMenu); - webSocketClientService = WebSocketClientService.getInstance(); - setOnCloseRequest(event -> { - if (!controller.handleTabClosed()) { - event.consume(); + if (doCloseCheck()) { + handleTabClosed(); } else { - webSocketClientService.removeWebSocketMessageHandler(this); + event.consume(); } }); - - webSocketClientService.addWebSocketMessageHandler(this); } /** @@ -89,4 +84,19 @@ public void secureStoreChanged(List validTokens) { public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestoreWebSocketMessage) { } + + /** + * Performs suitable cleanup, e.g. close web socket and PVs (where applicable). + */ + public void handleTabClosed() { + controller.handleTabClosed(); + } + + /** + * Checks if the tab may be closed, e.g. if data managed in the UI has been saved. + * @return false if tab contains unsaved data, otherwise true + */ + public boolean doCloseCheck() { + return controller.doCloseCheck(); + } } 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 6ba19f78d5..a8b48e1eb2 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 @@ -66,6 +66,7 @@ import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.SelectionService; import org.phoebus.ui.application.ContextMenuHelper; +import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.FocusUtil; import org.phoebus.ui.javafx.ImageCache; @@ -85,7 +86,7 @@ public class ConfigurationController extends SaveAndRestoreBaseController implem @FXML @SuppressWarnings("unused") - private BorderPane root; + private BorderPane borderPane; @FXML @SuppressWarnings("unused") @@ -484,7 +485,7 @@ public void loadConfiguration(final Node node) { try { configurationData = saveAndRestoreService.getConfiguration(node.getUniqueId()); } catch (Exception e) { - Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(root, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e)); + Platform.runLater(() -> ExceptionDetailsErrorDialog.openError(borderPane, Messages.errorGeneric, Messages.errorUnableToRetrieveData, e)); return; } @@ -512,24 +513,28 @@ public void loadConfiguration(final Node node) { } /** - * Handles clean-up when the associated {@link ConfigurationTab} is closed. * A check is made if content is dirty, in which case user is prompted to cancel or close anyway. * * @return true if content is not dirty or user chooses to close anyway, * otherwise false. */ @Override - public boolean handleTabClosed() { + public boolean doCloseCheck() { if (dirty.get()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle(Messages.closeTabPrompt); + alert.setTitle(Messages.closeConfigurationTabPrompt); alert.setContentText(Messages.closeConfigurationWarning); + DialogHelper.positionDialog(alert, borderPane, -200, -200); Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); - } else { - webSocketClientService.removeWebSocketMessageHandler(this); - return true; } + + return true; + } + + @Override + public void handleTabClosed(){ + webSocketClientService.removeWebSocketMessageHandler(this); } @Override diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java index 7001537dde..5d0005f063 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java @@ -75,12 +75,4 @@ public SearchAndFilterTab(SaveAndRestoreController saveAndRestoreController) { setText(Messages.search); setGraphic(new ImageView(ImageCache.getImage(ImageCache.class, "/icons/sar-search_18x18.png"))); } - - public void filterActivated(String filterName){ - ((SearchAndFilterViewController)controller).filterActivated(filterName); - } - - public void filterDeactivated(String filterName){ - ((SearchAndFilterViewController)controller).filterDeactivated(filterName); - } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java index 25f7d53bbe..ec45d6bf19 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java @@ -648,11 +648,13 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRest } } - public void filterActivated(String filterName){ - + @Override + public void handleTabClosed(){ + webSocketClientService.removeWebSocketMessageHandler(this); } - public void filterDeactivated(String filterName){ - + @Override + public boolean doCloseCheck(){ + return true; } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java index d593898a7e..661c564843 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java @@ -419,8 +419,13 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRest } } - public boolean handleTabClosed() { + @Override + public void handleTabClosed() { webSocketClientService.removeWebSocketMessageHandler(this); + } + + @Override + public boolean doCloseCheck(){ return true; } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java index 78c5be1a3e..e6a40200ca 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/CompositeSnapshotController.java @@ -423,17 +423,21 @@ public void loadCompositeSnapshot(final Node node) { } @Override - public boolean handleTabClosed() { + public boolean doCloseCheck() { if (dirty.get()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle(Messages.closeTabPrompt); + alert.setTitle(Messages.closeCompositeSnapshotTabPrompt); alert.setContentText(Messages.closeCompositeSnapshotWarning); + DialogHelper.positionDialog(alert, borderPane, -200, -200); Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); - } else { - webSocketClientService.removeWebSocketMessageHandler(this); - return true; } + return true; + } + + @Override + public void handleTabClosed(){ + webSocketClientService.removeWebSocketMessageHandler(this); } /** diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index 3998927938..58ab60a109 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -832,33 +832,35 @@ private void updateLoadedSnapshot(TableEntry rowValue, VType newValue) { } /** - * Handles clean-up when the associated {@link SnapshotTab} is closed. * A check is made if content is dirty, in which case user is prompted to cancel or close anyway. * * @return true if content is not dirty or user chooses to close anyway, * otherwise false. */ @Override - public boolean handleTabClosed() { + public boolean doCloseCheck() { if (snapshotDataDirty.get()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle(Messages.closeTabPrompt); - alert.setContentText(Messages.promptCloseSnapshotTabContent); - DialogHelper.positionDialog(alert, borderPane, -150, -150); + alert.setTitle(Messages.closeSnapshotWarning); + alert.setContentText(Messages.closeSnapshotWarning); + DialogHelper.positionDialog(alert, borderPane, -200, -200); Optional result = alert.showAndWait(); return result.isPresent() && result.get().equals(ButtonType.OK); - } else { - webSocketClientService.removeWebSocketMessageHandler(this); - dispose(); - return true; } + return true; + } + + @Override + public void handleTabClosed(){ + webSocketClientService.removeWebSocketMessageHandler(this); + dispose(); } /** * Releases PV resources. */ private void dispose() { - tableEntryItems.forEach(tableEntry -> tableEntry.dispose()); + tableEntryItems.forEach(TableEntry::dispose); } private void showLoggingError(String cause) { @@ -1115,9 +1117,8 @@ private void showTakeSnapshotResult(List snapshotItems) { for (SnapshotItem snapshotItem : snapshotItems) { if (snapshotItem.getValue().equals(VDisconnectedData.INSTANCE)) { disconnectedPvEncountered.set(true); - Platform.runLater(() -> { - actionResultColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png"))); - }); + Platform.runLater(() -> + actionResultColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png")))); break; } } @@ -1125,9 +1126,9 @@ private void showTakeSnapshotResult(List snapshotItems) { if (snapshotItem.getConfigPv().getReadbackPvName() != null && snapshotItem.getReadbackValue() != null && snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { disconnectedReadbackPvEncountered.set(true); - Platform.runLater(() -> { - actionResultReadbackColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png"))); - }); + Platform.runLater(() -> + actionResultReadbackColumn.setGraphic(new ImageView(ImageCache.getImage(SnapshotController.class, "/icons/error.png")))); + break; } } @@ -1214,7 +1215,7 @@ private void applyFilter() { .map(andItem -> Pattern.compile(andItem.trim())) .collect(Collectors.toList()); } - }).collect(Collectors.toList()); + }).toList(); List filteredEntries = tableEntryItems.stream() .filter(item -> { boolean matchEither = false; @@ -1392,6 +1393,7 @@ private void addSnapshot(Snapshot snapshot) { tableEntry = new TableEntry(snpshotItem); tableEntry.idProperty().setValue(tableEntryItems.size() + i + 1); tableEntryItems.add(tableEntry); + tableEntry.connect(); } else { tableEntry = tableEntryOptional.get(); } @@ -1452,7 +1454,7 @@ private void addSnapshot(Snapshot snapshot) { */ private void showSnapshotInTable() { AtomicInteger counter = new AtomicInteger(0); - tableEntryItems.forEach(tableEntry -> tableEntry.dispose()); + tableEntryItems.forEach(TableEntry::dispose); tableEntryItems.clear(); snapshot.getSnapshotData().getSnapshotItems().forEach(snapshotItem -> { TableEntry tableEntry = new TableEntry(snapshotItem); @@ -1462,6 +1464,9 @@ private void showSnapshotInTable() { tableEntryItems.add(tableEntry); }); + JobManager.schedule("Connect to PVs", monitor -> + tableEntryItems.forEach(TableEntry::connect)); + updateTable(); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java index f72b8b4f22..892fc091b2 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/TableEntry.java @@ -30,13 +30,13 @@ import org.phoebus.applications.saveandrestore.model.ConfigPv; import org.phoebus.applications.saveandrestore.model.SnapshotItem; import org.phoebus.applications.saveandrestore.ui.SingleListenerBooleanProperty; +import org.phoebus.applications.saveandrestore.ui.VTypePair; +import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.saveandrestore.util.Threshold; import org.phoebus.saveandrestore.util.Utilities; import org.phoebus.saveandrestore.util.VNoData; -import org.phoebus.applications.saveandrestore.ui.VTypePair; -import org.phoebus.core.vtypes.VDisconnectedData; import java.time.Instant; import java.util.ArrayList; @@ -140,23 +140,20 @@ public TableEntry(SnapshotItem snapshotItem) { setReadbackValue(snapshotItem.getReadbackValue()); if (snapshotItem.getValue() == null || snapshotItem.getValue().equals(VDisconnectedData.INSTANCE)) { setActionResult(ActionResult.FAILED); - } - else { + } else { setActionResult(ActionResult.OK); } - if (snapshotItem.getConfigPv().getReadbackPvName() != null){ - if(snapshotItem.getReadbackValue() == null || snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { + if (snapshotItem.getConfigPv().getReadbackPvName() != null) { + if (snapshotItem.getReadbackValue() == null || snapshotItem.getReadbackValue().equals(VDisconnectedData.INSTANCE)) { setActionResultReadback(ActionResult.FAILED); - } - else{ + } else { setActionResultReadback(ActionResult.OK); } } this.configPv = snapshotItem.getConfigPv(); - connect(); } - public SnapshotItem getSnapshotItem(){ + public SnapshotItem getSnapshotItem() { return snapshotItem; } @@ -469,34 +466,40 @@ public ObjectProperty getSnapshotVal() { return snapshotVal; } - public ObjectProperty actionResultProperty(){ + + + @SuppressWarnings("unused") + public ObjectProperty actionResultProperty() { return actionResult; } - public void setActionResult(ActionResult actionResult){ + public void setActionResult(ActionResult actionResult) { this.actionResult.set(actionResult); } - public ObjectProperty actionResultReadbackProperty(){ + public ObjectProperty actionResultReadbackProperty() { return actionResultReadback; } - public void setActionResultReadback(ActionResult actionResult){ + public void setActionResultReadback(ActionResult actionResult) { this.actionResultReadback.set(actionResult); } - private void connect(){ + /** + * Connects to PV and read-back PV (if defined). + */ + public void connect() { try { pv = PVPool.getPV(pvNameProperty().get()); pv.onValueEvent().throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) .subscribe(value -> setLiveValue(PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value)); - - if (readbackName.isNotNull().get() && !readbackName.get().isEmpty()) { + if(readbackName.isNotNull().get() && !readbackName.get().isEmpty()) { readbackPv = PVPool.getPV(readbackName.get()); readbackPv.onValueEvent() .throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) .subscribe(value -> setReadbackValue(PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value)); - } else { + } + else { // If configuration does not define read-back PV, then UI should show "no data" rather than "disconnected" setReadbackValue(VNoData.INSTANCE); } 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 d0ba22cd63..7371d54ca7 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 @@ -15,9 +15,12 @@ cancel=Cancel cannotCompareHeader=No snapshot data available for comparison. cannotCompareTitle=Cannot Compare choose=Choose -closeConfigurationWarning=Configuration modified, but not saved. Do you wish to continue? +closeConfigurationWarning=Save&restore configuration modified, but not saved. Do you wish to continue? closeCompositeSnapshotWarning=Composite snapshot modified, but not saved. Do you wish to continue? -closeTabPrompt=Close tab? +closeSnapshotWarning=Save&restore snapshot created or modified, but not saved. Do you wish to continue? +closeCompositeSnapshotTabPrompt=Close composite snapshot tab? +closeConfigurationTabPrompt=Close configuration tab? +closeSnapshotTabPrompt=Close snapshot tab? comment=Comment comparisonMode=Comparison Mode comparisonTolerance=Tolerance @@ -144,7 +147,6 @@ openSearchView=Open Search View overwrite=Overwrite paste=Paste preserveSelection=Preserve selection after -promptCloseSnapshotTabContent=Snapshot data is not saved. Do you wish to continue? promptDeletePVTitle=Delete PV? promptDeletePVFromSaveSet=NOTE: deleting PVs from the save set will also delete stored values in all associated snapshots. \n\nDo you wish to continue? promptDeleteSelectedTitle=Delete selected item(s)? 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 70ac3d2d65..60d2d47e01 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,7 @@ -
From 3b796cc99f9189b792156c68cd544c49aa2cb0a6 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 13 Aug 2025 12:32:45 +0200 Subject: [PATCH 5/6] Updates based on review feedback --- app/save-and-restore/app/doc/index.rst | 4 ++-- .../saveandrestore/SaveAndRestoreInstance.java | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/save-and-restore/app/doc/index.rst b/app/save-and-restore/app/doc/index.rst index f9174acccc..eebaa67192 100644 --- a/app/save-and-restore/app/doc/index.rst +++ b/app/save-and-restore/app/doc/index.rst @@ -340,8 +340,8 @@ Restore from context menu ^^^^^^^^^^^^^^^^^^^^^^^^^ User may invoke a restore operation (from client or from service) from context menu items in the tree -view or in the search-and-filer view. In this case user will not have the possibility to deselect specific PVs. -Filtering/exclusion based on PV name will also not be possible. +view or in the search-and-filter view. In this case user will not have the possibility to deselect specific PVs. +Filtering/exclusion based on PV name will not be possible either. However, PV items configured as read-only will always be excluded from a restore operation. Restore result diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java index 7ebc22b015..c566fdafe0 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/SaveAndRestoreInstance.java @@ -96,8 +96,14 @@ private boolean mayClose() { } else { CompletableFuture completableFuture = new CompletableFuture<>(); Platform.runLater(() -> { - boolean okToClose = saveAndRestoreController.doCloseCheck(); - completableFuture.complete(okToClose); + try { + boolean okToClose = saveAndRestoreController.doCloseCheck(); + completableFuture.complete(okToClose); + } catch (Exception e) { + Logger.getLogger(SaveAndRestoreInstance.class.getName()).log(Level.SEVERE, + "Got exception when checking if OK to close", e); + completableFuture.complete(true); + } }); try { return completableFuture.get(); From 022953059e9974e297d4ac0977263c2b32599448 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 27 Aug 2025 14:01:07 +0200 Subject: [PATCH 6/6] Fix merge mishap --- .../saveandrestore/ui/snapshot/SnapshotController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index 32aa1fcd33..fc79edaba4 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -1535,6 +1535,7 @@ private int measureStringWidth(String text, Font font) { return (int) mText.getLayoutBounds().getWidth(); } + /** * @param configurationData {@link ConfigurationData} obejct of a {@link org.phoebus.applications.saveandrestore.model.Configuration} * @return true if any if the {@link ConfigPv} items in {@link ConfigurationData#getPvList()} defines a non-null read-back * PV name, otherwise false.