diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java index d6b3e08b26..189c0256cc 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/Messages.java @@ -42,6 +42,7 @@ public class Messages BoolButtonError_Body, BoolButtonError_Title, Blue, + Cancel, ColorDialog_Current, ColorDialog_Custom, ColorDialog_Default, @@ -62,6 +63,12 @@ public class Messages ConvertToEmbeddedJavaScript, ConvertToEmbeddedPython, ConvertToScriptFile, + CopyPVName, + CopyPVNames, + CopyPVNameAndDescription, + CopyPVNamesAndDescriptions, + Description, + DeSelectAll, Edit, ExportWidgetInfo, ExportFailed, @@ -102,6 +109,7 @@ public class Messages PointsTable_Empty, PointsTable_X, PointsTable_Y, + PVName, Red, Remove, Row, @@ -136,6 +144,7 @@ public class Messages ScriptsDialog_ScriptsTT, ScriptsDialog_Title, Select, + SelectAll, ShowConfirmationDialogTitle, ShowErrorDialogTitle, ShowMessageDialogTitle, diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java index b685b56ce7..655ce9b688 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/WidgetInfoDialog.java @@ -14,11 +14,46 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.FutureTask; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.control.cell.CheckBoxTableCell; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.stage.Window; +import javafx.util.Pair; import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.WidgetDescriptor; @@ -44,22 +79,10 @@ import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.scene.Node; -import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.Button; import javafx.scene.control.ButtonBar.ButtonData; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Dialog; -import javafx.scene.control.Label; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; import javafx.stage.FileChooser; /** Dialog for displaying widget information @@ -77,6 +100,7 @@ public class WidgetInfoDialog extends Dialog /** PV info */ public static class NameStateValue { + public SimpleBooleanProperty selected; /** PV Name */ public final String name; /** State, incl. read-only or writable? */ @@ -93,6 +117,7 @@ public static class NameStateValue */ public NameStateValue(final String name, final String state, final VType value, final String path) { + this.selected = new SimpleBooleanProperty(false); this.name = name; this.state = state; this.value = value; @@ -134,7 +159,8 @@ protected void updateItem(final String item, final boolean empty) * @param widget {@link Widget} * @param pvs {@link Collection}s, may be empty */ - public WidgetInfoDialog(final Widget widget, final Collection pvs) + public WidgetInfoDialog(final Widget widget, + final Collection pvs) { this.pvs = pvs; this.widget = widget; @@ -155,7 +181,10 @@ public WidgetInfoDialog(final Widget widget, final Collection pv // No icon, no problem } } - final TabPane tabs = new TabPane(createProperties(widget), createPVs(pvs), createMacros(widget.getEffectiveMacros())); + final CheckBox selectAllPVsCheckBox = new CheckBox(); + TableView pvsTable = createPVs(pvs, selectAllPVsCheckBox); + Tab pvTab = new Tab(Messages.WidgetInfoDialog_TabPVs, pvsTable); + final TabPane tabs = new TabPane(createProperties(widget), pvTab, createMacros(widget.getEffectiveMacros())); // For display model, show stats if (widget instanceof DisplayModel) @@ -167,9 +196,11 @@ public WidgetInfoDialog(final Widget widget, final Collection pv tabs.getSelectionModel().select(1); final ButtonType export = new ButtonType(Messages.ExportWidgetInfo, ButtonData.LEFT); + final ButtonType copyPvNamesButtonType = new ButtonType(Messages.CopyPVNames, ButtonData.LEFT); + final ButtonType copyPvNamesWithDescriptionsButtonType = new ButtonType(Messages.CopyPVNamesAndDescriptions + "...", ButtonData.LEFT); getDialogPane().setContent(tabs); - getDialogPane().getButtonTypes().addAll(export, ButtonType.CLOSE); + getDialogPane().getButtonTypes().addAll(export, copyPvNamesButtonType, copyPvNamesWithDescriptionsButtonType, ButtonType.CLOSE); setResizable(true); tabs.setMinWidth(800); @@ -189,6 +220,107 @@ public WidgetInfoDialog(final Widget widget, final Collection pv } ); + Button copyPvNamesWithDescriptionsButton = (Button) getDialogPane().lookupButton(copyPvNamesWithDescriptionsButtonType); + + setOnCloseRequest(closeRequest -> { + // Prevent pressing copyPvNamesWithDescriptionsButton from closing the WidgetInfoDialog: + if (copyPvNamesWithDescriptionsButton.isPressed() || copyPvNamesWithDescriptionsButton.isArmed()) { + closeRequest.consume(); + } + }); + + Button copyPvNamesButton = (Button) getDialogPane().lookupButton(copyPvNamesButtonType); + + { + // copyPvNamesButton and copyPvNamesWithDescriptionsButton are only visible on the PV-tab: + tabs.getSelectionModel().selectedItemProperty().addListener((property, old_value, new_value) -> { + if (new_value == pvTab) { + copyPvNamesButton.setVisible(true); + copyPvNamesWithDescriptionsButton.setVisible(true); + } + else { + copyPvNamesButton.setVisible(false); + copyPvNamesWithDescriptionsButton.setVisible(false); + } + }); + + if (tabs.getSelectionModel().getSelectedItem() == pvTab) { + copyPvNamesButton.setVisible(true); + copyPvNamesWithDescriptionsButton.setVisible(true); + } + else { + copyPvNamesButton.setVisible(false); + copyPvNamesWithDescriptionsButton.setVisible(false); + } + } + + { + // Disable copyPvNamesWithDescriptionsButton when no PV has been selected: + List selectedStatuses = pvs.stream().map(nameStateValue -> nameStateValue.selected).collect(Collectors.toList()); + + Runnable enableOrDisableButtonsAndCheckbox = () -> { + if (selectedStatuses.stream().allMatch(selected -> !selected.get())) { + copyPvNamesButton.setDisable(true); + copyPvNamesWithDescriptionsButton.setDisable(true); + selectAllPVsCheckBox.setIndeterminate(false); + selectAllPVsCheckBox.setSelected(false); + } + else { + copyPvNamesButton.setDisable(false); + copyPvNamesWithDescriptionsButton.setDisable(false); + if (selectedStatuses.stream().allMatch(selected -> selected.get())) { + selectAllPVsCheckBox.setIndeterminate(false); + selectAllPVsCheckBox.setSelected(true); + } + else { + selectAllPVsCheckBox.setIndeterminate(true); + selectAllPVsCheckBox.setSelected(false); + } + } + }; + + for (var isPVSelectedProperty : selectedStatuses) { + isPVSelectedProperty.addListener((property, old_value, new_value) -> enableOrDisableButtonsAndCheckbox.run()); + } + + enableOrDisableButtonsAndCheckbox.run(); + } + + copyPvNamesButton.setOnAction(actionEvent -> { + List pvNames = new LinkedList<>(); + for (NameStateValue nameStateValue : pvsTable.getItems()) { + if (nameStateValue.selected.get()) { + pvNames.add(nameStateValue.name); + } + } + copyPVNamesToClipboard(pvNames); + }); + + copyPvNamesWithDescriptionsButton.setOnAction(actionEvent -> { + + List> pvNamesAndDefaultDescriptions = new LinkedList<>(); + for (NameStateValue nameStateValue : pvsTable.getItems()) { + if (nameStateValue.selected.get()) { + String opiName; + try { + opiName = widget.getTopDisplayModel().getName(); + } + catch (Exception exception) { + String warningMessage_formatString = "The PV ''{0}'' doesn't have an associated top-level display model."; + String warningMessage = MessageFormat.format(warningMessage_formatString, nameStateValue.name); + Logger.getLogger(getClass().getName()).log(Level.WARNING, warningMessage, exception); + + opiName = ""; + } + Pair pvNameAndDefaultDescription = new Pair<>(nameStateValue.name, opiName); + pvNamesAndDefaultDescriptions.add(pvNameAndDefaultDescription); + } + } + + copyPVNamesAndDescriptionsDialog(pvNamesAndDefaultDescriptions, + pvsTable.getScene().getWindow()); + }); + setResultConverter(button -> true); } @@ -288,8 +420,42 @@ private Tab createMacros(final Macros orig_macros) return new Tab(Messages.WidgetInfoDialog_TabMacros, table); } - private Tab createPVs(final Collection pvs) + private TableView createPVs(final Collection pvs, + final CheckBox selectAllPVsCheckBox) { + final TableColumn selectionColumn = new TableColumn<>(); + { + double columnWidth = 40; + selectionColumn.setMinWidth(columnWidth); + selectionColumn.setPrefWidth(columnWidth); + selectionColumn.setMaxWidth(columnWidth); + } + + selectionColumn.graphicProperty().set(selectAllPVsCheckBox); + selectAllPVsCheckBox.setOnAction(actionEvent -> { + if (selectAllPVsCheckBox.isSelected() || selectAllPVsCheckBox.isIndeterminate()) { + selectAllPVsCheckBox.setSelected(true); + pvs.forEach(nameStateValue -> nameStateValue.selected.set(true)); + } + else { + pvs.forEach(nameStateValue -> nameStateValue.selected.set(false)); + } + }); + + MenuItem selectAllMenuItem = new MenuItem(Messages.SelectAll); + selectAllMenuItem.setOnAction(actionEvent -> pvs.forEach(nameStateValue -> nameStateValue.selected.set(true))); + MenuItem deSelectAllMenuItem = new MenuItem(Messages.DeSelectAll); + deSelectAllMenuItem.setOnAction(actionEvent -> pvs.forEach(nameStateValue -> nameStateValue.selected.set(false))); + + ContextMenu selectionContextMenu = new ContextMenu(selectAllMenuItem, deSelectAllMenuItem); + + selectionColumn.setCellFactory(col -> { + CheckBoxTableCell checkBoxTableCell = new CheckBoxTableCell<>(); + checkBoxTableCell.setContextMenu(selectionContextMenu); + return checkBoxTableCell; + }); + selectionColumn.setCellValueFactory(param -> param.getValue().selected); + // Use text field to allow users to copy the name, value to clipboard final TableColumn name = new TableColumn<>(Messages.WidgetInfoDialog_Name); name.setCellFactory(col -> new ReadOnlyTextCell<>()); @@ -314,13 +480,15 @@ private Tab createPVs(final Collection pvs) final ObservableList pv_data = FXCollections.observableArrayList(pvs); pv_data.sort(Comparator.comparing(a -> a.name)); final TableView table = new TableView<>(pv_data); + table.setEditable(true); + table.getColumns().add(selectionColumn); table.getColumns().add(name); table.getColumns().add(state); table.getColumns().add(value); table.getColumns().add(path); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - return new Tab(Messages.WidgetInfoDialog_TabPVs, table); + return table; } private Tab createProperties(final Widget widget) @@ -423,4 +591,206 @@ private String getPVValue(VType vtype){ } return text; } + + public static void copyPVNamesAndDescriptionsDialog(final List> pvNamesAndDefaultDescriptions, + final Window windowToPositionTheConfirmationDialogOver) { + + FutureTask>> displayCopyDialog = new FutureTask(() -> { + final Dialog copyDialog = new Dialog(); + + boolean usePlural = pvNamesAndDefaultDescriptions.size() > 1; + if (usePlural) { + copyDialog.setTitle(Messages.CopyPVNames); + copyDialog.setHeaderText(Messages.CopyPVNames); + } + else { + copyDialog.setTitle(Messages.CopyPVName); + copyDialog.setHeaderText(Messages.CopyPVName); + } + + { + // In JavaFX, a button of type ButtonType.CLOSE must be present for the 'X'-button to work. + // The button created by ButtonType.CLOSE is, however, not left-aligned correctly, and + // therefore the button defined by ButtonType.CLOSE is hidden and a custom ButtonType is + // declared for the close-button. + copyDialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE); + Button hiddenCloseButton = (Button) copyDialog.getDialogPane().lookupButton(ButtonType.CLOSE); + hiddenCloseButton.setVisible(false); + } + + // Custom close-button with correct alignment: + final Button closeButton; + { + ButtonType closeButtonType = new ButtonType(Messages.Cancel, ButtonData.LEFT); + copyDialog.getDialogPane().getButtonTypes().add(closeButtonType); + closeButton = (Button) copyDialog.getDialogPane().lookupButton(closeButtonType); + ButtonBar.setButtonUniformSize(closeButton, false); + } + + final Button copyWithoutDescriptionButton; + { + ButtonType copyWithoutDescriptionButtonType = new ButtonType(usePlural ? Messages.CopyPVNames : Messages.CopyPVName, ButtonData.RIGHT); + copyDialog.getDialogPane().getButtonTypes().add(copyWithoutDescriptionButtonType); + copyWithoutDescriptionButton = (Button) copyDialog.getDialogPane().lookupButton(copyWithoutDescriptionButtonType); + copyWithoutDescriptionButton.setTooltip(new Tooltip(copyWithoutDescriptionButton.getText())); + ButtonBar.setButtonUniformSize(copyWithoutDescriptionButton, false); + } + + final Button copyWithDescriptionButton; + { + ButtonType copyWithDescriptionButtonType = new ButtonType(usePlural ? Messages.CopyPVNamesAndDescriptions : Messages.CopyPVNameAndDescription, ButtonData.RIGHT); + copyDialog.getDialogPane().getButtonTypes().add(copyWithDescriptionButtonType); + copyWithDescriptionButton = (Button) copyDialog.getDialogPane().lookupButton(copyWithDescriptionButtonType); + copyWithDescriptionButton.setTooltip(new Tooltip(copyWithDescriptionButton.getText())); + ButtonBar.setButtonUniformSize(copyWithDescriptionButton, false); + } + + final GridPane gridPane = new GridPane(); + gridPane.setPrefWidth(Double.MAX_VALUE); + gridPane.setHgap(0); + gridPane.setVgap(4); + + final ColumnConstraints firstColumnColumnConstraints = new ColumnConstraints(); + gridPane.getColumnConstraints().add(0, firstColumnColumnConstraints); + + // The second column defines the gap between the first and third columns. + final ColumnConstraints secondColumnColumnConstraints = new ColumnConstraints(); + int width = 10; + secondColumnColumnConstraints.setMinWidth(width); + secondColumnColumnConstraints.setPrefWidth(width); + secondColumnColumnConstraints.setMaxWidth(width); + gridPane.getColumnConstraints().add(1, secondColumnColumnConstraints); + + final ColumnConstraints thirdColumnColumnConstraints = new ColumnConstraints(); + thirdColumnColumnConstraints.setHgrow(Priority.SOMETIMES); + thirdColumnColumnConstraints.setFillWidth(true); + gridPane.getColumnConstraints().add(2, thirdColumnColumnConstraints); + + int currentRow = 0; + + final List> pvNamesAndDescriptionTextFields = new LinkedList<>(); + boolean pvsHaveBeenAddedPreviously = false; + for (Pair pvNameAndDefaultDescription : pvNamesAndDefaultDescriptions) { + String pvName = pvNameAndDefaultDescription.getKey(); + String defaultDescription = pvNameAndDefaultDescription.getValue(); + + if (pvsHaveBeenAddedPreviously) { + gridPane.add(new Separator(), 0, currentRow, 3, 1); + currentRow++; + } + + Text pvNameLabelText = new Text(Messages.PVName + ":"); + pvNameLabelText.setStyle("-fx-font-size: 14; -fx-font-weight: bold; "); + gridPane.add(pvNameLabelText, 0, currentRow); + + Text pvNameText = new Text(pvName); + pvNameText.setStyle("-fx-font-size: 14; -fx-font-style: italic; "); + gridPane.add(pvNameText, 2, currentRow); + currentRow++; + + Text descriptionLabelText = new Text(Messages.Description + ":"); + descriptionLabelText.setStyle("-fx-font-size: 14; -fx-font-weight: bold; "); + gridPane.add(descriptionLabelText, 0, currentRow); + + TextField descriptionText = new TextField(defaultDescription); + descriptionText.setStyle("-fx-font-size: 14; "); + descriptionText.setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER) { + keyEvent.consume(); + copyWithDescriptionButton.fire(); + } + }); + gridPane.add(descriptionText, 2, currentRow); + currentRow++; + + pvNamesAndDescriptionTextFields.add(new Pair(pvName, descriptionText)); + pvsHaveBeenAddedPreviously = true; + } + + copyWithoutDescriptionButton.setOnAction(actionEvent -> { + List pvNames = pvNamesAndDescriptionTextFields.stream().map(pvNameAndTextField -> pvNameAndTextField.getKey()).collect(Collectors.toList()); + copyPVNamesToClipboard(pvNames); + }); + + copyWithDescriptionButton.setOnAction(actionEvent -> { + List> pvNamesAndDescriptions = pvNamesAndDescriptionTextFields.stream().map(pvNameAndTextField -> new Pair(pvNameAndTextField.getKey(), pvNameAndTextField.getValue().getText())).collect(Collectors.toList()); + copyPVNamesAndDescriptionsToClipboard(pvNamesAndDescriptions); + }); + + ScrollPane scrollPane = new ScrollPane(gridPane); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + copyDialog.getDialogPane().setContent(scrollPane); + + copyDialog.getDialogPane().setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ESCAPE) { + closeButton.fire(); + } + }); + + int prefWidth = 650; + int maxHeight = 600; + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + copyDialog.setResizable(true); + copyDialog.getDialogPane().setPrefWidth(prefWidth); + copyDialog.getDialogPane().setMaxHeight(maxHeight); + copyDialog.initOwner(windowToPositionTheConfirmationDialogOver); + + Platform.runLater(() -> { + if (pvNamesAndDescriptionTextFields.size() > 0) { + TextField topMostDescriptionTextField = pvNamesAndDescriptionTextFields.get(0).getValue(); + topMostDescriptionTextField.requestFocus(); + topMostDescriptionTextField.selectAll(); + } + }); + + copyDialog.showAndWait(); + return Optional.empty(); + }); + + if (Platform.isFxApplicationThread()) { + displayCopyDialog.run(); + } + else { + Platform.runLater(displayCopyDialog); + } + } + + static private void copyPVNamesToClipboard(List pvNames) { + String newClipboardContent = ""; + + for (String pvName : pvNames) { + if (!newClipboardContent.equals("")) { + newClipboardContent += System.lineSeparator(); + } + newClipboardContent += pvName; + } + + ClipboardContent newContent = new ClipboardContent(); + newContent.putString(newClipboardContent); + + Clipboard clipboard = Clipboard.getSystemClipboard(); + clipboard.setContent(newContent); + } + + static private void copyPVNamesAndDescriptionsToClipboard(List> pvNamesAndDescriptions) { + String newClipboardContent = ""; + + for (Pair pvNameAndDescription : pvNamesAndDescriptions) { + String pvName = pvNameAndDescription.getKey(); + String description = pvNameAndDescription.getValue(); + + if (!newClipboardContent.equals("")) { + newClipboardContent += System.lineSeparator(); + } + newClipboardContent += pvName + "," + description; + } + + ClipboardContent newContent = new ClipboardContent(); + newContent.putString(newClipboardContent); + + Clipboard clipboard = Clipboard.getSystemClipboard(); + clipboard.setContent(newContent); + } } diff --git a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties b/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties index 05c6b429d1..9a84dc4d11 100644 --- a/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties +++ b/app/display/representation-javafx/src/main/resources/org/csstudio/display/builder/representation/javafx/messages.properties @@ -24,6 +24,7 @@ Alpha=Alpha Blue=Blue BoolButtonError_Body=Confirmation dialog supported only for toggle mode. BoolButtonError_Title=Unsupported configuration +Cancel=Cancel ColorDialog_Current=Current ColorDialog_Custom=Custom Color ColorDialog_Default=Default @@ -45,6 +46,12 @@ ConvertToEmbeddedJavaScript=Convert to Embedded JavaScript\u2026 ConvertToEmbeddedPython=Convert to Embedded Python\u2026 ConvertToScriptFile=Convert to Script File Copy=Copy +CopyPVName=Copy PV Name +CopyPVNames=Copy PV Names +CopyPVNameAndDescription=Copy PV Name & Description +CopyPVNamesAndDescriptions=Copy PV Names & Descriptions +Description=Description +DeSelectAll=De-Select All Duplicate=Duplicate Edit=Edit\u2026 ExportWidgetInfo=Export to file @@ -84,6 +91,7 @@ PointsDialog_Title=Edit Points PointsTable_Empty=No points PointsTable_X=X PointsTable_Y=Y +PVName=PV Name Red=Red Remove=Remove Row=Row @@ -118,6 +126,7 @@ ScriptsDialog_PythonScriptFile=my_script.py ScriptsDialog_ScriptsTT=Select external script file or edit embedded script text ScriptsDialog_Title=Scripts Select=Select\u2026 +SelectAll=Select All ShowConfirmationDialogTitle=Please Confirm ShowErrorDialogTitle=Error ShowMessageDialogTitle=Message diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuAppendPvToClipboard.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuAppendPvToClipboard.java new file mode 100644 index 0000000000..2cf9292b91 --- /dev/null +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuAppendPvToClipboard.java @@ -0,0 +1,52 @@ +package org.csstudio.display.builder.runtime; + +import javafx.scene.image.Image; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import org.phoebus.core.types.ProcessVariable; +import org.phoebus.framework.selection.Selection; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.spi.ContextMenuEntry; + +import java.util.List; +import java.util.stream.Collectors; + +public class ContextMenuAppendPvToClipboard implements ContextMenuEntry { + @Override + public String getName() { + return Messages.AppendPVNameToClipboard; + } + + private Image icon = ImageCache.getImage(ImageCache.class, "/icons/copy.png"); + @Override + public Image getIcon() { + return icon; + } + + @Override + public Class getSupportedType() { + return ProcessVariable.class; + } + + @Override + public void call(final Selection selection) + { + List pvs = selection.getSelections(); + String pvNamesToAppendToClipboard = pvs.stream().map(ProcessVariable::getName).collect(Collectors.joining(System.lineSeparator())); + + Clipboard clipboard = Clipboard.getSystemClipboard(); + String newContentInClipboard; + { + String existingContentInClipboard; + if (clipboard.hasString()) { + existingContentInClipboard = clipboard.getString() + "\n"; + } else { + existingContentInClipboard = ""; + } + newContentInClipboard = existingContentInClipboard + pvNamesToAppendToClipboard; + } + ClipboardContent newContent = new ClipboardContent(); + newContent.putString(newContentInClipboard); + clipboard.setContent(newContent); + } +} diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuAppendPvToClipboardWithDescription.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuAppendPvToClipboardWithDescription.java new file mode 100644 index 0000000000..490396cc83 --- /dev/null +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuAppendPvToClipboardWithDescription.java @@ -0,0 +1,205 @@ +package org.csstudio.display.builder.runtime; + +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.text.Text; +import javafx.stage.Window; +import javafx.util.Pair; +import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.phoebus.core.types.ProcessVariable; +import org.phoebus.framework.selection.Selection; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.docking.DockItem; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.spi.ContextMenuEntry; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +public class ContextMenuAppendPvToClipboardWithDescription implements ContextMenuEntry { + @Override + public String getName() { + return Messages.AppendPVNameToClipboardWithDescription; + } + + private Image icon = ImageCache.getImage(ImageCache.class, "/icons/copy.png"); + @Override + public Image getIcon() { + return icon; + } + + @Override + public Class getSupportedType() { + return ProcessVariable.class; + } + + @Override + public void call(final Selection selection) + { + List pvs = selection.getSelections(); + String pvNamesToAppendToClipboard = pvs.stream().map(ProcessVariable::getName).collect(Collectors.joining(System.lineSeparator())); + + String defaultDescription; + { + var activeDockPane = DockPane.getActiveDockPane(); + var activeDockItem = (DockItem) activeDockPane.getSelectionModel().getSelectedItem(); + if (activeDockItem.getApplication() instanceof DisplayRuntimeInstance) { + DisplayRuntimeInstance displayRuntimeInstance = (DisplayRuntimeInstance) activeDockItem.getApplication(); + defaultDescription = displayRuntimeInstance.getDisplayName(); + } + else { + defaultDescription = ""; + } + } + + BiConsumer appendPVAndDescriptionToClipboardContinuation = (pvName, description) -> { + Clipboard clipboard = Clipboard.getSystemClipboard(); + String newContentInClipboard; + { + String existingContentInClipboard; + if (clipboard.hasString()) { + existingContentInClipboard = clipboard.getString() + "\n"; + } else { + existingContentInClipboard = ""; + } + newContentInClipboard = existingContentInClipboard + pvName + "," + description; + } + ClipboardContent newContent = new ClipboardContent(); + newContent.putString(newContentInClipboard); + clipboard.setContent(newContent); + }; + + String pvName = pvNamesToAppendToClipboard; + + addDescriptionToPvNameModalDialog(pvName, + defaultDescription, + "Append", + appendPVAndDescriptionToClipboardContinuation); + } + + public static void addDescriptionToPvNameModalDialog(String pvName, + String defaultDescription, + String acceptButtonText, + BiConsumer continuation) { + Window windowToPositionTheConfirmationDialogOver = DockPane.getActiveDockPane().getScene().getWindow(); + + ButtonType acceptButtonType = new ButtonType(acceptButtonText); + ButtonType cancelButtonType = new ButtonType("Cancel"); + + FutureTask>> displayConfirmationWindow = new FutureTask(() -> { + Alert prompt = new Alert(Alert.AlertType.NONE); + + prompt.getDialogPane().getButtonTypes().add(cancelButtonType); + Button cancelButton = (Button) prompt.getDialogPane().lookupButton(cancelButtonType); + cancelButton.setTooltip(new Tooltip(cancelButton.getText())); + + prompt.getDialogPane().getButtonTypes().add(acceptButtonType); + Button acceptButton = (Button) prompt.getDialogPane().lookupButton(acceptButtonType); + acceptButton.setTooltip(new Tooltip(acceptButton.getText())); + + GridPane gridPane = new GridPane(); + gridPane.setPrefWidth(Double.MAX_VALUE); + gridPane.setVgap(4); + + ColumnConstraints firstColumnColumnConstraints = new ColumnConstraints(); + gridPane.getColumnConstraints().add(0, firstColumnColumnConstraints); + ColumnConstraints secondColumnColumnConstraints = new ColumnConstraints(); + secondColumnColumnConstraints.setHgrow(Priority.ALWAYS); + secondColumnColumnConstraints.setFillWidth(true); + gridPane.getColumnConstraints().add(1, secondColumnColumnConstraints); + + int currentRow = 0; + + Text pvNameLabelText = new Text("PV Name: "); + pvNameLabelText.setStyle("-fx-font-size: 14; -fx-font-weight: bold; "); + gridPane.add(pvNameLabelText, 0, currentRow); + + Text pvNameText = new Text(pvName); + pvNameText.setStyle("-fx-font-size: 14; -fx-font-style: italic; "); + gridPane.add(pvNameText, 1, currentRow); + currentRow++; + + Text descriptionLabelText = new Text("Description: "); + descriptionLabelText.setStyle("-fx-font-size: 14; -fx-font-weight: bold; "); + gridPane.add(descriptionLabelText, 0, currentRow); + + TextField descriptionText = new TextField(defaultDescription); + descriptionText.setStyle("-fx-font-size: 14; "); + descriptionText.setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER) { + keyEvent.consume(); + acceptButton.fire(); + } + }); + gridPane.add(descriptionText, 1, currentRow); + currentRow++; + + prompt.getDialogPane().setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ESCAPE) { + keyEvent.consume(); + cancelButton.fire(); + } + }); + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setContent(gridPane); + + prompt.getDialogPane().setContent(gridPane); + + prompt.setHeaderText("Append PV Name with Description to the Clipboard"); + prompt.setTitle("Append PV Name with Description to the Clipboard"); + + int prefWidth = 500; + int prefHeight = 150; + prompt.getDialogPane().setPrefSize(prefWidth, prefHeight); + prompt.getDialogPane().setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + prompt.setResizable(false); + + DialogHelper.positionDialog(prompt, windowToPositionTheConfirmationDialogOver.getScene().getRoot(), -prefWidth/2, -prefHeight/2); + descriptionText.requestFocus(); + descriptionText.selectAll(); + + if (prompt.showAndWait().orElse(ButtonType.CANCEL) == acceptButtonType) { + String description = descriptionText.getText(); + Pair pvNameAndDescription = new Pair<>(pvName, description); + return Optional.of(pvNameAndDescription); + } + else { + return Optional.empty(); + } + }); + + if (Platform.isFxApplicationThread()) { + displayConfirmationWindow.run(); + } + else { + Platform.runLater(displayConfirmationWindow); + } + try { + Optional> maybePVNameAndDescription = displayConfirmationWindow.get(); + maybePVNameAndDescription.ifPresent(pair -> continuation.accept(pair.getKey(), pair.getValue())); + } catch (ExecutionException e) { + ; // Do nothing. + } catch (InterruptedException e) { + ; // Do nothing. + } + } +} diff --git a/app/probe/src/main/java/org/phoebus/applications/probe/ContextMenuPvAndValueToClipboard.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvAndValueToClipboard.java similarity index 94% rename from app/probe/src/main/java/org/phoebus/applications/probe/ContextMenuPvAndValueToClipboard.java rename to app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvAndValueToClipboard.java index 11615ef796..0eac7339d4 100644 --- a/app/probe/src/main/java/org/phoebus/applications/probe/ContextMenuPvAndValueToClipboard.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvAndValueToClipboard.java @@ -5,13 +5,13 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ -package org.phoebus.applications.probe; - -import static org.phoebus.applications.probe.Probe.logger; +package org.csstudio.display.builder.runtime; import java.util.List; import java.util.logging.Level; +import static org.csstudio.display.builder.runtime.WidgetRuntime.logger; + import org.epics.vtype.Alarm; import org.epics.vtype.AlarmSeverity; import org.epics.vtype.Time; @@ -27,7 +27,7 @@ * @author Kay Kasemir */ @SuppressWarnings("nls") -public class ContextMenuPvAndValueToClipboard extends ContextMenuPvToClipboard +public class ContextMenuCopyPvAndValueToClipboard extends ContextMenuCopyPvToClipboard { @Override public String getName() diff --git a/app/probe/src/main/java/org/phoebus/applications/probe/ContextMenuPvToClipboard.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvToClipboard.java similarity index 94% rename from app/probe/src/main/java/org/phoebus/applications/probe/ContextMenuPvToClipboard.java rename to app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvToClipboard.java index 20ffe941f4..357f8e98c3 100644 --- a/app/probe/src/main/java/org/phoebus/applications/probe/ContextMenuPvToClipboard.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvToClipboard.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ -package org.phoebus.applications.probe; +package org.csstudio.display.builder.runtime; import java.util.List; import java.util.stream.Collectors; @@ -27,7 +27,7 @@ * @author Kay Kasemir */ @SuppressWarnings("nls") -public class ContextMenuPvToClipboard implements ContextMenuEntry +public class ContextMenuCopyPvToClipboard implements ContextMenuEntry { private static final Class supportedTypes = ProcessVariable.class; diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvToClipboardWithDescription.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvToClipboardWithDescription.java new file mode 100644 index 0000000000..f97b632524 --- /dev/null +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopyPvToClipboardWithDescription.java @@ -0,0 +1,72 @@ +package org.csstudio.display.builder.runtime; + +import javafx.scene.image.Image; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.phoebus.core.types.ProcessVariable; +import org.phoebus.framework.selection.Selection; +import org.phoebus.ui.docking.DockItem; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.spi.ContextMenuEntry; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +public class ContextMenuCopyPvToClipboardWithDescription implements ContextMenuEntry { + @Override + public String getName() { + return Messages.CopyPVNameToClipboardWithDescription; + } + + private Image icon = ImageCache.getImage(ImageCache.class, "/icons/copy.png"); + @Override + public Image getIcon() { + return icon; + } + + @Override + public Class getSupportedType() { + return ProcessVariable.class; + } + + @Override + public void call(final Selection selection) + { + List pvs = selection.getSelections(); + String pvNamesToAppendToClipboard = pvs.stream().map(ProcessVariable::getName).collect(Collectors.joining(System.lineSeparator())); + + String defaultDescription; + { + var activeDockPane = DockPane.getActiveDockPane(); + var activeDockItem = (DockItem) activeDockPane.getSelectionModel().getSelectedItem(); + if (activeDockItem.getApplication() instanceof DisplayRuntimeInstance) { + DisplayRuntimeInstance displayRuntimeInstance = (DisplayRuntimeInstance) activeDockItem.getApplication(); + defaultDescription = displayRuntimeInstance.getDisplayName(); + } + else { + defaultDescription = ""; + } + } + + BiConsumer copyPVAndDescriptionToClipboardContinuation = (pvName, description) -> { + String newContentInClipboard; + { + newContentInClipboard = pvName + "," + description; + } + ClipboardContent newContent = new ClipboardContent(); + newContent.putString(newContentInClipboard); + Clipboard clipboard = Clipboard.getSystemClipboard(); + clipboard.setContent(newContent); + }; + + String pvName = pvNamesToAppendToClipboard; + + ContextMenuAppendPvToClipboardWithDescription.addDescriptionToPvNameModalDialog(pvName, + defaultDescription, + "Copy", + copyPVAndDescriptionToClipboardContinuation); + } +} diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopySubMenu.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopySubMenu.java new file mode 100644 index 0000000000..762365823b --- /dev/null +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/ContextMenuCopySubMenu.java @@ -0,0 +1,36 @@ +package org.csstudio.display.builder.runtime; + +import javafx.scene.image.Image; +import org.phoebus.core.types.ProcessVariable; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.spi.ContextMenuEntry; + +import java.util.Arrays; +import java.util.List; + +public class ContextMenuCopySubMenu implements ContextMenuEntry { + @Override + public String getName() { + return Messages.CopySubMenu; + } + + private Image icon = ImageCache.getImage(ImageCache.class, "/icons/copy.png"); + @Override + public Image getIcon() { + return icon; + } + + @Override + public Class getSupportedType() { + return ProcessVariable.class; + } + + @Override + public List getChildren() { + return Arrays.asList(new ContextMenuCopyPvToClipboard(), + new ContextMenuCopyPvToClipboardWithDescription(), + new ContextMenuAppendPvToClipboard(), + new ContextMenuAppendPvToClipboardWithDescription(), + new ContextMenuCopyPvAndValueToClipboard()); + } +} diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/Messages.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/Messages.java index ef703a11c0..a8bbd07b69 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/Messages.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/Messages.java @@ -14,7 +14,13 @@ public class Messages { // Keep in alphabetical order and aligned with messages.properties /** Localized message */ - public static String NavigateBack_TT, + public static String AppendPVNameToClipboard, + AppendPVNameToClipboardWithDescription, + Copy, + CopyPVNameToClipboardWithDescription, + CopySubMenu, + CopyWithValue, + NavigateBack_TT, NavigateForward_TT, OpenDataBrowser, OpenInEditor, diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java index ffeeb6c07a..d458cd19a1 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java @@ -271,6 +271,10 @@ DisplayInfo getDisplayInfo() return display_info.orElse(null); } + public String getDisplayName() { + return active_model.getDisplayName(); + } + /** Load display file, represent it, start runtime * @param info Display file to load and represent */ diff --git a/app/display/runtime/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry b/app/display/runtime/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry index 079574586b..5ce7ca4304 100644 --- a/app/display/runtime/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry +++ b/app/display/runtime/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry @@ -1 +1,2 @@ -org.csstudio.display.builder.runtime.app.ProbeDisplayContextMenuEntry \ No newline at end of file +org.csstudio.display.builder.runtime.ContextMenuCopySubMenu +org.csstudio.display.builder.runtime.app.ProbeDisplayContextMenuEntry diff --git a/app/display/runtime/src/main/resources/org/csstudio/display/builder/runtime/messages.properties b/app/display/runtime/src/main/resources/org/csstudio/display/builder/runtime/messages.properties index 36a3330eb0..aeb53236af 100644 --- a/app/display/runtime/src/main/resources/org/csstudio/display/builder/runtime/messages.properties +++ b/app/display/runtime/src/main/resources/org/csstudio/display/builder/runtime/messages.properties @@ -1,3 +1,9 @@ +AppendPVNameToClipboard=Append PV Name to Clipboard +AppendPVNameToClipboardWithDescription=Append PV Name to Clipboard with Description... +Copy=Copy PV Name to Clipboard +CopyPVNameToClipboardWithDescription=Copy PV Name to Clipboard with Description... +CopySubMenu=Copy PV Name... +CopyWithValue=Copy PV Name to Clipboard with Value NavigateBack_TT=Open previous display NavigateForward_TT=Open next display OpenDataBrowser=Open Data Browser diff --git a/app/probe/src/main/java/org/phoebus/applications/probe/Messages.java b/app/probe/src/main/java/org/phoebus/applications/probe/Messages.java index b546b5ef1e..85609d0d23 100644 --- a/app/probe/src/main/java/org/phoebus/applications/probe/Messages.java +++ b/app/probe/src/main/java/org/phoebus/applications/probe/Messages.java @@ -21,8 +21,6 @@ public class Messages public static String Alarm; public static String Alarms; public static String ControlRange; - public static String Copy; - public static String CopyWithValue; public static String DisplayRange; public static String EnumLbls; public static String Format; diff --git a/app/probe/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry b/app/probe/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry index 2e9c2823ad..bc9b517f6e 100644 --- a/app/probe/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry +++ b/app/probe/src/main/resources/META-INF/services/org.phoebus.ui.spi.ContextMenuEntry @@ -1,3 +1 @@ -org.phoebus.applications.probe.ContextMenuPvToClipboard -org.phoebus.applications.probe.ContextMenuPvAndValueToClipboard org.phoebus.applications.probe.ContextLaunchProbe diff --git a/app/probe/src/main/resources/org/phoebus/applications/probe/messages.properties b/app/probe/src/main/resources/org/phoebus/applications/probe/messages.properties index d304742ce3..1042f97662 100644 --- a/app/probe/src/main/resources/org/phoebus/applications/probe/messages.properties +++ b/app/probe/src/main/resources/org/phoebus/applications/probe/messages.properties @@ -1,8 +1,6 @@ Alarm=Alarm: Alarms=Alarms: ControlRange=Control Range: -Copy=Copy PV to Clipboard -CopyWithValue=Copy PV to Clipboard with Value DisplayRange=Display Range: EnumLbls=Enumeration Labels: Format=Format: diff --git a/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuHelper.java b/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuHelper.java index 58e4baebb3..e8a8a39575 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuHelper.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/ContextMenuHelper.java @@ -11,23 +11,18 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.logging.Level; +import javafx.scene.control.Menu; import org.phoebus.framework.adapter.AdapterService; -import org.phoebus.framework.selection.Selection; import org.phoebus.framework.selection.SelectionService; import org.phoebus.framework.selection.SelectionUtil; -import org.phoebus.ui.docking.DockStage; import org.phoebus.ui.spi.ContextMenuEntry; -import javafx.scene.Node; import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.stage.Stage; -import javafx.stage.Window; /** Helper for selection-based additions to context menu * @author Kay Kasemir @@ -42,43 +37,64 @@ public class ContextMenuHelper * to add entries based on the current selection. * * @param setFocus Sets the correct focus (typically on a DockPane or Window) before running the action associated with a menu entry - * @param menu Menu where selection-based entries will be added + * @param contextMenu Menu where selection-based entries will be added * @return true if a supported entry was added. */ - public static boolean addSupportedEntries(Runnable setFocus, final ContextMenu menu) + public static boolean addSupportedEntries(Runnable setFocus, final ContextMenu contextMenu) { final List entries = ContextMenuService.getInstance().listSupportedContextMenuEntries(); if (entries.isEmpty()) return false; + Menu menu = new Menu(); + Runnable hideContextMenu = () -> contextMenu.hide(); + addEntriesToMenu(entries, menu, hideContextMenu, setFocus); + menu.getItems().forEach(item -> contextMenu.getItems().add(item)); + + return true; + } + + private static void addEntriesToMenu(List entries, + Menu menu, + Runnable hideContextMenu, + Runnable setFocus) { for (ContextMenuEntry entry : entries) { - final MenuItem item = new MenuItem(entry.getName()); + final MenuItem item; + if (entry.getChildren().isEmpty()) { + item = new MenuItem(); + + item.setOnAction(e -> + { + setFocus.run(); + try + { + List selection = new ArrayList<>(); + SelectionService.getInstance().getSelection() + .getSelections().stream().forEach(s -> { + AdapterService.adapt(s, entry.getSupportedType()) + .ifPresent(found -> selection.add(found)); + }); + entry.call(SelectionUtil.createSelection(selection)); + hideContextMenu.run(); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot invoke context menu", ex); + } + }); + } + else { + Menu subMenu = new Menu(); + addEntriesToMenu(entry.getChildren(), subMenu, hideContextMenu, setFocus); + item = subMenu; + } + item.setText(entry.getName()); final Image icon = entry.getIcon(); if (icon != null) item.setGraphic(new ImageView(icon)); - item.setOnAction(e -> - { - setFocus.run(); - try - { - List selection = new ArrayList<>(); - SelectionService.getInstance().getSelection() - .getSelections().stream().forEach(s -> { - AdapterService.adapt(s, entry.getSupportedType()) - .ifPresent(found -> selection.add(found)); - }); - entry.call(SelectionUtil.createSelection(selection)); - } - catch (Exception ex) - { - logger.log(Level.WARNING, "Cannot invoke context menu", ex); - } - }); menu.getItems().add(item); } - - return true; } } diff --git a/core/ui/src/main/java/org/phoebus/ui/spi/ContextMenuEntry.java b/core/ui/src/main/java/org/phoebus/ui/spi/ContextMenuEntry.java index 4fc0a0ef79..5e625230d0 100644 --- a/core/ui/src/main/java/org/phoebus/ui/spi/ContextMenuEntry.java +++ b/core/ui/src/main/java/org/phoebus/ui/spi/ContextMenuEntry.java @@ -1,10 +1,12 @@ package org.phoebus.ui.spi; +import javafx.scene.Node; +import javafx.scene.image.Image; import org.phoebus.framework.selection.Selection; import org.phoebus.ui.application.ContextMenuHelper; -import javafx.scene.Node; -import javafx.scene.image.Image; +import java.util.Collections; +import java.util.List; /** * Context menu entry service interface @@ -100,5 +102,11 @@ public default void call(Node parent, Selection selection) throws Exception throw new UnsupportedOperationException(getName() + ".call(node, selection) Is not implemented."); }; + /** + * @return Sub-menu items + */ + public default List getChildren() { + return Collections.emptyList(); + } }