From 45d90e57b1f30f4615e7a473dc010c7d41da10dc Mon Sep 17 00:00:00 2001 From: Percy Baker Date: Mon, 26 Oct 2026 09:16:37 +0000 Subject: [PATCH 1/6] Add search bar --- .../alarm/ui/tree/AlarmTreeView.java | 243 +++++++++++++----- 1 file changed, 176 insertions(+), 67 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index f3d65c9801..04c0c1a44c 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -24,6 +25,7 @@ import java.util.stream.Collectors; import javafx.collections.FXCollections; +import javafx.scene.control.TextField; import org.phoebus.applications.alarm.AlarmSystem; import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.client.AlarmClientLeaf; @@ -140,6 +142,8 @@ public class AlarmTreeView extends BorderPane implements AlarmClientListener items.remove(changing); }); + private final TextField searchBox; + // Javadoc for TreeItem shows example for overriding isLeaf() and getChildren() // to dynamically create TreeItem as TreeView requests information. // @@ -176,7 +180,11 @@ public AlarmTreeView(final AlarmClient model, String itemName) tree_view.setCellFactory(view -> new AlarmTreeViewCell()); tree_view.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - setTop(createToolbar()); + searchBox = new TextField(); + searchBox.setPromptText("Search by PV"); + searchBox.setPrefWidth(300); + + setTop(createToolbar(searchBox)); setCenter(tree_view); createContextMenu(); @@ -234,7 +242,7 @@ private void startup() }); } - private ToolBar createToolbar() + private ToolBar createToolbar(TextField searchBox) { final Button collapse = new Button("", ImageCache.getImageView(AlarmUI.class, "/icons/collapse.png")); @@ -255,7 +263,12 @@ private ToolBar createToolbar() show_disabled.setTooltip(new Tooltip("Expand alarm tree to show disabled PVs")); show_disabled.setOnAction(event -> expandDisabledPVs(tree_view.getRoot())); - return new ToolBar(no_server, changing, ToolbarHelper.createSpring(), collapse, show_alarms, show_disabled); + final Button searchButton = new Button("Search"); + + searchButton.setOnAction(event -> propagateSearchDownTree(tree_view.getRoot())); + searchBox.setOnAction(event -> propagateSearchDownTree(tree_view.getRoot())); // act as if button pressed when enter pressed + + return new ToolBar(searchBox, searchButton, no_server, changing, ToolbarHelper.createSpring(), collapse, show_alarms, show_disabled); } ToolBar getToolbar() @@ -263,6 +276,96 @@ ToolBar getToolbar() return (ToolBar) getTop(); } + /** + * Recursively updates the tree so that it only shows the items matching the search. + * + * This is done bottom-up, by recursing down to the leaf nodes initially to determine + * whether they should be visible, and then returning up the tree to determine whether + * each component should be visible or not, and showing/hiding it accordingly. + */ + private boolean propagateSearchDownTree(final TreeItem> item) + { + for (final AlarmTreeItem child : item.getValue().getChildren()) { + final TreeItem> childItem = path2view.get(child.getPathName()); + + final boolean shouldDisplay = propagateSearchDownTree(childItem); + + if (shouldDisplay) { + final boolean doesNotHaveChildAlready = item.getChildren().stream() + .noneMatch(viewChild -> viewChild.getValue().getPathName().equals(child.getPathName())); + if (doesNotHaveChildAlready) { + // should display and does not have child, so add child + addChildToParent(item, childItem); + } + } else { + item.getChildren().removeIf( + alarmTreeItemTreeItem -> alarmTreeItemTreeItem.getValue() == child + ); + } + } + + return shouldDisplay(item); + } + + /** + * Ensures that an item is shown if it matches the search term, and not shown if it does not. + * + * This is done by determining whether the current node should be visible, and then propagating + * up the tree to ensure all the parent nodes are also only visible if they match the search term. + * This does not consider child nodes; it's intended to be called when a single node is added or + * updated to minimise state updates. + */ + private void propagateSearchUpTree(final TreeItem> item) + { + final AlarmTreeItem modelParent = item.getValue().getParent(); + if (modelParent == null) { + // root + return; + } + + final TreeItem> currentParent = item.getParent(); + final TreeItem> actualParent = path2view.get(modelParent.getPathName()); + + final boolean shouldDisplay = shouldDisplay(item); + if (shouldDisplay && currentParent == null) { + // should display but does not have a parent (so will not be displayed) + addChildToParent(actualParent, item); + logger.severe("showing"); + propagateSearchUpTree(actualParent); + } else if (!shouldDisplay && currentParent != null) { + // should not display but has a parent (so will be displayed) + actualParent.getChildren().remove(item); + logger.severe("hiding"); + propagateSearchUpTree(actualParent); + } + } + + private boolean shouldDisplay(final TreeItem> item) { + return (searchBox.getText().isEmpty() || !item.getChildren().isEmpty()) + || item.getValue() instanceof AlarmClientLeaf && item.getValue().getName().toLowerCase().contains(searchBox.getText().toLowerCase()); + } + + /** + * Adds a child TreeItem to a parent TreeItem, ensuring the order is preserved + * + * @param view_parent + * @param view_item + */ + private static void addChildToParent(TreeItem> view_parent, TreeItem> view_item) + { + // Keep sorted by inserting at appropriate index + final List>> items = view_parent.getChildren(); + final int index = Collections.binarySearch(items, view_item, + (a, b) -> CompareNatural.compareTo(a.getValue().getName(), + b.getValue().getName())); + if (items.isEmpty()) + items.add(view_item); // Adding an item to an index of an empty array throws an exception + else if (index < 0) + items.add(-index - 1, view_item); + else + items.add(index, view_item); + } + private void expandAlarms(final TreeItem> node) { if (node.isLeaf()) @@ -331,7 +434,7 @@ private void indicateChange() setCursor(Cursor.WAIT); final ObservableList items = getToolbar().getItems(); if (! items.contains(changing)) - items.add(1, changing); + items.add(3, changing); } else previous.cancel(false); @@ -398,24 +501,23 @@ public void itemAdded(final AlarmTreeItem item) dumpTree(tree_view.getRoot()); throw new IllegalStateException("Missing parent view item for " + item.getPathName()); } - // Create view item ASAP so that following updates will find it.. + // Create view item ASAP so that following model updates will find it.. final TreeItem> view_item = createViewItem(item); // .. but defer showing it on screen to UI thread final CountDownLatch done = new CountDownLatch(1); Platform.runLater(() -> { - indicateChange(); - // Keep sorted by inserting at appropriate index - final List>> items = view_parent.getChildren(); - final int index = Collections.binarySearch(items, view_item, - (a, b) -> CompareNatural.compareTo(a.getValue().getName(), - b.getValue().getName())); - if (index < 0) - items.add(-index-1, view_item); - else - items.add(index, view_item); - done.countDown(); + try { + // Refresh view parent in case it has changed since itemAdded was called + final TreeItem> current_view_parent = path2view.get(model_parent.getPathName()); + + indicateChange(); + addChildToParent(current_view_parent, view_item); + propagateSearchUpTree(view_item); + } finally { + done.countDown(); + } }); updateStats(); @@ -449,15 +551,20 @@ public void itemRemoved(final AlarmTreeItem item) final CountDownLatch done = new CountDownLatch(1); Platform.runLater(() -> { - indicateChange(); - // Can only locate the parent view item on UI thread, - // because item might just have been created by itemAdded() event - // and won't be on the screen until UI thread runs. - final TreeItem> view_parent = view_item.getParent(); - if (view_parent == null) - throw new IllegalStateException("No parent in view for " + item.getPathName()); - view_parent.getChildren().remove(view_item); - done.countDown(); + try { + indicateChange(); + // Can only locate the parent view item on UI thread, + // because item might just have been created by itemAdded() event + // and won't be on the screen until UI thread runs. + final TreeItem> view_parent = view_item.getParent(); + if (view_parent != null) { + // View parent can be null if the item is hidden; this is fine as long as we remove the item from path2view + view_parent.getChildren().remove(view_item); + propagateSearchUpTree(view_item); + } + } finally { + done.countDown(); + } }); updateStats(); @@ -531,49 +638,51 @@ private void performUpdates() items_to_update.clear(); } - // Remember selection - final ObservableList>> updatedSelectedItems = - FXCollections.observableArrayList(tree_view.getSelectionModel().getSelectedItems()); - - // How to update alarm tree cells when data changed? - // `setValue()` with a truly new value (not 'equal') should suffice, - // but there are two problems: - // Since we're currently using the alarm tree model item as a value, - // the value as seen by the TreeView remains the same. - // We could use a model item wrapper class as the cell value - // and replace it (while still holding the same model item!) - // for the TreeView to see a different wrapper value, but - // as shown in org.phoebus.applications.alarm.TreeItemUpdateDemo, - // replacing a tree cell value fails to trigger refreshes - // for certain hidden items. - // Only replacing the TreeItem gives reliable refreshes. - for (final TreeItem> view_item : view_items) - // Top-level item has no parent, and is not visible, so we keep it - if (view_item.getParent() != null) - { - // Locate item in tree parent - final TreeItem> parent = view_item.getParent(); - final int index = parent.getChildren().indexOf(view_item); - - // Create new TreeItem for that value - final AlarmTreeItem value = view_item.getValue(); - final TreeItem> update = new TreeItem<>(value); - if (updatedSelectedItems.contains(view_item)) { - updatedSelectedItems.remove(view_item); - updatedSelectedItems.add(update); + Platform.runLater(() -> { + // Remember selection + final ObservableList>> updatedSelectedItems = + FXCollections.observableArrayList(tree_view.getSelectionModel().getSelectedItems()); + + // How to update alarm tree cells when data changed? + // `setValue()` with a truly new value (not 'equal') should suffice, + // but there are two problems: + // Since we're currently using the alarm tree model item as a value, + // the value as seen by the TreeView remains the same. + // We could use a model item wrapper class as the cell value + // and replace it (while still holding the same model item!) + // for the TreeView to see a different wrapper value, but + // as shown in org.phoebus.applications.alarm.TreeItemUpdateDemo, + // replacing a tree cell value fails to trigger refreshes + // for certain hidden items. + // Only replacing the TreeItem gives reliable refreshes. + for (final TreeItem> view_item : view_items) + // Top-level item has no parent, and is not visible, so we keep it + if (view_item.getParent() != null) + { + // Locate item in tree parent + final TreeItem> parent = view_item.getParent(); + final int index = parent.getChildren().indexOf(view_item); + + // Create new TreeItem for that value + final AlarmTreeItem value = view_item.getValue(); + final TreeItem> update = new TreeItem<>(value); + if (updatedSelectedItems.contains(view_item)) { + updatedSelectedItems.remove(view_item); + updatedSelectedItems.add(update); + } + // Move child links to new item + final ArrayList>> children = new ArrayList<>(view_item.getChildren()); + view_item.getChildren().clear(); + update.getChildren().addAll(children); + update.setExpanded(view_item.isExpanded()); + + path2view.put(value.getPathName(), update); + parent.getChildren().set(index, update); } - // Move child links to new item - final ArrayList>> children = new ArrayList<>(view_item.getChildren()); - view_item.getChildren().clear(); - update.getChildren().addAll(children); - update.setExpanded(view_item.isExpanded()); - - path2view.put(value.getPathName(), update); - parent.getChildren().set(index, update); - } - // Restore selection - tree_view.getSelectionModel().clearSelection(); - updatedSelectedItems.forEach(item -> tree_view.getSelectionModel().select(item)); + // Restore selection + tree_view.getSelectionModel().clearSelection(); + updatedSelectedItems.forEach(item -> tree_view.getSelectionModel().select(item)); + }); } /** Context menu, details depend on selected items */ From 718f4b5950a4ca5c2e02faeb03a0bf4ebfaef90e Mon Sep 17 00:00:00 2001 From: Percy Baker Date: Tue, 27 Oct 2026 10:36:50 +0000 Subject: [PATCH 2/6] Improve robustness of alarm tree updates --- .../alarm/ui/area/AlarmAreaView.java | 17 +- .../alarm/ui/tree/AlarmTreeView.java | 210 ++++++++---------- 2 files changed, 108 insertions(+), 119 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java index 63f5762453..809ad3d2e1 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/area/AlarmAreaView.java @@ -75,8 +75,11 @@ public class AlarmAreaView extends StackPane implements AlarmClientListener */ private final AtomicReference> item_changes = new AtomicReference<>(); - /** Set of items that have updated, with iteration order based on when they updated */ - private final Set items_to_update = new LinkedHashSet<>(); + /** Set of items that have updated, with iteration order based on when they updated + * + * SYNC on access + */ + private final Set item_paths_to_update = new LinkedHashSet<>(); /** Throttle [5Hz] used for updates of existing items */ private final UpdateThrottle update_throttle = new UpdateThrottle(200, TimeUnit.MILLISECONDS, this::updateItems); @@ -165,9 +168,9 @@ public void itemUpdated(final AlarmTreeItem item) if (null == item_name) return; - synchronized (items_to_update) + synchronized (item_paths_to_update) { - items_to_update.add(item_name); + item_paths_to_update.add(item_name); } update_throttle.trigger(); } @@ -178,10 +181,10 @@ private void updateItems() final List changed_items = item_changes.getAndSet(null); final String[] update_array; - synchronized (items_to_update) + synchronized (item_paths_to_update) { - update_array = items_to_update.toArray(new String[items_to_update.size()]); - items_to_update.clear(); + update_array = item_paths_to_update.toArray(new String[item_paths_to_update.size()]); + item_paths_to_update.clear(); } Platform.runLater(() -> { diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index f3d65c9801..3671f35bed 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -12,10 +12,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -80,6 +81,14 @@ * as generic JavaFX Node, only calling public methods * defined on this class. * + *

The model (AlarmClient) and the AlarmTreeView are kept in + * sync by a lockstep between model and view updates. When the + * model is changed, it calls item[removed|added]. The AlarmClient + * then blocks until the method returns (i.e. until the UI has + * been updated) and then can move on to the next update. The + * exception to this is itemUpdated, which adds to a queue of + * items to be updated which is then regularly processed. + * * @author Kay Kasemir */ @SuppressWarnings("nls") @@ -112,13 +121,13 @@ public class AlarmTreeView extends BorderPane implements AlarmClientListener private final CountDownLatch block_item_changes = new CountDownLatch(1); /** Map from alarm tree path to view's TreeItem */ - private final ConcurrentHashMap>> path2view = new ConcurrentHashMap<>(); + private final Map>> path2view = new HashMap<>(); /** Items to update, ordered by time of original update request * - * SYNC on access + * SYNC on access */ - private final Set>> items_to_update = new LinkedHashSet<>(); + private final Set item_paths_to_update = new LinkedHashSet<>(); /** Throttle [5Hz] used for updates of existing items */ private final UpdateThrottle throttle = new UpdateThrottle(200, TimeUnit.MILLISECONDS, this::performUpdates); @@ -161,7 +170,7 @@ public AlarmTreeView(final AlarmClient model) { * @param model Model to represent. Must not be running, yet * @param itemName item name that may be expanded or given focus */ - public AlarmTreeView(final AlarmClient model, String itemName) + public AlarmTreeView(final AlarmClient model, final String itemName) { if (model.isRunning()) throw new IllegalStateException(); @@ -387,37 +396,34 @@ private void blockItemChanges() public void itemAdded(final AlarmTreeItem item) { blockItemChanges(); - // System.out.println(Thread.currentThread() + " Add " + item.getPathName()); - - // Parent must already exist - final AlarmTreeItem model_parent = item.getParent(); - final TreeItem> view_parent = path2view.get(model_parent.getPathName()); - if (view_parent == null) - { - dumpTree(tree_view.getRoot()); - throw new IllegalStateException("Missing parent view item for " + item.getPathName()); - } - // Create view item ASAP so that following updates will find it.. - final TreeItem> view_item = createViewItem(item); - - // .. but defer showing it on screen to UI thread final CountDownLatch done = new CountDownLatch(1); + Platform.runLater(() -> { indicateChange(); + + // Parent must already exist + final AlarmTreeItem model_parent = item.getParent(); + final TreeItem> view_parent = path2view.get(model_parent.getPathName()); + + if (view_parent == null) { + dumpTree(tree_view.getRoot()); + throw new IllegalStateException("Missing parent view item for " + item.getPathName()); + } + final TreeItem> view_item = createViewItem(item); + // Keep sorted by inserting at appropriate index final List>> items = view_parent.getChildren(); final int index = Collections.binarySearch(items, view_item, - (a, b) -> CompareNatural.compareTo(a.getValue().getName(), - b.getValue().getName())); + (a, b) -> CompareNatural.compareTo(a.getValue().getName(), + b.getValue().getName())); if (index < 0) - items.add(-index-1, view_item); + items.add(-index - 1, view_item); else items.add(index, view_item); done.countDown(); }); - updateStats(); // Waiting on the UI thread throttles the model's updates // to a rate that the UI can handle. @@ -438,18 +444,19 @@ public void itemAdded(final AlarmTreeItem item) public void itemRemoved(final AlarmTreeItem item) { blockItemChanges(); - // System.out.println(Thread.currentThread() + " Removed " + item.getPathName()); - // Remove item and all sub-items from model2ui - final TreeItem> view_item = removeViewItems(item); - if (view_item == null) - throw new IllegalStateException("No view item for " + item.getPathName()); - - // Remove the corresponding view final CountDownLatch done = new CountDownLatch(1); Platform.runLater(() -> { indicateChange(); + + // Remove item and all sub-items from model2ui + final TreeItem> view_item = removeViewItems(item); + if (view_item == null) + throw new IllegalStateException("No view item for " + item.getPathName()); + + // Remove the corresponding view + // // Can only locate the parent view item on UI thread, // because item might just have been created by itemAdded() event // and won't be on the screen until UI thread runs. @@ -459,7 +466,6 @@ public void itemRemoved(final AlarmTreeItem item) view_parent.getChildren().remove(view_item); done.countDown(); }); - updateStats(); // Waiting on the UI thread throttles the model's updates // to a rate that the UI can handle. @@ -493,87 +499,85 @@ private TreeItem> removeViewItems(final AlarmTreeItem item) public void itemUpdated(final AlarmTreeItem item) { blockItemChanges(); - // System.out.println(Thread.currentThread() + " Updated " + item.getPathName()); - final TreeItem> view_item = path2view.get(item.getPathName()); - if (view_item == null) - { - System.out.println("Unknown view for " + item.getPathName()); - path2view.keySet().stream().forEach(System.out::println); - throw new IllegalStateException("No view item for " + item.getPathName()); - } // UI update of existing item, i.e. // Platform.runLater(() -> TreeHelper.triggerTreeItemRefresh(view_item)); // is throttled. // If several items update, they're all redrawn in one Platform call, // and rapid updates of the same item are merged into just one final update - synchronized (items_to_update) + // We push the path instead of the view item in case the view item is removed + // between now and the update being handled. + synchronized (item_paths_to_update) { - items_to_update.add(view_item); + item_paths_to_update.add(item.getPathName()); } throttle.trigger(); - updateStats(); } /** Called by throttle to perform accumulated updates */ - @SuppressWarnings("unchecked") private void performUpdates() { - final TreeItem>[] view_items; - synchronized (items_to_update) + + // Drain items to update in one go; we will process it later on the main thread + final String[] view_item_paths; + synchronized (item_paths_to_update) { - // Creating a direct copy, i.e. another new LinkedHashSet<>(items_to_update), - // would be expensive, since we only need a _list_ of what's to update. - // Could use type-safe - // new ArrayList>>(items_to_update) - // but that calls toArray() internally, so doing that directly - view_items = items_to_update.toArray(new TreeItem[items_to_update.size()]); - items_to_update.clear(); + view_item_paths = item_paths_to_update.toArray(new String[item_paths_to_update.size()]); + item_paths_to_update.clear(); } - // Remember selection - final ObservableList>> updatedSelectedItems = - FXCollections.observableArrayList(tree_view.getSelectionModel().getSelectedItems()); - - // How to update alarm tree cells when data changed? - // `setValue()` with a truly new value (not 'equal') should suffice, - // but there are two problems: - // Since we're currently using the alarm tree model item as a value, - // the value as seen by the TreeView remains the same. - // We could use a model item wrapper class as the cell value - // and replace it (while still holding the same model item!) - // for the TreeView to see a different wrapper value, but - // as shown in org.phoebus.applications.alarm.TreeItemUpdateDemo, - // replacing a tree cell value fails to trigger refreshes - // for certain hidden items. - // Only replacing the TreeItem gives reliable refreshes. - for (final TreeItem> view_item : view_items) - // Top-level item has no parent, and is not visible, so we keep it - if (view_item.getParent() != null) - { - // Locate item in tree parent - final TreeItem> parent = view_item.getParent(); - final int index = parent.getChildren().indexOf(view_item); - - // Create new TreeItem for that value - final AlarmTreeItem value = view_item.getValue(); - final TreeItem> update = new TreeItem<>(value); - if (updatedSelectedItems.contains(view_item)) { - updatedSelectedItems.remove(view_item); - updatedSelectedItems.add(update); + Platform.runLater(() -> { + // Remember selection + final ObservableList>> updatedSelectedItems = + FXCollections.observableArrayList(tree_view.getSelectionModel().getSelectedItems()); + + // How to update alarm tree cells when data changed? + // `setValue()` with a truly new value (not 'equal') should suffice, + // but there are two problems: + // Since we're currently using the alarm tree model item as a value, + // the value as seen by the TreeView remains the same. + // We could use a model item wrapper class as the cell value + // and replace it (while still holding the same model item!) + // for the TreeView to see a different wrapper value, but + // as shown in org.phoebus.applications.alarm.TreeItemUpdateDemo, + // replacing a tree cell value fails to trigger refreshes + // for certain hidden items. + // Only replacing the TreeItem gives reliable refreshes. + for (final String view_item_path : view_item_paths) { + final TreeItem> view_item = path2view.get(view_item_path); + if (view_item == null) { + // Possible if the item has been removed since it was added to the update queue + continue; + } + + // Top-level item has no parent, and is not visible, so we keep it + if (view_item.getParent() != null) { + // Locate item in tree parent + final TreeItem> parent = view_item.getParent(); + final int index = parent.getChildren().indexOf(view_item); + + // Create new TreeItem for that value + final AlarmTreeItem value = view_item.getValue(); + final TreeItem> update = new TreeItem<>(value); + if (updatedSelectedItems.contains(view_item)) { + updatedSelectedItems.remove(view_item); + updatedSelectedItems.add(update); + } + // Move child links to new item + final ArrayList>> children = new ArrayList<>(view_item.getChildren()); + view_item.getChildren().clear(); + update.getChildren().addAll(children); + update.setExpanded(view_item.isExpanded()); + + path2view.put(value.getPathName(), update); + parent.getChildren().set(index, update); } - // Move child links to new item - final ArrayList>> children = new ArrayList<>(view_item.getChildren()); - view_item.getChildren().clear(); - update.getChildren().addAll(children); - update.setExpanded(view_item.isExpanded()); - - path2view.put(value.getPathName(), update); - parent.getChildren().set(index, update); } - // Restore selection - tree_view.getSelectionModel().clearSelection(); - updatedSelectedItems.forEach(item -> tree_view.getSelectionModel().select(item)); + + // Restore selection + tree_view.getSelectionModel().clearSelection(); + updatedSelectedItems.forEach(item -> tree_view.getSelectionModel().select(item)); + }); } /** Context menu, details depend on selected items */ @@ -689,25 +693,7 @@ private void addDragSupport() }); } -// private long next_stats = 0; -// private final AtomicInteger update_count = new AtomicInteger(); -// private volatile double updates_per_sec = 0.0; - - private void updateStats() - { -// final long time = System.currentTimeMillis(); -// if (time > next_stats) -// { -// final int updates = update_count.getAndSet(0); -// updates_per_sec = updates_per_sec * 0.9 + updates * 0.1; -// next_stats = time + 1000; -// System.out.format("%.2f updates/sec\n", updates_per_sec); -// } -// else -// update_count.incrementAndGet(); - } - - private void dumpTree(TreeItem> item) + private static void dumpTree(final TreeItem> item) { final ObservableList>> children = item.getChildren(); System.out.printf("item: %s , has %d children.\n", item.getValue().getName(), children.size()); From bee80eb545eb58376da72cb3ff7f442e5a4b294c Mon Sep 17 00:00:00 2001 From: Percy Baker Date: Tue, 27 Oct 2026 11:41:25 +0000 Subject: [PATCH 3/6] Fix search bar issues --- .../alarm/ui/tree/AlarmTreeView.java | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index ef31ff9c56..15281de2d6 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -15,7 +15,6 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -274,8 +273,8 @@ private ToolBar createToolbar(TextField searchBox) final Button searchButton = new Button("Search"); - searchButton.setOnAction(event -> propagateSearchDownTree(tree_view.getRoot())); - searchBox.setOnAction(event -> propagateSearchDownTree(tree_view.getRoot())); // act as if button pressed when enter pressed + searchButton.setOnAction(event -> propagateVisibilityDownTree(tree_view.getRoot())); + searchBox.setOnAction(event -> propagateVisibilityDownTree(tree_view.getRoot())); // act as if button pressed when enter pressed return new ToolBar(searchBox, searchButton, no_server, changing, ToolbarHelper.createSpring(), collapse, show_alarms, show_disabled); } @@ -285,49 +284,49 @@ ToolBar getToolbar() return (ToolBar) getTop(); } - /** - * Recursively updates the tree so that it only shows the items matching the search. + /** Recursively updates the tree so that it only shows the items matching the search. * * This is done bottom-up, by recursing down to the leaf nodes initially to determine * whether they should be visible, and then returning up the tree to determine whether * each component should be visible or not, and showing/hiding it accordingly. */ - private boolean propagateSearchDownTree(final TreeItem> item) + private boolean propagateVisibilityDownTree(final TreeItem> item) { - for (final AlarmTreeItem child : item.getValue().getChildren()) { + for (final AlarmTreeItem child : item.getValue().getChildren()) + { final TreeItem> childItem = path2view.get(child.getPathName()); - final boolean shouldDisplay = propagateSearchDownTree(childItem); + final boolean shouldDisplay = propagateVisibilityDownTree(childItem); - if (shouldDisplay) { + if (shouldDisplay) + { final boolean doesNotHaveChildAlready = item.getChildren().stream() .noneMatch(viewChild -> viewChild.getValue().getPathName().equals(child.getPathName())); - if (doesNotHaveChildAlready) { + if (doesNotHaveChildAlready) // should display and does not have child, so add child addChildToParent(item, childItem); - } - } else { + } + else item.getChildren().removeIf( alarmTreeItemTreeItem -> alarmTreeItemTreeItem.getValue() == child ); - } } return shouldDisplay(item); } - /** - * Ensures that an item is shown if it matches the search term, and not shown if it does not. + /** Ensures that an item is shown if it matches the search term, and not shown if it does not. * * This is done by determining whether the current node should be visible, and then propagating * up the tree to ensure all the parent nodes are also only visible if they match the search term. * This does not consider child nodes; it's intended to be called when a single node is added or * updated to minimise state updates. */ - private void propagateSearchUpTree(final TreeItem> item) + private void propagateVisibilityUpTree(final TreeItem> item) { final AlarmTreeItem modelParent = item.getValue().getParent(); - if (modelParent == null) { + if (modelParent == null) + { // root return; } @@ -336,20 +335,22 @@ private void propagateSearchUpTree(final TreeItem> item) final TreeItem> actualParent = path2view.get(modelParent.getPathName()); final boolean shouldDisplay = shouldDisplay(item); - if (shouldDisplay && currentParent == null) { + if (shouldDisplay && currentParent == null) + { // should display but does not have a parent (so will not be displayed) addChildToParent(actualParent, item); - logger.severe("showing"); - propagateSearchUpTree(actualParent); - } else if (!shouldDisplay && currentParent != null) { + propagateVisibilityUpTree(actualParent); + } + else if (!shouldDisplay && currentParent != null) + { // should not display but has a parent (so will be displayed) actualParent.getChildren().remove(item); - logger.severe("hiding"); - propagateSearchUpTree(actualParent); + propagateVisibilityUpTree(actualParent); } } - private boolean shouldDisplay(final TreeItem> item) { + private boolean shouldDisplay(final TreeItem> item) + { return (searchBox.getText().isEmpty() || !item.getChildren().isEmpty()) || item.getValue() instanceof AlarmClientLeaf && item.getValue().getName().toLowerCase().contains(searchBox.getText().toLowerCase()); } @@ -367,9 +368,7 @@ private static void addChildToParent(TreeItem> view_parent, Tre final int index = Collections.binarySearch(items, view_item, (a, b) -> CompareNatural.compareTo(a.getValue().getName(), b.getValue().getName())); - if (items.isEmpty()) - items.add(view_item); // Adding an item to an index of an empty array throws an exception - else if (index < 0) + if (index < 0) items.add(-index - 1, view_item); else items.add(index, view_item); @@ -516,15 +515,7 @@ public void itemAdded(final AlarmTreeItem item) } final TreeItem> view_item = createViewItem(item); - // Keep sorted by inserting at appropriate index - final List>> items = view_parent.getChildren(); - final int index = Collections.binarySearch(items, view_item, - (a, b) -> CompareNatural.compareTo(a.getValue().getName(), - b.getValue().getName())); - if (index < 0) - items.add(-index - 1, view_item); - else - items.add(index, view_item); + propagateVisibilityUpTree(view_item); done.countDown(); }); @@ -564,11 +555,10 @@ public void itemRemoved(final AlarmTreeItem item) // because item might just have been created by itemAdded() event // and won't be on the screen until UI thread runs. final TreeItem> view_parent = view_item.getParent(); - if (view_parent == null) + if (view_parent != null) { - // View parent can be null if the item is hidden; this is fine as long as we remove the item from path2view view_parent.getChildren().remove(view_item); - propagateSearchUpTree(view_item); + propagateVisibilityUpTree(view_parent); } done.countDown(); }); From 327d80269ae1588061ce5c683ecb3cdb6c3b82c7 Mon Sep 17 00:00:00 2001 From: Percy Baker Date: Tue, 27 Oct 2026 13:49:24 +0000 Subject: [PATCH 4/6] Alarm tree search messages localisation --- .../java/org/phoebus/applications/alarm/ui/Messages.java | 2 ++ .../phoebus/applications/alarm/ui/tree/AlarmTreeView.java | 5 +++-- .../org/phoebus/applications/alarm/ui/messages.properties | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java index 13272352db..e8ebd79875 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/Messages.java @@ -43,6 +43,8 @@ public class Messages public static String promptTitle; public static String removeComponentFailed; public static String renameItemFailed; + public static String search; + public static String searchByPv; public static String totalPVs; public static String unacknowledgeFailed; public static String withEnableDate; diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index 15281de2d6..a8c6a51ab1 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -35,6 +35,7 @@ import org.phoebus.applications.alarm.model.BasicState; import org.phoebus.applications.alarm.ui.AlarmContextMenuHelper; import org.phoebus.applications.alarm.ui.AlarmUI; +import org.phoebus.applications.alarm.ui.Messages; import org.phoebus.applications.alarm.ui.config.ItemConfigDialog; import org.phoebus.framework.selection.Selection; import org.phoebus.framework.selection.SelectionService; @@ -189,7 +190,7 @@ public AlarmTreeView(final AlarmClient model, final String itemName) tree_view.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); searchBox = new TextField(); - searchBox.setPromptText("Search by PV"); + searchBox.setPromptText(Messages.searchByPv); searchBox.setPrefWidth(300); setTop(createToolbar(searchBox)); @@ -271,7 +272,7 @@ private ToolBar createToolbar(TextField searchBox) show_disabled.setTooltip(new Tooltip("Expand alarm tree to show disabled PVs")); show_disabled.setOnAction(event -> expandDisabledPVs(tree_view.getRoot())); - final Button searchButton = new Button("Search"); + final Button searchButton = new Button(Messages.search); searchButton.setOnAction(event -> propagateVisibilityDownTree(tree_view.getRoot())); searchBox.setOnAction(event -> propagateVisibilityDownTree(tree_view.getRoot())); // act as if button pressed when enter pressed diff --git a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties index 83b48427d4..e083056f7d 100644 --- a/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties +++ b/app/alarm/ui/src/main/resources/org/phoebus/applications/alarm/ui/messages.properties @@ -81,5 +81,7 @@ tooltipMoveEditDetail=Edit the detail field of table item tooltipMoveTableItemDown=Move table item down tooltipMoveTableItemUp=Move table item up totalPVs=PVs: {0} +search=Search +searchByPv=Search by PV unacknowledgeFailed=Failed to unacknowledge alarm(s) withEnableDate=With Enable Date From 777b1bba2cb3bfd9415ce477cefba07ba26a0ebf Mon Sep 17 00:00:00 2001 From: Percy Baker Date: Tue, 27 Oct 2026 14:44:17 +0000 Subject: [PATCH 5/6] Change to clearing text field --- .../org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index a8c6a51ab1..960b68ae30 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -43,6 +43,7 @@ import org.phoebus.ui.application.SaveSnapshotAction; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.javafx.ClearingTextField; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.javafx.PrintAction; import org.phoebus.ui.javafx.Screenshot; @@ -189,7 +190,7 @@ public AlarmTreeView(final AlarmClient model, final String itemName) tree_view.setCellFactory(view -> new AlarmTreeViewCell()); tree_view.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - searchBox = new TextField(); + searchBox = new ClearingTextField(); searchBox.setPromptText(Messages.searchByPv); searchBox.setPrefWidth(300); From 660198b385d15d4383f8d75e0c65cd23f0e3cd5f Mon Sep 17 00:00:00 2001 From: Percy Baker Date: Thu, 29 Oct 2026 13:44:32 +0000 Subject: [PATCH 6/6] Also automatically refresh when search box cleared --- .../phoebus/applications/alarm/ui/tree/AlarmTreeView.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java index 960b68ae30..0025cd5954 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java @@ -277,6 +277,14 @@ private ToolBar createToolbar(TextField searchBox) searchButton.setOnAction(event -> propagateVisibilityDownTree(tree_view.getRoot())); searchBox.setOnAction(event -> propagateVisibilityDownTree(tree_view.getRoot())); // act as if button pressed when enter pressed + searchBox.textProperty().addListener(_ -> + { + // act as if button pressed when text box cleared + if (searchBox.getText().isEmpty()) + { + propagateVisibilityDownTree(tree_view.getRoot()); + } + }); return new ToolBar(searchBox, searchButton, no_server, changing, ToolbarHelper.createSpring(), collapse, show_alarms, show_disabled); }