diff --git a/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java b/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java index c9333d91d9..c361f33be7 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/PhoebusApplication.java @@ -53,6 +53,9 @@ import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.input.DragEvent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.TransferMode; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; @@ -479,6 +482,29 @@ public ToolBar getToolbar() { return toolbar; } + /** + * Event handler for when a file is being dragged into the application to open it. + */ + private void onDragOverFile(DragEvent e) { + final Dragboard db = e.getDragboard(); + if (db.hasFiles()) // Allow any drag operation containing files + e.acceptTransferModes(TransferMode.MOVE); + + e.consume(); + } + + /** + * See `onDragOverFile` - this is called when the drag item was released. + */ + private void onDragDroppedFile(DragEvent e) { + final Dragboard db = e.getDragboard(); + + this.openDroppedResources(db); + + e.setDropCompleted(true); + e.consume(); + } + private void startUI(final MementoTree memento, final JobMonitor monitor) throws Exception { monitor.beginTask(Messages.MonitorTaskUi, 4); @@ -487,6 +513,11 @@ private void startUI(final MementoTree memento, final JobMonitor monitor) throws toolbar = createToolbar(); createTopResourcesMenu(); + menuBar.setOnDragOver(this::onDragOverFile); + menuBar.setOnDragDropped(this::onDragDroppedFile); + toolbar.setOnDragOver(this::onDragOverFile); + toolbar.setOnDragDropped(this::onDragDroppedFile); + DockStage.configureStage(main_stage); // Patch ID of main window // (in case we ever need to identify the main window) @@ -1153,6 +1184,25 @@ private AppResourceDescriptor findApplication(final URI resource, final boolean return applications.get(options.indexOf(result.get())); } + /** + * Callback to trigger the loading of dragged resources. + * For each file the "Choose editor" dialog is shown. + */ + public void openDroppedResources(final Dragboard db) { + + if (!db.hasFiles()) + return; + + var files = db.getFiles(); // Copy, because the event is going to be gone + + Platform.runLater(() -> { + // Run this delayed, closing the drag event first, such that the icon will disappear from the UI + for (File file : files) { + this.openResource(file.toURI(), true); + } + }); + } + /** * @param resource Resource received as command line argument * @param prompt Prompt if there are multiple applications, or use first one? diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java index 3f4795f84c..924332ccd4 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java @@ -33,6 +33,7 @@ import javafx.scene.control.TabPane; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.application.Messages; +import org.phoebus.ui.application.PhoebusApplication; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.javafx.Styles; @@ -45,6 +46,7 @@ import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.input.Dragboard; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.DragEvent; import javafx.scene.input.KeyCode; @@ -543,9 +545,11 @@ public List getDockItems() /** Accept dock items */ private void handleDragOver(final DragEvent event) { - if (!isFixed() && - DockItem.dragged_item.get() != null) + // For files or for tabs: + if (event.getDragboard().hasFiles() || + !isFixed() && DockItem.dragged_item.get() != null) event.acceptTransferModes(TransferMode.MOVE); + event.consume(); } @@ -568,51 +572,56 @@ private void handleDragExited(final DragEvent event) /** Accept a dropped tab */ private void handleDrop(final DragEvent event) { - if (!event.getDragboard().hasContent(DockItem.DOCK_ITEM)){ - return; - } - final DockItem item = DockItem.dragged_item.getAndSet(null); - if (item == null) - logger.log(Level.SEVERE, "Empty drop, " + event); - else - { - logger.log(Level.INFO, "Somebody dropped " + item + " into " + this); - final TabPane old_parent = item.getTabPane(); - - // Unexpected, but would still "work" at this time - if (! (old_parent instanceof DockPane)) - logger.log(Level.SEVERE, "DockItem is not in DockPane but " + old_parent); - - // When moving to a new scene, - // assert that styles used in old scene are still available - final Scene old_scene = old_parent.getScene(); - final Scene scene = getScene(); - if (scene != old_scene) - for (String css : old_scene.getStylesheets()) - Styles.set(scene, css); - - // Move tab. In principle, - // (1) first remove from old parent, - // (2) then add to new parent. - // But modifying tabs triggers tab listener, which registers SplitPane.merge() - // in Platform.runLater(). The merge could re-arrange tab panes, - // we when we later want to add the tab, we'll face a different scene graph. - // Issue the tab addition (2) with runlater right now so it'll happen before any - // split pane cleanup. - Platform.runLater(() -> - { - // When adding the tab to its new parent (this dock) right away, - // the tab would sometimes not properly render until the pane is resized. - // Moving to the next UI tick helps - logger.log(Level.INFO, "Adding " + item + " to " + this); - addTab(item); - Platform.runLater(this::autoHideTabs); - }); + final Dragboard db = event.getDragboard(); + if (db.hasFiles()) { + PhoebusApplication.INSTANCE.openDroppedResources(db); + + } else if (db.hasContent(DockItem.DOCK_ITEM)) { + final DockItem item = DockItem.dragged_item.getAndSet(null); + if (item == null) + logger.log(Level.SEVERE, "Empty drop, " + event); + else { + logger.log(Level.INFO, "Somebody dropped " + item + " into " + this); + final TabPane old_parent = item.getTabPane(); + + // Unexpected, but would still "work" at this time + if (!(old_parent instanceof DockPane)) + logger.log(Level.SEVERE, "DockItem is not in DockPane but " + old_parent); + + // When moving to a new scene, + // assert that styles used in old scene are still available + final Scene old_scene = old_parent.getScene(); + final Scene scene = getScene(); + if (scene != old_scene) + for (String css : old_scene.getStylesheets()) + Styles.set(scene, css); + + // Move tab. In principle, + // (1) first remove from old parent, + // (2) then add to new parent. + // But modifying tabs triggers tab listener, which registers SplitPane.merge() + // in Platform.runLater(). The merge could re-arrange tab panes, + // we when we later want to add the tab, we'll face a different scene graph. + // Issue the tab addition (2) with runlater right now so it'll happen before any + // split pane cleanup. + Platform.runLater(() -> + { + // When adding the tab to its new parent (this dock) right away, + // the tab would sometimes not properly render until the pane is resized. + // Moving to the next UI tick helps + logger.log(Level.INFO, "Adding " + item + " to " + this); + addTab(item); + Platform.runLater(this::autoHideTabs); + }); - // With tab addition already in the UI thread queue, remove item from old tab - logger.log(Level.INFO, "Removing " + item + " from " + old_parent); - old_parent.getTabs().remove(item); + // With tab addition already in the UI thread queue, remove item from old tab + logger.log(Level.INFO, "Removing " + item + " from " + old_parent); + old_parent.getTabs().remove(item); + } + } else { + return; // No idea how to handle this dragged item } + event.setDropCompleted(true); event.consume(); }