From f6e8ee5f9b2252d51fb39ae5df83e679def8ef28 Mon Sep 17 00:00:00 2001 From: shroffk Date: Wed, 10 Dec 2025 14:28:33 -0500 Subject: [PATCH 01/11] First example of alarm configuration dialogs with the alarm tree --- .../alarm/ui/tree/AlarmTreeConfigDialog.java | 119 +++ .../alarm/ui/tree/AlarmTreeConfigView.java | 710 ++++++++++++++++++ .../alarm/ui/tree/MoveTreeItemAction.java | 27 +- 3 files changed, 846 insertions(+), 10 deletions(-) create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java new file mode 100644 index 0000000000..50f5667a9d --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java @@ -0,0 +1,119 @@ +package org.phoebus.applications.alarm.ui.tree; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; + +import java.util.Optional; + +/** + * Dialog for configuring alarm tree items with path selection. + * Displays the alarm tree structure and allows user to select a path from the tree. + */ +public class AlarmTreeConfigDialog extends Dialog +{ + private final TextField pathInput; + + /** + * Constructor for AlarmTreeConfigDialog + * + * @param alarmClient The alarm client model to display the tree + * @param currentPath The current path (initial value for text input) + * @param title The title of the dialog + * @param headerText The header text of the dialog + */ + public AlarmTreeConfigDialog(AlarmClient alarmClient, String currentPath, String title, String headerText) + { + setTitle(title); + setHeaderText(headerText); + setResizable(true); + + // Create content + VBox content = new VBox(10); + content.setPadding(new Insets(15)); + + // Add AlarmTreeConfigView + AlarmTreeConfigView configView = new AlarmTreeConfigView(alarmClient); + configView.setPrefHeight(300); + configView.setPrefWidth(400); + + // Initialize path input first + pathInput = new TextField(); + pathInput.setText(currentPath != null ? currentPath : ""); + pathInput.setStyle("-fx-font-family: monospace;"); + pathInput.setPromptText("Select a path from the tree above or type manually"); + pathInput.setEditable(true); + + // Add listener to update path when tree selection changes + // Access the tree view through reflection or by wrapping it + if (configView.getCenter() instanceof TreeView) + { + @SuppressWarnings("unchecked") + TreeView> treeView = (TreeView>) configView.getCenter(); + treeView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> + { + if (newVal != null && newVal.getValue() != null) + { + String selectedPath = newVal.getValue().getPathName(); + if (selectedPath != null && !selectedPath.isEmpty()) + { + pathInput.setText(selectedPath); + } + } + }); + } + + // Add text input for path + Label pathLabel = new Label("Selected Path:"); + + content.getChildren().addAll( + configView, + pathLabel, + pathInput + ); + + // Make tree view grow to fill available space + VBox.setVgrow(configView, Priority.ALWAYS); + + getDialogPane().setContent(content); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + getDialogPane().setPrefSize(500, 600); + + // Set result converter + setResultConverter(this::handleResult); + } + + /** + * Handle the dialog result + */ + private String handleResult(ButtonType buttonType) + { + if (buttonType == ButtonType.OK) + { + String path = pathInput.getText().trim(); + if (path.isEmpty()) + { + ExceptionDetailsErrorDialog.openError("Invalid Path", + "Path cannot be empty.", + null); + return null; + } + return path; + } + return null; + } + + /** + * Show the dialog and get the result + * + * @return Optional containing the path if OK was clicked, empty otherwise + */ + public Optional getPath() + { + return showAndWait(); + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java new file mode 100644 index 0000000000..8ee945a9e5 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java @@ -0,0 +1,710 @@ +/******************************************************************************* + * Copyright (c) 2018-2023 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.phoebus.applications.alarm.ui.tree; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.ToolBar; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.image.ImageView; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.TransferMode; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.CornerRadii; +import javafx.scene.paint.Color; +import org.phoebus.applications.alarm.AlarmSystem; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.client.AlarmClientLeaf; +import org.phoebus.applications.alarm.client.AlarmClientListener; +import org.phoebus.applications.alarm.client.AlarmClientNode; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.model.BasicState; +import org.phoebus.applications.alarm.ui.AlarmContextMenuHelper; +import org.phoebus.applications.alarm.ui.AlarmUI; +import org.phoebus.framework.selection.Selection; +import org.phoebus.framework.selection.SelectionService; +import org.phoebus.ui.application.ContextMenuService; +import org.phoebus.ui.application.SaveSnapshotAction; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.javafx.PrintAction; +import org.phoebus.ui.javafx.Screenshot; +import org.phoebus.ui.javafx.ToolbarHelper; +import org.phoebus.ui.javafx.UpdateThrottle; +import org.phoebus.ui.selection.AppSelection; +import org.phoebus.ui.spi.ContextMenuEntry; +import org.phoebus.util.text.CompareNatural; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import static org.phoebus.applications.alarm.AlarmSystem.logger; + +/** Tree-based UI for alarm configuration + * + *

Implemented as {@link BorderPane}, but should be treated + * as generic JavaFX Node, only calling public methods + * defined on this class. + * + * @author Kay Kasemir + */ +@SuppressWarnings("nls") +public class AlarmTreeConfigView extends BorderPane implements AlarmClientListener +{ + private final Label no_server = AlarmUI.createNoServerLabel(); + private final TreeView> tree_config_view = new TreeView<>(); + + /** Model with current alarm tree, sends updates */ + private final AlarmClient model; + private final String itemName; + + /** Latch for initially pausing model listeners + * + * Imagine a large alarm tree that changes. + * The alarm table can periodically display the current + * alarms, it does not need to display every change right away. + * The tree on the other hand must reflect every added or removed item, + * because updates cannot be applied once the tree structure gets out of sync. + * When the model is first started, there is a flurry of additions and removals, + * which arrive in the order in which the tree was generated, not necessarily + * in the order they're laid out in the hierarchy. + * These can be slow to render, especially if displaying via a remote desktop (ssh-X). + * The alarm tree view thus starts in stages: + * 1) Wait for model to receive the bulk of initial additions and removals + * 2) Add listeners to model changes, but block them via this latch + * 3) Represent the initial model + * 4) Release this latch to handle changes (blocked and those that arrive from now on) + */ + private final CountDownLatch block_item_changes = new CountDownLatch(1); + + /** Map from alarm tree path to view's TreeItem */ + private final ConcurrentHashMap>> path2view = new ConcurrentHashMap<>(); + + /** Items to update, ordered by time of original update request + * + * SYNC on access + */ + private final Set>> items_to_update = new LinkedHashSet<>(); + + /** Throttle [5Hz] used for updates of existing items */ + private final UpdateThrottle throttle = new UpdateThrottle(200, TimeUnit.MILLISECONDS, this::performUpdates); + + /** Cursor change doesn't work on Mac, so add indicator to toolbar */ + private final Label changing = new Label("Loading..."); + + /** Is change indicator shown, and future been submitted to clear it? */ + private final AtomicReference> ongoing_change = new AtomicReference<>(); + + /** Clear the change indicator */ + private final Runnable clear_change_indicator = () -> + Platform.runLater(() -> + { + logger.log(Level.INFO, "Alarm tree changes end"); + ongoing_change.set(null); + setCursor(null); + final ObservableList items = getToolbar().getItems(); + items.remove(changing); + }); + + // Javadoc for TreeItem shows example for overriding isLeaf() and getChildren() + // to dynamically create TreeItem as TreeView requests information. + // + // The alarm tree, however, keeps changing, and needs to locate the TreeItem + // for the changed AlarmTreeItem. + // Added code for checking if a TreeItem has been created, yet, + // can only make things slower, + // and the overall performance should not degrade when user opens more and more + // sections of the overall tree. + // --> Create the complete TreeItems ASAP and then keep updating to get + // constant performance? + + /** @param model Model to represent. Must not be running, yet */ + public AlarmTreeConfigView(final AlarmClient model) { + this(model, null); + } + + /** + * @param model Model to represent. Must not be running, yet + * @param itemName item name that may be expanded or given focus + */ + public AlarmTreeConfigView(final AlarmClient model, String itemName) + { + changing.setTextFill(Color.WHITE); + changing.setBackground(new Background(new BackgroundFill(Color.BLUE, CornerRadii.EMPTY, Insets.EMPTY))); + + this.model = model; + this.itemName = itemName; + + tree_config_view.setShowRoot(false); + tree_config_view.setCellFactory(view -> new AlarmTreeViewCell()); + tree_config_view.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + setTop(createToolbar()); + setCenter(tree_config_view); + + if (AlarmSystem.alarm_tree_startup_ms <= 0) + { + // Original implementation: + // Create initial (empty) representation, + // register listener, then model gets started + block_item_changes.countDown(); + tree_config_view.setRoot(createViewItem(model.getRoot())); + model.addListener(AlarmTreeConfigView.this); + } + else + UpdateThrottle.TIMER.schedule(this::startup, AlarmSystem.alarm_tree_startup_ms, TimeUnit.MILLISECONDS); + + // Caller will start the model once we return from constructor + } + + private void startup() + { + // Waited for model to receive the bulk of initial additions and removals... + Platform.runLater(() -> + { + if (! model.isRunning()) + { + logger.log(Level.WARNING, model.getRoot().getName() + " was disposed while waiting for alarm tree startup"); + return; + } + // Listen to model changes, but they're blocked, + // so this blocks model changes from now on + model.addListener(AlarmTreeConfigView.this); + + // Represent model that should by now be fairly complete + tree_config_view.setRoot(createViewItem(model.getRoot())); + + // expand tree item if is matches item name + if (tree_config_view.getRoot() != null && itemName != null) { + for (TreeItem treeItem : tree_config_view.getRoot().getChildren()) { + if (((AlarmTreeItem) treeItem.getValue()).getName().equals(itemName)) { + expandAlarms(treeItem); + break; + } + } + } + + // Set change indicator so that it clears when there are no more changes + indicateChange(); + showServerState(model.isServerAlive()); + + // Un-block to handle changes from now on + block_item_changes.countDown(); + }); + } + + private ToolBar createToolbar() + { + final Button collapse = new Button("", + ImageCache.getImageView(AlarmUI.class, "/icons/collapse.png")); + collapse.setTooltip(new Tooltip("Collapse alarm tree")); + collapse.setOnAction(event -> + { + for (TreeItem> sub : tree_config_view.getRoot().getChildren()) + sub.setExpanded(false); + }); + + final Button show_alarms = new Button("", + ImageCache.getImageView(AlarmUI.class, "/icons/expand_alarms.png")); + show_alarms.setTooltip(new Tooltip("Expand alarm tree to show active alarms")); + show_alarms.setOnAction(event -> expandAlarms(tree_config_view.getRoot())); + + final Button show_disabled = new Button("", + ImageCache.getImageView(AlarmUI.class, "/icons/expand_disabled.png")); + show_disabled.setTooltip(new Tooltip("Expand alarm tree to show disabled PVs")); + show_disabled.setOnAction(event -> expandDisabledPVs(tree_config_view.getRoot())); + + return new ToolBar(no_server, changing, ToolbarHelper.createSpring(), collapse, show_alarms, show_disabled); + } + + ToolBar getToolbar() + { + return (ToolBar) getTop(); + } + + private void expandAlarms(final TreeItem> node) + { + if (node.isLeaf()) + return; + + // Always expand the root, which itself is not visible, + // but this will show all the top-level elements. + // In addition, expand those items which are in active alarm. + final boolean expand = node.getValue().getState().severity.isActive() || + node == tree_config_view.getRoot(); + node.setExpanded(expand); + for (TreeItem> sub : node.getChildren()) + expandAlarms(sub); + } + + /** @param node Subtree node where to expand disabled PVs + * @return Does subtree contain disabled PVs? + */ + private boolean expandDisabledPVs(final TreeItem> node) + { + if (node != null && (node.getValue() instanceof AlarmClientLeaf)) + { + AlarmClientLeaf pv = (AlarmClientLeaf) node.getValue(); + if (! pv.isEnabled()) + return true; + } + + // Always expand the root, which itself is not visible, + // but this will show all the top-level elements. + // In addition, expand those items which contain disabled PV. + boolean expand = node == tree_config_view.getRoot(); + for (TreeItem> sub : node.getChildren()) + if (expandDisabledPVs(sub)) + expand = true; + node.setExpanded(expand); + return expand; + } + + private TreeItem> createViewItem(final AlarmTreeItem model_item) + { + // Create view item for model item itself + final TreeItem> view_item = new TreeItem<>(model_item); + final TreeItem> previous = path2view.put(model_item.getPathName(), view_item); + if (previous != null) + throw new IllegalStateException("Found existing view item for " + model_item.getPathName()); + + // Create view items for model item's children + for (final AlarmTreeItem model_child : model_item.getChildren()) + view_item.getChildren().add(createViewItem(model_child)); + + return view_item; + } + + /** Called when an item is added/removed to tell user + * that there are changes to the tree structure, + * may not make sense to interact with the tree right now. + * + *

Resets on its own after 1 second without changes. + */ + private void indicateChange() + { + final ScheduledFuture previous = ongoing_change.getAndSet(UpdateThrottle.TIMER.schedule(clear_change_indicator, 1, TimeUnit.SECONDS)); + if (previous == null) + { + logger.log(Level.INFO, "Alarm tree changes start"); + setCursor(Cursor.WAIT); + final ObservableList items = getToolbar().getItems(); + if (! items.contains(changing)) + items.add(1, changing); + } + else + previous.cancel(false); + } + + /** @param alive Have we seen server messages? */ + private void showServerState(final boolean alive) + { + final ObservableList items = getToolbar().getItems(); + items.remove(no_server); + if (! alive) + // Place left of spring, collapse, expand_alarms, + // i.e. right of potential AlarmConfigSelector + items.add(items.size()-3, no_server); + } + + // AlarmClientModelListener + @Override + public void serverStateChanged(final boolean alive) + { + Platform.runLater(() -> showServerState(alive)); + } + + // AlarmClientModelListener + @Override + public void serverModeChanged(final boolean maintenance_mode) + { + // NOP + } + + // AlarmClientModelListener + @Override + public void serverDisableNotifyChanged(final boolean disable_notify) + { + // NOP + } + + /** Block until changes to items should be shown */ + private void blockItemChanges() + { + try + { + block_item_changes.await(); + } + catch (InterruptedException ex) + { + logger.log(Level.WARNING, "Blocker for item changes got interrupted", ex); + } + } + + // AlarmClientModelListener + @Override + 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_config_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(); + // 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(); + }); + updateStats(); + + // Waiting on the UI thread throttles the model's updates + // to a rate that the UI can handle. + // The result is a slower startup when loading the model, + // but keeping the UI responsive + try + { + done.await(); + } + catch (final InterruptedException ex) + { + logger.log(Level.WARNING, "Alarm tree update error for added item " + item.getPathName(), ex); + } + } + + // AlarmClientModelListener + @Override + 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(); + // 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(); + }); + updateStats(); + + // Waiting on the UI thread throttles the model's updates + // to a rate that the UI can handle. + // The result is a slower startup when loading the model, + // but keeping the UI responsive + try + { + done.await(); + } + catch (final InterruptedException ex) + { + logger.log(Level.WARNING, "Alarm tree update error for removed item " + item.getPathName(), ex); + } + } + + /** @param item Item for which the TreeItem should be removed from path2view. Recurses to all child entries. + * @return TreeItem for 'item' + */ + private TreeItem> removeViewItems(final AlarmTreeItem item) + { + final TreeItem> view_item = path2view.remove(item.getPathName()); + + for (final AlarmTreeItem child : item.getChildren()) + removeViewItems(child); + + return view_item; + } + + // AlarmClientModelListener + @Override + 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) + { + items_to_update.add(view_item); + } + throttle.trigger(); + updateStats(); + } + + /** Called by throttle to perform accumulated updates */ + @SuppressWarnings("unchecked") + private void performUpdates() + { + final TreeItem>[] view_items; + synchronized (items_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(); + } + + // Remember selection + final ObservableList>> updatedSelectedItems = + FXCollections.observableArrayList(tree_config_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); + } + // Restore selection + tree_config_view.getSelectionModel().clearSelection(); + updatedSelectedItems.forEach(item -> tree_config_view.getSelectionModel().select(item)); + } + + /** Context menu, details depend on selected items */ + private void createContextMenu() + { + final ContextMenu menu = new ContextMenu(); + + tree_config_view.setOnContextMenuRequested(event -> + { + final ObservableList menu_items = menu.getItems(); + menu_items.clear(); + + final List> selection = tree_config_view.getSelectionModel().getSelectedItems().stream().map(TreeItem::getValue).collect(Collectors.toList()); + + // Add guidance etc. + new AlarmContextMenuHelper().addSupportedEntries(tree_config_view, model, menu, selection); + if (menu_items.size() > 0) + menu_items.add(new SeparatorMenuItem()); + + if (AlarmUI.mayConfigure(model)) + { + if (selection.size() <= 0) + // Add first item to empty config + menu_items.add(new AddComponentAction(tree_config_view, model, model.getRoot())); + else if (selection.size() == 1) + { + final AlarmTreeItem item = selection.get(0); + menu_items.add(new ConfigureComponentAction(tree_config_view, model, item)); + menu_items.add(new SeparatorMenuItem()); + + if (item instanceof AlarmClientNode) + menu_items.add(new AddComponentAction(tree_config_view, model, item)); + + menu_items.add(new RenameTreeItemAction(tree_config_view, model, item)); + + if (item instanceof AlarmClientLeaf) + menu_items.add(new DuplicatePVAction(tree_config_view, model, (AlarmClientLeaf) item)); + + menu_items.add(new MoveTreeItemAction(tree_config_view, model, item)); + } + if (selection.size() >= 1) + { + menu_items.add(new EnableComponentAction(tree_config_view, model, selection)); + menu_items.add(new DisableComponentAction(tree_config_view, model, selection)); + menu_items.add(new RemoveComponentAction(tree_config_view, model, selection)); + } + } + + menu_items.add(new SeparatorMenuItem()); + menu_items.add(new PrintAction(tree_config_view)); + menu_items.add(new SaveSnapshotAction(DockPane.getActiveDockPane())); + + // Add context menu actions based on the selection (i.e. email, logbook, etc...) + final Selection originalSelection = SelectionService.getInstance().getSelection(); + final List newSelection = Arrays.asList(AppSelection.of(tree_config_view, "Alarm Screenshot", "See alarm tree screenshot", () -> Screenshot.imageFromNode(tree_config_view))); + SelectionService.getInstance().setSelection("AlarmTree", newSelection); + List supported = ContextMenuService.getInstance().listSupportedContextMenuEntries(); + supported.stream().forEach(action -> { + MenuItem menuItem = new MenuItem(action.getName(), new ImageView(action.getIcon())); + menuItem.setOnAction((e) -> { + try + { + SelectionService.getInstance().setSelection("AlarmTree", newSelection); + action.call(tree_config_view, SelectionService.getInstance().getSelection()); + } catch (Exception ex) + { + logger.log(Level.WARNING, "Failed to execute " + action.getName() + " from AlarmTree.", ex); + } + }); + menu_items.add(menuItem); + }); + SelectionService.getInstance().setSelection("AlarmTree", originalSelection); + + menu.show(tree_config_view.getScene().getWindow(), event.getScreenX(), event.getScreenY()); + }); + } + + /** Double-click on item opens configuration dialog */ + private void addClickSupport() + { + tree_config_view.setOnMouseClicked(event -> + { + if (!AlarmUI.mayConfigure(model) || + event.getClickCount() != 2 || + tree_config_view.getSelectionModel().getSelectedItems().size() != 1) + return; + + final AlarmTreeItem item = tree_config_view.getSelectionModel().getSelectedItems().get(0).getValue(); + final ItemConfigDialog dialog = new ItemConfigDialog(model, item); + DialogHelper.positionDialog(dialog, tree_config_view, -150, -300); + // Show dialog, not waiting for it to close with OK or Cancel + dialog.show(); + }); + } + + /** For leaf nodes, drag PV name */ + private void addDragSupport() + { + tree_config_view.setOnDragDetected(event -> + { + final ObservableList>> items = tree_config_view.getSelectionModel().getSelectedItems(); + if (items.size() != 1) + return; + final AlarmTreeItem item = items.get(0).getValue(); + if (! (item instanceof AlarmClientLeaf)) + return; + final Dragboard db = tree_config_view.startDragAndDrop(TransferMode.COPY); + final ClipboardContent content = new ClipboardContent(); + content.putString(item.getName()); + db.setContent(content); + event.consume(); + }); + } + +// 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) + { + final ObservableList>> children = item.getChildren(); + System.out.printf("item: %s , has %d children.\n", item.getValue().getName(), children.size()); + for (final TreeItem> child : children) + { + System.out.println(child.getValue().getName()); + dumpTree(child); + } + } +} diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/MoveTreeItemAction.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/MoveTreeItemAction.java index 69f99a68c8..8d0ee3caf4 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/MoveTreeItemAction.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/MoveTreeItemAction.java @@ -39,25 +39,32 @@ public MoveTreeItemAction(TreeView> node, setOnAction(event -> { - //Prompt for new name - - String prompt = "Enter new path for item"; - + // Show dialog with tree visualization for path selection String path = item.getPathName(); while (true) { - path = AlarmTreeHelper.prompt(getText(), prompt, path, node); - if (path == null) + AlarmTreeConfigDialog dialog = new AlarmTreeConfigDialog( + model, + path, + getText(), + "Select new path for item" + ); + var result = dialog.getPath(); + if (result.isEmpty()) return; - if (AlarmTreeHelper.validateNewPath(path, node.getRoot().getValue()) ) + path = result.get(); + if (AlarmTreeHelper.validateNewPath(path, node.getRoot().getValue())) break; - prompt = "Invalid path. Try again or cancel"; + + // Show error dialog and retry + ExceptionDetailsErrorDialog.openError("Invalid Path", + "Invalid path. Please try again.", + null); } // Tree view keeps the selection indices, which will point to wrong content // after those items have been removed. - if (node instanceof TreeView) - ((TreeView) node).getSelectionModel().clearSelection(); + node.getSelectionModel().clearSelection(); final String new_path = path; // On a background thread, send the item configuration updates for the item to be moved and all its children. From acb8aad48212c62a81a88e1acc2a8c86b308bc1b Mon Sep 17 00:00:00 2001 From: shroffk Date: Mon, 15 Dec 2025 14:38:56 -0500 Subject: [PATCH 02/11] Cache the move item name to prepare more intuitive destination paths --- .../alarm/ui/tree/AlarmTreeConfigDialog.java | 47 ++++-- .../alarm/ui/tree/AlarmTreeConfigView.java | 153 +++--------------- 2 files changed, 52 insertions(+), 148 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java index 50f5667a9d..0667f5133d 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigDialog.java @@ -1,5 +1,6 @@ package org.phoebus.applications.alarm.ui.tree; +import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; import javafx.scene.control.*; import javafx.scene.layout.Priority; @@ -48,24 +49,42 @@ public AlarmTreeConfigDialog(AlarmClient alarmClient, String currentPath, String pathInput.setPromptText("Select a path from the tree above or type manually"); pathInput.setEditable(true); - // Add listener to update path when tree selection changes - // Access the tree view through reflection or by wrapping it - if (configView.getCenter() instanceof TreeView) - { - @SuppressWarnings("unchecked") - TreeView> treeView = (TreeView>) configView.getCenter(); - treeView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> + // Extract the last segment from the initial currentPath to preserve across selections + final String selectedTreeItem; + if (currentPath != null && !currentPath.isEmpty()) { + int lastSlashIndex = currentPath.lastIndexOf('/'); + if (lastSlashIndex >= 0 && lastSlashIndex < currentPath.length() - 1) { + selectedTreeItem = currentPath.substring(lastSlashIndex + 1); + } else { + selectedTreeItem = ""; + } + } else { + selectedTreeItem = ""; + } + + // Store the listener in a variable + ChangeListener>> selectionListener = (obs, oldVal, newVal) -> { + if (newVal != null && newVal.getValue() != null) { - if (newVal != null && newVal.getValue() != null) + String selectedPath = newVal.getValue().getPathName(); + if (selectedPath != null && !selectedPath.isEmpty()) { - String selectedPath = newVal.getValue().getPathName(); - if (selectedPath != null && !selectedPath.isEmpty()) - { - pathInput.setText(selectedPath); + // Only update if not focused + if (!pathInput.isFocused()) { + // Append the preserved last segment to the selected path + String newPath = selectedPath; + if (!selectedTreeItem.isEmpty()) { + newPath = selectedPath + "/" + selectedTreeItem; + } + + pathInput.setText(newPath); } } - }); - } + } + }; + configView.addTreeSelectionListener(selectionListener); + // Remove the listener when the dialog is closed + this.setOnHidden(e -> configView.removeTreeSelectionListener(selectionListener)); // Add text input for path Label pathLabel = new Label("Selected Path:"); diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java index 8ee945a9e5..59f0a0008f 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java @@ -8,6 +8,7 @@ package org.phoebus.applications.alarm.ui.tree; import javafx.application.Platform; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Insets; @@ -408,7 +409,6 @@ public void itemAdded(final AlarmTreeItem item) 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. @@ -450,7 +450,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. @@ -503,7 +502,6 @@ public void itemUpdated(final AlarmTreeItem item) items_to_update.add(view_item); } throttle.trigger(); - updateStats(); } /** Called by throttle to perform accumulated updates */ @@ -523,7 +521,7 @@ private void performUpdates() } // Remember selection - final ObservableList>> updatedSelectedItems = + final ObservableList>> updatedSelectedItems = FXCollections.observableArrayList(tree_config_view.getSelectionModel().getSelectedItems()); // How to update alarm tree cells when data changed? @@ -562,139 +560,10 @@ private void performUpdates() path2view.put(value.getPathName(), update); parent.getChildren().set(index, update); } - // Restore selection + tree_config_view.getSelectionModel().clearSelection(); updatedSelectedItems.forEach(item -> tree_config_view.getSelectionModel().select(item)); - } - - /** Context menu, details depend on selected items */ - private void createContextMenu() - { - final ContextMenu menu = new ContextMenu(); - - tree_config_view.setOnContextMenuRequested(event -> - { - final ObservableList menu_items = menu.getItems(); - menu_items.clear(); - - final List> selection = tree_config_view.getSelectionModel().getSelectedItems().stream().map(TreeItem::getValue).collect(Collectors.toList()); - - // Add guidance etc. - new AlarmContextMenuHelper().addSupportedEntries(tree_config_view, model, menu, selection); - if (menu_items.size() > 0) - menu_items.add(new SeparatorMenuItem()); - - if (AlarmUI.mayConfigure(model)) - { - if (selection.size() <= 0) - // Add first item to empty config - menu_items.add(new AddComponentAction(tree_config_view, model, model.getRoot())); - else if (selection.size() == 1) - { - final AlarmTreeItem item = selection.get(0); - menu_items.add(new ConfigureComponentAction(tree_config_view, model, item)); - menu_items.add(new SeparatorMenuItem()); - - if (item instanceof AlarmClientNode) - menu_items.add(new AddComponentAction(tree_config_view, model, item)); - - menu_items.add(new RenameTreeItemAction(tree_config_view, model, item)); - - if (item instanceof AlarmClientLeaf) - menu_items.add(new DuplicatePVAction(tree_config_view, model, (AlarmClientLeaf) item)); - - menu_items.add(new MoveTreeItemAction(tree_config_view, model, item)); - } - if (selection.size() >= 1) - { - menu_items.add(new EnableComponentAction(tree_config_view, model, selection)); - menu_items.add(new DisableComponentAction(tree_config_view, model, selection)); - menu_items.add(new RemoveComponentAction(tree_config_view, model, selection)); - } - } - - menu_items.add(new SeparatorMenuItem()); - menu_items.add(new PrintAction(tree_config_view)); - menu_items.add(new SaveSnapshotAction(DockPane.getActiveDockPane())); - - // Add context menu actions based on the selection (i.e. email, logbook, etc...) - final Selection originalSelection = SelectionService.getInstance().getSelection(); - final List newSelection = Arrays.asList(AppSelection.of(tree_config_view, "Alarm Screenshot", "See alarm tree screenshot", () -> Screenshot.imageFromNode(tree_config_view))); - SelectionService.getInstance().setSelection("AlarmTree", newSelection); - List supported = ContextMenuService.getInstance().listSupportedContextMenuEntries(); - supported.stream().forEach(action -> { - MenuItem menuItem = new MenuItem(action.getName(), new ImageView(action.getIcon())); - menuItem.setOnAction((e) -> { - try - { - SelectionService.getInstance().setSelection("AlarmTree", newSelection); - action.call(tree_config_view, SelectionService.getInstance().getSelection()); - } catch (Exception ex) - { - logger.log(Level.WARNING, "Failed to execute " + action.getName() + " from AlarmTree.", ex); - } - }); - menu_items.add(menuItem); - }); - SelectionService.getInstance().setSelection("AlarmTree", originalSelection); - - menu.show(tree_config_view.getScene().getWindow(), event.getScreenX(), event.getScreenY()); - }); - } - - /** Double-click on item opens configuration dialog */ - private void addClickSupport() - { - tree_config_view.setOnMouseClicked(event -> - { - if (!AlarmUI.mayConfigure(model) || - event.getClickCount() != 2 || - tree_config_view.getSelectionModel().getSelectedItems().size() != 1) - return; - - final AlarmTreeItem item = tree_config_view.getSelectionModel().getSelectedItems().get(0).getValue(); - final ItemConfigDialog dialog = new ItemConfigDialog(model, item); - DialogHelper.positionDialog(dialog, tree_config_view, -150, -300); - // Show dialog, not waiting for it to close with OK or Cancel - dialog.show(); - }); - } - /** For leaf nodes, drag PV name */ - private void addDragSupport() - { - tree_config_view.setOnDragDetected(event -> - { - final ObservableList>> items = tree_config_view.getSelectionModel().getSelectedItems(); - if (items.size() != 1) - return; - final AlarmTreeItem item = items.get(0).getValue(); - if (! (item instanceof AlarmClientLeaf)) - return; - final Dragboard db = tree_config_view.startDragAndDrop(TransferMode.COPY); - final ClipboardContent content = new ClipboardContent(); - content.putString(item.getName()); - db.setContent(content); - event.consume(); - }); - } - -// 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) @@ -707,4 +576,20 @@ private void dumpTree(TreeItem> item) dumpTree(child); } } + + /** + * Allows external classes to attach a selection listener to the tree view. + * @param listener ChangeListener for selected TreeItem + */ + public void addTreeSelectionListener(ChangeListener>> listener) { + tree_config_view.getSelectionModel().selectedItemProperty().addListener(listener); + } + + /** + * Allows external classes to remove a selection listener from the tree view. + * @param listener ChangeListener for selected TreeItem + */ + public void removeTreeSelectionListener(ChangeListener>> listener) { + tree_config_view.getSelectionModel().selectedItemProperty().removeListener(listener); + } } From 32893872d1e902fe993031a8dad9fccda9f3ddda Mon Sep 17 00:00:00 2001 From: shroffk Date: Mon, 15 Dec 2025 15:26:15 -0500 Subject: [PATCH 03/11] Cleanup imports for the new alarm tree view --- .../alarm/ui/tree/AlarmTreeConfigView.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java index 59f0a0008f..45b99ff4e5 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeConfigView.java @@ -15,19 +15,12 @@ import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Button; -import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; -import javafx.scene.control.MenuItem; import javafx.scene.control.SelectionMode; -import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.ToolBar; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; -import javafx.scene.image.ImageView; -import javafx.scene.input.ClipboardContent; -import javafx.scene.input.Dragboard; -import javafx.scene.input.TransferMode; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BorderPane; @@ -37,28 +30,15 @@ import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.client.AlarmClientLeaf; import org.phoebus.applications.alarm.client.AlarmClientListener; -import org.phoebus.applications.alarm.client.AlarmClientNode; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.model.BasicState; -import org.phoebus.applications.alarm.ui.AlarmContextMenuHelper; import org.phoebus.applications.alarm.ui.AlarmUI; -import org.phoebus.framework.selection.Selection; -import org.phoebus.framework.selection.SelectionService; -import org.phoebus.ui.application.ContextMenuService; -import org.phoebus.ui.application.SaveSnapshotAction; -import org.phoebus.ui.dialog.DialogHelper; -import org.phoebus.ui.docking.DockPane; import org.phoebus.ui.javafx.ImageCache; -import org.phoebus.ui.javafx.PrintAction; -import org.phoebus.ui.javafx.Screenshot; import org.phoebus.ui.javafx.ToolbarHelper; import org.phoebus.ui.javafx.UpdateThrottle; -import org.phoebus.ui.selection.AppSelection; -import org.phoebus.ui.spi.ContextMenuEntry; import org.phoebus.util.text.CompareNatural; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; @@ -69,7 +49,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; -import java.util.stream.Collectors; import static org.phoebus.applications.alarm.AlarmSystem.logger; From d4a8dfb1f057713cf437418baad88158f185c6ba Mon Sep 17 00:00:00 2001 From: shroffk Date: Mon, 22 Dec 2025 15:04:34 -0500 Subject: [PATCH 04/11] a prototype context menu action for adding PVs to alarm configurations --- .../ui/tree/ContextMenuAddComponentPVs.java | 273 ++++++++++++++++++ .../org.phoebus.ui.spi.ContextMenuEntry | 1 + 2 files changed, 274 insertions(+) create mode 100644 app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java create mode 100644 app/alarm/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java new file mode 100644 index 0000000000..3692861083 --- /dev/null +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java @@ -0,0 +1,273 @@ +package org.phoebus.applications.alarm.ui.tree; + +import javafx.beans.value.ChangeListener; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TreeItem; +import javafx.scene.image.Image; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.phoebus.applications.alarm.AlarmSystem; +import org.phoebus.applications.alarm.client.AlarmClient; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.ui.AlarmConfigSelector; +import org.phoebus.applications.alarm.ui.AlarmURI; +import org.phoebus.core.types.ProcessVariable; +import org.phoebus.framework.selection.Selection; +import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.spi.ContextMenuEntry; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import static org.phoebus.applications.alarm.AlarmSystemConstants.logger; + +public class ContextMenuAddComponentPVs implements ContextMenuEntry { + + private static final Class supportedTypes = ProcessVariable.class; + + private String server = null; + private String config_name = null; + private AlarmClient client = null; + + @Override + public String getName() { + return "Add Component"; + } + + @Override + public Image getIcon() { + return ImageCache.getImageView(ImageCache.class, "/icons/add.png").getImage(); + } + + @Override + public Class getSupportedType() { + return supportedTypes; + } + + @Override + public void call(Selection selection) throws Exception { + List pvs = selection.getSelections(); + server = AlarmSystem.server; + config_name = AlarmSystem.config_name; + + client = new AlarmClient(server, config_name, AlarmSystem.kafka_properties); + + AddComponentPVsDialog addDialog = new AddComponentPVsDialog(client, + pvs.stream().map(ProcessVariable::getName).collect(Collectors.toList()), null); + DialogResult dialogResult = addDialog.showAndGetResult(); + if (dialogResult == null) { + // User cancelled + return; + } + String path = dialogResult.path; + List pvNames = dialogResult.pvNames; + + if (AlarmTreeHelper.validateNewPath(path, client.getRoot())) { + try { + pvNames.forEach(pvName -> client.addPV(path, pvName)); + } catch (Exception ex) { + logger.log(Level.WARNING, "Cannot add component PVs to " + path, ex); + ExceptionDetailsErrorDialog.openError("Add Component PVs Failed", + "Failed to add component PVs to " + path, + ex); + } + } else { + // Show error dialog and retry + ExceptionDetailsErrorDialog.openError("Invalid Path", + "Invalid path. Please try again.", + null); + } + + } + + private Node create(final URI input, String itemName) throws Exception { + final String[] parsed = AlarmURI.parseAlarmURI(input); + String server = parsed[0]; + String config_name = parsed[1]; + + try { + AlarmClient client = new AlarmClient(server, config_name, AlarmSystem.kafka_properties); + final AlarmTreeConfigView tree_view = new AlarmTreeConfigView(client, itemName); + client.start(); + + if (AlarmSystem.config_names.length > 0) { + final AlarmConfigSelector configs = new AlarmConfigSelector(config_name, this::changeConfig); + tree_view.getToolbar().getItems().add(0, configs); + } + + return tree_view; + } catch (final Exception ex) { + logger.log(Level.WARNING, "Cannot create alarm tree for " + input, ex); + return new Label("Cannot create alarm tree for " + input); + } + } + + private void changeConfig(final String new_config_name) { + // Dispose existing setup + dispose(); + + try { + // Use same server name, but new config_name + final URI new_input = AlarmURI.createURI(server, new_config_name); + // no need for initial item name +// tab.setContent(create(new_input, null)); +// tab.setInput(new_input); +// Platform.runLater(() -> tab.setLabel(config_name + " " + app.getDisplayName())); + } catch (Exception ex) { + logger.log(Level.WARNING, "Cannot switch alarm tree to " + config_name, ex); + } + } + + private void dispose() { + if (client != null) { + client.shutdown(); + client = null; + } + } + + private static class AddComponentPVsDialog extends Dialog { + private final TextArea pvNamesInput; + private final TextField pathInput; + + public AddComponentPVsDialog(AlarmClient alarmClient, List initialPVs, String currentPath) { + setTitle("Add PVs to Alarm Configuration"); + setHeaderText("Select PVs and destination path"); + setResizable(true); + + // Create content + VBox content = new VBox(10); + content.setPadding(new Insets(15)); + + // PV Names input section + Label pvLabel = new Label("PV Names (semicolon-separated):"); + pvNamesInput = new TextArea(); + pvNamesInput.setPromptText("Enter PV names separated by semicolons (;)"); + pvNamesInput.setPrefRowCount(3); + pvNamesInput.setWrapText(true); + + // Pre-populate with initial PVs if provided + if (initialPVs != null && !initialPVs.isEmpty()) { + pvNamesInput.setText(String.join("; ", initialPVs)); + } + + // Add AlarmTreeConfigView for path selection + Label treeLabel = new Label("Select destination in alarm tree:"); + AlarmTreeConfigView configView = new AlarmTreeConfigView(alarmClient); + configView.setPrefHeight(300); + configView.setPrefWidth(500); + alarmClient.start(); + + // Path input + Label pathLabel = new Label("Destination Path:"); + pathInput = new TextField(); + pathInput.setText(currentPath != null ? currentPath : ""); + pathInput.setStyle("-fx-font-family: monospace;"); + pathInput.setPromptText("Select a path from the tree above or type manually"); + pathInput.setEditable(true); + + // Store the listener in a variable + ChangeListener>> selectionListener = (obs, oldVal, newVal) -> { + if (newVal != null && newVal.getValue() != null) { + String selectedPath = newVal.getValue().getPathName(); + if (selectedPath != null && !selectedPath.isEmpty()) { + // Only update if path input is not focused + if (!pathInput.isFocused()) { + pathInput.setText(selectedPath); + } + } + } + }; + configView.addTreeSelectionListener(selectionListener); + + // Remove the listener when the dialog is closed + this.setOnHidden(e -> configView.removeTreeSelectionListener(selectionListener)); + + // Add all components to layout + content.getChildren().addAll( + pvLabel, + pvNamesInput, + treeLabel, + configView, + pathLabel, + pathInput + ); + + // Make tree view grow to fill available space + VBox.setVgrow(configView, Priority.ALWAYS); + + getDialogPane().setContent(content); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + getDialogPane().setPrefSize(600, 700); + + // Set result converter - returns path if OK, null if Cancel + setResultConverter(buttonType -> { + if (buttonType == ButtonType.OK) { + String path = pathInput.getText().trim(); + if (path.isEmpty()) { + ExceptionDetailsErrorDialog.openError("Invalid Path", + "Destination path cannot be empty.", + null); + return null; + } + return path; + } + return null; + }); + } + + /** + * Get the list of PV names entered by the user + * + * @return List of PV names (trimmed and non-empty) + */ + public List getPVNames() { + String text = pvNamesInput.getText(); + if (text == null || text.trim().isEmpty()) { + return List.of(); + } + + // Split by semicolon, trim each entry, and filter out empty strings + return List.of(text.split(";")) + .stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + + /** + * Show the dialog and get both the path and PV names + * + * @return DialogResult containing path and PV names, or null if cancelled + */ + public DialogResult showAndGetResult() { + Optional result = showAndWait(); + if (result.isPresent()) { + return new DialogResult(result.get(), getPVNames()); + } + return null; + } + } + + /** + * Result from AddComponentPVsDialog containing both path and PV names + */ + private static class DialogResult { + final String path; + final List pvNames; + + DialogResult(String path, List pvNames) { + this.path = path; + this.pvNames = pvNames; + } + } +} diff --git a/app/alarm/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry b/app/alarm/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry new file mode 100644 index 0000000000..f0f8dc2a79 --- /dev/null +++ b/app/alarm/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry @@ -0,0 +1 @@ +org.phoebus.applications.alarm.ui.tree.ContextMenuAddComponentPVs From c632ccba6a13ee897ed722ad63978ea5e95b050e Mon Sep 17 00:00:00 2001 From: shroffk Date: Wed, 31 Dec 2025 14:05:59 -0500 Subject: [PATCH 05/11] Add support for changing condifurations --- .../ui/tree/ContextMenuAddComponentPVs.java | 158 +++++++++--------- 1 file changed, 80 insertions(+), 78 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java index 3692861083..a5b93be6ac 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java @@ -1,5 +1,6 @@ package org.phoebus.applications.alarm.ui.tree; +import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; import javafx.scene.Node; @@ -35,10 +36,6 @@ public class ContextMenuAddComponentPVs implements ContextMenuEntry { private static final Class supportedTypes = ProcessVariable.class; - private String server = null; - private String config_name = null; - private AlarmClient client = null; - @Override public String getName() { return "Add Component"; @@ -57,89 +54,43 @@ public Class getSupportedType() { @Override public void call(Selection selection) throws Exception { List pvs = selection.getSelections(); - server = AlarmSystem.server; - config_name = AlarmSystem.config_name; - client = new AlarmClient(server, config_name, AlarmSystem.kafka_properties); + AddComponentPVsDialog addDialog = new AddComponentPVsDialog(AlarmSystem.server, + AlarmSystem.config_name, + AlarmSystem.kafka_properties, + pvs.stream().map(ProcessVariable::getName).collect(Collectors.toList()), + null); - AddComponentPVsDialog addDialog = new AddComponentPVsDialog(client, - pvs.stream().map(ProcessVariable::getName).collect(Collectors.toList()), null); DialogResult dialogResult = addDialog.showAndGetResult(); if (dialogResult == null) { // User cancelled return; } - String path = dialogResult.path; - List pvNames = dialogResult.pvNames; - - if (AlarmTreeHelper.validateNewPath(path, client.getRoot())) { - try { - pvNames.forEach(pvName -> client.addPV(path, pvName)); - } catch (Exception ex) { - logger.log(Level.WARNING, "Cannot add component PVs to " + path, ex); - ExceptionDetailsErrorDialog.openError("Add Component PVs Failed", - "Failed to add component PVs to " + path, - ex); - } - } else { - // Show error dialog and retry - ExceptionDetailsErrorDialog.openError("Invalid Path", - "Invalid path. Please try again.", - null); - } - - } - - private Node create(final URI input, String itemName) throws Exception { - final String[] parsed = AlarmURI.parseAlarmURI(input); - String server = parsed[0]; - String config_name = parsed[1]; - - try { - AlarmClient client = new AlarmClient(server, config_name, AlarmSystem.kafka_properties); - final AlarmTreeConfigView tree_view = new AlarmTreeConfigView(client, itemName); - client.start(); - - if (AlarmSystem.config_names.length > 0) { - final AlarmConfigSelector configs = new AlarmConfigSelector(config_name, this::changeConfig); - tree_view.getToolbar().getItems().add(0, configs); - } - - return tree_view; - } catch (final Exception ex) { - logger.log(Level.WARNING, "Cannot create alarm tree for " + input, ex); - return new Label("Cannot create alarm tree for " + input); - } - } - - private void changeConfig(final String new_config_name) { - // Dispose existing setup - dispose(); - - try { - // Use same server name, but new config_name - final URI new_input = AlarmURI.createURI(server, new_config_name); - // no need for initial item name -// tab.setContent(create(new_input, null)); -// tab.setInput(new_input); -// Platform.runLater(() -> tab.setLabel(config_name + " " + app.getDisplayName())); - } catch (Exception ex) { - logger.log(Level.WARNING, "Cannot switch alarm tree to " + config_name, ex); - } - } - - private void dispose() { - if (client != null) { - client.shutdown(); - client = null; - } } + /** + * Dialog for adding component PVs to an alarm configuration + */ private static class AddComponentPVsDialog extends Dialog { private final TextArea pvNamesInput; private final TextField pathInput; - public AddComponentPVsDialog(AlarmClient alarmClient, List initialPVs, String currentPath) { + private AlarmClient alarmClient; + /** + * Constructor for AddComponentPVsDialog + * + * @param server The alarm server + * @param config_name The alarm configuration name + * @param kafka_properties Kafka properties for the AlarmClient + * @param pvNames Initial list of PV names to pre-populate + * @param currentPath The current path (initial value for text input) + */ + + public AddComponentPVsDialog(String server, String config_name, String kafka_properties, List pvNames, String currentPath) { + // Model/Controller + + alarmClient = new AlarmClient(server, config_name, kafka_properties); + setTitle("Add PVs to Alarm Configuration"); setHeaderText("Select PVs and destination path"); setResizable(true); @@ -156,13 +107,19 @@ public AddComponentPVsDialog(AlarmClient alarmClient, List initialPVs, S pvNamesInput.setWrapText(true); // Pre-populate with initial PVs if provided - if (initialPVs != null && !initialPVs.isEmpty()) { - pvNamesInput.setText(String.join("; ", initialPVs)); + if (pvNames != null && !pvNames.isEmpty()) { + pvNamesInput.setText(String.join("; ", pvNames)); } // Add AlarmTreeConfigView for path selection Label treeLabel = new Label("Select destination in alarm tree:"); AlarmTreeConfigView configView = new AlarmTreeConfigView(alarmClient); + + if (AlarmSystem.config_names.length > 0) { + final AlarmConfigSelector configs = new AlarmConfigSelector(config_name, this::changeConfig); + configView.getToolbar().getItems().add(0, configs); + } + configView.setPrefHeight(300); configView.setPrefWidth(500); alarmClient.start(); @@ -189,8 +146,11 @@ public AddComponentPVsDialog(AlarmClient alarmClient, List initialPVs, S }; configView.addTreeSelectionListener(selectionListener); - // Remove the listener when the dialog is closed - this.setOnHidden(e -> configView.removeTreeSelectionListener(selectionListener)); + // Remove the listener and dispose AlarmClient when the dialog is closed + this.setOnHidden(e -> { + configView.removeTreeSelectionListener(selectionListener); + dispose(); + }); // Add all components to layout content.getChildren().addAll( @@ -219,12 +179,54 @@ public AddComponentPVsDialog(AlarmClient alarmClient, List initialPVs, S null); return null; } + if (AlarmTreeHelper.validateNewPath(path, alarmClient.getRoot())) { + try { + getPVNames().forEach(pvName -> alarmClient.addPV(path, pvName)); + } catch (Exception ex) { + logger.log(Level.WARNING, "Cannot add component PVs to " + path, ex); + ExceptionDetailsErrorDialog.openError("Add Component PVs Failed", + "Failed to add component PVs to " + path, + ex); + } + } else { + // Show error dialog and retry + ExceptionDetailsErrorDialog.openError("Invalid Path", + "Invalid path. Please try again.", + null); + } return path; } return null; }); } + private void changeConfig(String new_config_name) { + // Dispose existing setup + dispose(); + + try + { + // Use same server name, but new config_name + final URI new_input = AlarmURI.createURI(AlarmSystem.server, new_config_name); + // no need for initial item name + alarmClient = new AlarmClient(AlarmSystem.server, new_config_name, AlarmSystem.kafka_properties); + AlarmTreeConfigView configView = new AlarmTreeConfigView(alarmClient); + alarmClient.start(); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot switch alarm tree to " + new_config_name, ex); + } + } + + private void dispose() + { + if (alarmClient != null) + { + alarmClient.shutdown(); + alarmClient = null; + } + } /** * Get the list of PV names entered by the user * From 14027951df84df92a664e3846d1463cff76497bd Mon Sep 17 00:00:00 2001 From: shroffk Date: Wed, 31 Dec 2025 14:12:46 -0500 Subject: [PATCH 06/11] Cleanup the dialog, the AlarmClient lifecycle is now completely within the dialog and not shared with the action --- .../ui/tree/ContextMenuAddComponentPVs.java | 44 +++---------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java index a5b93be6ac..0d5eb0c4fe 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java @@ -1,9 +1,7 @@ package org.phoebus.applications.alarm.ui.tree; -import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; -import javafx.scene.Node; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; import javafx.scene.control.Label; @@ -26,7 +24,6 @@ import java.net.URI; import java.util.List; -import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; @@ -61,17 +58,13 @@ public void call(Selection selection) throws Exception { pvs.stream().map(ProcessVariable::getName).collect(Collectors.toList()), null); - DialogResult dialogResult = addDialog.showAndGetResult(); - if (dialogResult == null) { - // User cancelled - return; - } + addDialog.showAndWait(); } /** * Dialog for adding component PVs to an alarm configuration */ - private static class AddComponentPVsDialog extends Dialog { + private static class AddComponentPVsDialog extends Dialog { private final TextArea pvNamesInput; private final TextField pathInput; @@ -169,7 +162,7 @@ public AddComponentPVsDialog(String server, String config_name, String kafka_pro getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); getDialogPane().setPrefSize(600, 700); - // Set result converter - returns path if OK, null if Cancel + // Set result converter - handles PV addition and returns null setResultConverter(buttonType -> { if (buttonType == ButtonType.OK) { String path = pathInput.getText().trim(); @@ -189,12 +182,11 @@ public AddComponentPVsDialog(String server, String config_name, String kafka_pro ex); } } else { - // Show error dialog and retry + // Show error dialog ExceptionDetailsErrorDialog.openError("Invalid Path", "Invalid path. Please try again.", null); } - return path; } return null; }); @@ -232,7 +224,7 @@ private void dispose() * * @return List of PV names (trimmed and non-empty) */ - public List getPVNames() { + private List getPVNames() { String text = pvNamesInput.getText(); if (text == null || text.trim().isEmpty()) { return List.of(); @@ -245,31 +237,5 @@ public List getPVNames() { .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } - - /** - * Show the dialog and get both the path and PV names - * - * @return DialogResult containing path and PV names, or null if cancelled - */ - public DialogResult showAndGetResult() { - Optional result = showAndWait(); - if (result.isPresent()) { - return new DialogResult(result.get(), getPVNames()); - } - return null; - } - } - - /** - * Result from AddComponentPVsDialog containing both path and PV names - */ - private static class DialogResult { - final String path; - final List pvNames; - - DialogResult(String path, List pvNames) { - this.path = path; - this.pvNames = pvNames; - } } } From 95c2b752c883549e8b91f4a3d68c71d1186001b9 Mon Sep 17 00:00:00 2001 From: shroffk Date: Wed, 31 Dec 2025 14:26:06 -0500 Subject: [PATCH 07/11] The "add pvs to alarm tree" supports multiple configuration --- .../ui/tree/ContextMenuAddComponentPVs.java | 135 +++++++++++------- 1 file changed, 82 insertions(+), 53 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java index 0d5eb0c4fe..71c6e33d67 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java @@ -15,14 +15,12 @@ import org.phoebus.applications.alarm.client.AlarmClient; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.ui.AlarmConfigSelector; -import org.phoebus.applications.alarm.ui.AlarmURI; import org.phoebus.core.types.ProcessVariable; import org.phoebus.framework.selection.Selection; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.spi.ContextMenuEntry; -import java.net.URI; import java.util.List; import java.util.logging.Level; import java.util.stream.Collectors; @@ -67,8 +65,15 @@ public void call(Selection selection) throws Exception { private static class AddComponentPVsDialog extends Dialog { private final TextArea pvNamesInput; private final TextField pathInput; + private final VBox content; + private final Label treeLabel; private AlarmClient alarmClient; + private AlarmTreeConfigView configView; + private ChangeListener>> selectionListener; + private final String server; + private final String kafka_properties; + /** * Constructor for AddComponentPVsDialog * @@ -80,16 +85,15 @@ private static class AddComponentPVsDialog extends Dialog { */ public AddComponentPVsDialog(String server, String config_name, String kafka_properties, List pvNames, String currentPath) { - // Model/Controller - - alarmClient = new AlarmClient(server, config_name, kafka_properties); + this.server = server; + this.kafka_properties = kafka_properties; setTitle("Add PVs to Alarm Configuration"); setHeaderText("Select PVs and destination path"); setResizable(true); // Create content - VBox content = new VBox(10); + content = new VBox(10); content.setPadding(new Insets(15)); // PV Names input section @@ -104,18 +108,8 @@ public AddComponentPVsDialog(String server, String config_name, String kafka_pro pvNamesInput.setText(String.join("; ", pvNames)); } - // Add AlarmTreeConfigView for path selection - Label treeLabel = new Label("Select destination in alarm tree:"); - AlarmTreeConfigView configView = new AlarmTreeConfigView(alarmClient); - - if (AlarmSystem.config_names.length > 0) { - final AlarmConfigSelector configs = new AlarmConfigSelector(config_name, this::changeConfig); - configView.getToolbar().getItems().add(0, configs); - } - - configView.setPrefHeight(300); - configView.setPrefWidth(500); - alarmClient.start(); + // Tree label + treeLabel = new Label("Select destination in alarm tree:"); // Path input Label pathLabel = new Label("Destination Path:"); @@ -125,32 +119,18 @@ public AddComponentPVsDialog(String server, String config_name, String kafka_pro pathInput.setPromptText("Select a path from the tree above or type manually"); pathInput.setEditable(true); - // Store the listener in a variable - ChangeListener>> selectionListener = (obs, oldVal, newVal) -> { - if (newVal != null && newVal.getValue() != null) { - String selectedPath = newVal.getValue().getPathName(); - if (selectedPath != null && !selectedPath.isEmpty()) { - // Only update if path input is not focused - if (!pathInput.isFocused()) { - pathInput.setText(selectedPath); - } - } - } - }; - configView.addTreeSelectionListener(selectionListener); - - // Remove the listener and dispose AlarmClient when the dialog is closed - this.setOnHidden(e -> { - configView.removeTreeSelectionListener(selectionListener); - dispose(); - }); - - // Add all components to layout + // Add static components to layout content.getChildren().addAll( pvLabel, pvNamesInput, - treeLabel, - configView, + treeLabel + ); + + // Create initial tree view + createTreeView(config_name); + + // Add path input section + content.getChildren().addAll( pathLabel, pathInput ); @@ -192,22 +172,71 @@ public AddComponentPVsDialog(String server, String config_name, String kafka_pro }); } + private void createTreeView(String config_name) { + // Create new AlarmClient + alarmClient = new AlarmClient(server, config_name, kafka_properties); + + // Create new AlarmTreeConfigView + configView = new AlarmTreeConfigView(alarmClient); + configView.setPrefHeight(300); + configView.setPrefWidth(500); + + // Add config selector if multiple configs are available + if (AlarmSystem.config_names.length > 0) { + final AlarmConfigSelector configs = new AlarmConfigSelector(config_name, this::changeConfig); + configView.getToolbar().getItems().add(0, configs); + } + + // Start the client + alarmClient.start(); + + // Create selection listener + selectionListener = (obs, oldVal, newVal) -> { + if (newVal != null && newVal.getValue() != null) { + String selectedPath = newVal.getValue().getPathName(); + if (selectedPath != null && !selectedPath.isEmpty()) { + // Only update if path input is not focused + if (!pathInput.isFocused()) { + pathInput.setText(selectedPath); + } + } + } + }; + configView.addTreeSelectionListener(selectionListener); + + // Remove the listener and dispose AlarmClient when the dialog is closed + this.setOnHidden(e -> { + configView.removeTreeSelectionListener(selectionListener); + dispose(); + }); + + // Find the position where tree view should be (after treeLabel) + int treeIndex = content.getChildren().indexOf(treeLabel) + 1; + + // Remove old tree view if present (when switching configs) + if (treeIndex < content.getChildren().size()) { + if (content.getChildren().get(treeIndex) instanceof AlarmTreeConfigView) { + content.getChildren().remove(treeIndex); + } + } + + // Add new tree view at the correct position + content.getChildren().add(treeIndex, configView); + VBox.setVgrow(configView, Priority.ALWAYS); + } + private void changeConfig(String new_config_name) { - // Dispose existing setup + // Dispose existing client dispose(); - try - { - // Use same server name, but new config_name - final URI new_input = AlarmURI.createURI(AlarmSystem.server, new_config_name); - // no need for initial item name - alarmClient = new AlarmClient(AlarmSystem.server, new_config_name, AlarmSystem.kafka_properties); - AlarmTreeConfigView configView = new AlarmTreeConfigView(alarmClient); - alarmClient.start(); - } - catch (Exception ex) - { + try { + // Create new tree view with new configuration + createTreeView(new_config_name); + } catch (Exception ex) { logger.log(Level.WARNING, "Cannot switch alarm tree to " + new_config_name, ex); + ExceptionDetailsErrorDialog.openError("Configuration Switch Failed", + "Failed to switch to configuration: " + new_config_name, + ex); } } From 9d1c23de2dbae705006ecc953e044ab93f2ef9ad Mon Sep 17 00:00:00 2001 From: shroffk Date: Mon, 5 Jan 2026 11:02:20 -0500 Subject: [PATCH 08/11] move the add PV action to the dialog instead of the action --- .../ui/tree/ContextMenuAddComponentPVs.java | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java index 71c6e33d67..1604103d32 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java @@ -142,33 +142,52 @@ public AddComponentPVsDialog(String server, String config_name, String kafka_pro getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); getDialogPane().setPrefSize(600, 700); - // Set result converter - handles PV addition and returns null - setResultConverter(buttonType -> { - if (buttonType == ButtonType.OK) { - String path = pathInput.getText().trim(); - if (path.isEmpty()) { - ExceptionDetailsErrorDialog.openError("Invalid Path", - "Destination path cannot be empty.", - null); - return null; - } - if (AlarmTreeHelper.validateNewPath(path, alarmClient.getRoot())) { - try { - getPVNames().forEach(pvName -> alarmClient.addPV(path, pvName)); - } catch (Exception ex) { - logger.log(Level.WARNING, "Cannot add component PVs to " + path, ex); - ExceptionDetailsErrorDialog.openError("Add Component PVs Failed", - "Failed to add component PVs to " + path, - ex); - } - } else { - // Show error dialog - ExceptionDetailsErrorDialog.openError("Invalid Path", - "Invalid path. Please try again.", - null); - } + // Validate and add PVs when OK is clicked + getDialogPane().lookupButton(ButtonType.OK).addEventFilter(javafx.event.ActionEvent.ACTION, event -> { + // Validate path + String path = pathInput.getText().trim(); + if (path.isEmpty()) { + event.consume(); // Prevent dialog from closing + ExceptionDetailsErrorDialog.openError("Invalid Path", + "Destination path cannot be empty.\nPlease enter or select a valid path.", + null); + return; + } + + // Validate that path exists in the alarm tree + if (!AlarmTreeHelper.validateNewPath(path, alarmClient.getRoot())) { + event.consume(); // Prevent dialog from closing + ExceptionDetailsErrorDialog.openError("Invalid Path", + "The path '" + path + "' is not valid in the alarm tree.\n\n" + + "Please select a valid path from the tree or enter a valid path manually.", + null); + return; + } + + // Get PV names + List pvNamesToAdd = getPVNames(); + if (pvNamesToAdd.isEmpty()) { + event.consume(); // Prevent dialog from closing + ExceptionDetailsErrorDialog.openError("No PV Names", + "No PV names were entered.\n\n" + + "Please enter one or more PV names separated by semicolons (;).", + null); + return; + } + + // Try to add PVs + try { + pvNamesToAdd.forEach(pvName -> alarmClient.addPV(path, pvName)); + logger.log(Level.INFO, "Successfully added " + pvNamesToAdd.size() + " PV(s) to " + path); + } catch (Exception ex) { + event.consume(); // Prevent dialog from closing + logger.log(Level.WARNING, "Cannot add component PVs to " + path, ex); + ExceptionDetailsErrorDialog.openError("Add Component PVs Failed", + "Failed to add PVs to path: " + path + "\n\n" + + "PVs attempted: " + String.join(", ", pvNamesToAdd) + "\n\n" + + "Error: " + ex.getMessage(), + ex); } - return null; }); } From eb15e540d2b96280ab4c538630e2d5f4add3394a Mon Sep 17 00:00:00 2001 From: shroffk Date: Mon, 5 Jan 2026 13:16:08 -0500 Subject: [PATCH 09/11] More clear name for the context menu action to add PVs to the alarm system --- .../applications/alarm/ui/tree/ContextMenuAddComponentPVs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java index 1604103d32..ffb04ee627 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java @@ -33,7 +33,7 @@ public class ContextMenuAddComponentPVs implements ContextMenuEntry { @Override public String getName() { - return "Add Component"; + return "Add PVs to Alarm System"; } @Override From b51cd18bd4694d6c1165407554f9cdd4a0c6efe1 Mon Sep 17 00:00:00 2001 From: shroffk Date: Thu, 8 Jan 2026 09:38:04 -0500 Subject: [PATCH 10/11] Handle the new AlarmTreePathException --- .../ui/tree/ContextMenuAddComponentPVs.java | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java index ffb04ee627..7c6b6af104 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/ContextMenuAddComponentPVs.java @@ -175,18 +175,41 @@ public AddComponentPVsDialog(String server, String config_name, String kafka_pro return; } - // Try to add PVs - try { - pvNamesToAdd.forEach(pvName -> alarmClient.addPV(path, pvName)); - logger.log(Level.INFO, "Successfully added " + pvNamesToAdd.size() + " PV(s) to " + path); - } catch (Exception ex) { + // Try to add PVs, tracking successes and failures + List successfulPVs = new java.util.ArrayList<>(); + List failedPVs = new java.util.ArrayList<>(); + Exception lastException = null; + + for (String pvName : pvNamesToAdd) { + try { + alarmClient.addPV(path, pvName); + successfulPVs.add(pvName); + } catch (Exception e) { + failedPVs.add(pvName); + lastException = e; + logger.log(Level.WARNING, "Failed to add PV '" + pvName + "' to " + path, e); + } + } + + // Report results + if (!failedPVs.isEmpty()) { event.consume(); // Prevent dialog from closing - logger.log(Level.WARNING, "Cannot add component PVs to " + path, ex); - ExceptionDetailsErrorDialog.openError("Add Component PVs Failed", - "Failed to add PVs to path: " + path + "\n\n" + - "PVs attempted: " + String.join(", ", pvNamesToAdd) + "\n\n" + - "Error: " + ex.getMessage(), - ex); + String message = String.format( + "Failed to add %d of %d PV(s) to path: %s\n\n" + + "Successful: %s\n" + + "Failed: %s\n\n" + + "Last error: %s", + failedPVs.size(), + pvNamesToAdd.size(), + path, + successfulPVs.isEmpty() ? "None" : String.join(", ", successfulPVs), + String.join(", ", failedPVs), + lastException != null ? lastException.getMessage() : "Unknown" + ); + logger.log(Level.WARNING, message); + ExceptionDetailsErrorDialog.openError("Add Component PVs Failed", message, lastException); + } else { + logger.log(Level.INFO, "Successfully added " + successfulPVs.size() + " PV(s) to " + path); } }); } From b7791b710d69a7c0fe624ec65a1c966cc53cbfaf Mon Sep 17 00:00:00 2001 From: shroffk Date: Fri, 9 Jan 2026 13:53:58 -0500 Subject: [PATCH 11/11] prepare for moving CI/CD to JDK 21 --- .github/workflows/build.yml | 2 +- .github/workflows/build_latest.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b1e890f3d..00ee599c99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Maven and Java Action uses: s4u/setup-maven-action@v1.18.0 with: - java-version: '17' + java-version: '21' maven-version: '3.9.6' - name: Build run: mvn --batch-mode install -DskipTests \ No newline at end of file diff --git a/.github/workflows/build_latest.yml b/.github/workflows/build_latest.yml index 6d39ae0918..72bc62f3be 100644 --- a/.github/workflows/build_latest.yml +++ b/.github/workflows/build_latest.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Maven and Java Action uses: s4u/setup-maven-action@v1.18.0 with: - java-version: '17' + java-version: '21' maven-version: '3.9.6' - name: Build run: mvn --batch-mode install -DskipTests