diff --git a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java index 5bc5651327..d356b4b10a 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java @@ -12,6 +12,7 @@ public class Messages { // Keep in alphabetical order and aligned with messages.properties + public static String AddLayout; public static String AllFiles; public static String AlwaysShowTabs; public static String Applications; 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 5c5ff9d6ca..4f7fa6fb92 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 @@ -2,17 +2,10 @@ import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -176,6 +169,11 @@ public class PhoebusApplication extends Application { */ private final Menu load_layout = new Menu(Messages.LoadLayout, ImageCache.getImageView(ImageCache.class, "/icons/layouts.png")); + /** + * Menu to add a layout to the current layout + */ + private final Menu add_layout = new Menu(Messages.AddLayout, ImageCache.getImageView(ImageCache.class, "/icons/layouts.png")); + /** * List of memento names * @@ -540,6 +538,7 @@ private MenuBar createMenu(final Stage stage) { new SeparatorMenuItem(), save_layout, load_layout, + add_layout, delete_layouts, new SeparatorMenuItem(), /* Full Screen placeholder */ @@ -592,6 +591,7 @@ void createLoadLayoutsMenu() { final List menuItemList = new ArrayList<>(); final List toolbarMenuItemList = new ArrayList<>(); + final List addLayoutMenuItemList = new ArrayList<>(); final Map layoutFiles = new HashMap(); @@ -646,6 +646,12 @@ void createLoadLayoutsMenu() { toolbarMenuItem.setMnemonicParsing(false); toolbarMenuItem.setOnAction(event -> startLayoutReplacement(file)); toolbarMenuItemList.add(toolbarMenuItem); + + // Create menu for adding a layout: + final MenuItem addLayoutMenuItem = new MenuItem(filename); + addLayoutMenuItem.setMnemonicParsing(false); + addLayoutMenuItem.setOnAction(event -> startAddingLayout(file)); + addLayoutMenuItemList.add(addLayoutMenuItem); } }); } @@ -654,6 +660,7 @@ void createLoadLayoutsMenu() { Platform.runLater(() -> { load_layout.getItems().setAll(menuItemList); + add_layout.getItems().setAll(addLayoutMenuItemList); layout_menu_button.getItems().setAll(toolbarMenuItemList); delete_layouts.setDisable(memento_files.isEmpty()); }); @@ -1011,6 +1018,28 @@ private void startLayoutReplacement(final File memento_file) { }); } + /** + * Initiate adding a layout to the current layout + * + * @param mementoFile Memento for the desired layout + */ + private void startAddingLayout(File mementoFile) { + JobManager.schedule(mementoFile.getName(), monitor -> + { + MementoTree mementoTree; + try { + mementoTree = loadMemento(mementoFile); + } catch (FileNotFoundException fileNotFoundException) { + logger.log(Level.SEVERE, "Unable to add a layout to the existing layout due to an error when opening the file '" + mementoFile.getAbsolutePath() + "'."); + return; + } catch (Exception exception) { + logger.log(Level.SEVERE, "Unable to add a layout to the existing layout due to an error when parsing the file '" + mementoFile.getAbsolutePath() + "'."); + return; + } + Platform.runLater(() -> addLayoutToCurrentLayout(mementoTree)); + }); + } + /** * @param memento Memento for new layout that should replace current one */ @@ -1096,6 +1125,41 @@ private MementoTree loadMemento(final File memfile) throws Exception { return XMLMementoTree.read(new FileInputStream(memfile)); } + /** + * Adds a layout from a MementoTree to the current layout. + */ + private void addLayoutToCurrentLayout(MementoTree mementoTree) { + + List restoreSelectedTabFunctions = new LinkedList<>(); + for (Stage stage : DockStage.getDockStages()) { + for (DockPane pane : DockStage.getDockPanes(stage)) { + DockItem tab = (DockItem) pane.getSelectionModel().getSelectedItem(); + restoreSelectedTabFunctions.add(() -> tab.select()); + } + } + + List focusNewlyCreatedStageFunctions = new LinkedList<>(); + for (MementoTree childMementoTree : mementoTree.getChildren()) { + Stage stage = new Stage(); + DockStage.configureStage(stage); + MementoHelper.restoreStage(childMementoTree, stage); + + DockStage.deferUntilAllPanesOfStageHaveScenes(stage, () -> + { + long numberOfRestoredTabsInStage = DockStage.getDockPanes(stage).stream() + .flatMap(pane -> pane.getTabs().stream()) + .count(); + if (numberOfRestoredTabsInStage > 0) { + focusNewlyCreatedStageFunctions.add(() -> stage.requestFocus()); + } else { + stage.close(); + } + restoreSelectedTabFunctions.forEach(f -> f.run()); + focusNewlyCreatedStageFunctions.forEach(f -> f.run()); + }); + } + } + /** * Restore stages from memento * 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 96f73196e6..e08435de32 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 @@ -9,15 +9,15 @@ import java.lang.ref.WeakReference; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.application.Messages; import org.phoebus.ui.dialog.DialogHelper; @@ -394,38 +394,58 @@ private StackPane findTabHeader() return null; } - /** Somewhat hacky: - * Need the scene of this dock pane to adjust the style sheet + private Deque> functionsDeferredUntilInScene = new LinkedList<>(); + private boolean changeListenerAdded = false; + /** Need the scene of this dock pane to adjust the style sheet * or to interact with the Window. * * We _have_ added this DockPane to a scene graph, so getScene() should * return the scene. * But if this dock pane is nested inside a newly created {@link SplitDock}, * it will not have a scene until it is rendered. - * So keep deferring to the next UI pulse until there is a scene. - * @param user_of_scene Something that needs to run once there is a scene + * + * If calls to deferUntilInScene() are not nested (i.e., there is no + * call of the form deferUntilInScene(f) where f() in turn contains further + * calls of the form deferUntilInScene(g) for some g), then the relative + * ordering in time of deferred function calls is preserved: if f1() is + * deferred before f2() is deferred, then f1() will be invoked before f2() + * is invoked. + * + * If, on the other hand, there *is* a call of the form deferUntilInScene(f) + * where f() in turn contains a nested call of the form deferUntilInScene(g), + * then the invocation of g() that is deferred by the call deferUntilInScene(g) + * will occur as part of the (possibly deferred) invocation of f(). I.e., it + * will *not* be deferred until after all other deferred function invocations + * have completed, but will be invoked as part of the (possibly deferred) + * invocation of f(). + * + * @param function Something that needs to run once there is a scene */ - public void deferUntilInScene(final Consumer user_of_scene) - { - // Tried to optimize this based on - // sceneProperty().addListener(...), - // creating list of registered users_of_scene, - // invoking once the scene property changes to != null, - // then deleting the list and removing the listener, - // but that added quite some code and failed for - // strange endless-loop type reasons. - deferUntilInScene(0, user_of_scene); - } - - // See deferUntilInScene, giving up after 10 attempts - private void deferUntilInScene(final int level, final Consumer user_of_scene) - { - if (getScene() != null) - user_of_scene.accept(getScene()); - else if (level < 10) - Platform.runLater(() -> deferUntilInScene(level+1, user_of_scene)); - else - logger.log(Level.WARNING, this + " has no scene for deferred call to " + user_of_scene); + public void deferUntilInScene(Consumer function) { + Scene scene = sceneProperty().get(); + if (scene != null) { + function.accept(scene); + } + else { + functionsDeferredUntilInScene.addLast(function); + + if (!changeListenerAdded) { + ChangeListener changeListener = new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Object oldValue, Object newValue) { + if (newValue != null) { + while(!functionsDeferredUntilInScene.isEmpty()) { + Consumer f = functionsDeferredUntilInScene.removeFirst(); + f.accept((Scene) newValue); + } + sceneProperty().removeListener(this); + } + } + }; + sceneProperty().addListener(changeListener); + changeListenerAdded = true; + } + } } /** Hide or show tabs @@ -579,7 +599,7 @@ private void handleDrop(final DragEvent event) public SplitDock split(final boolean horizontally) { final SplitDock split; - if (dock_parent instanceof BorderPane) + if (dock_parent instanceof BorderPane) { final BorderPane parent = (BorderPane) dock_parent; // Remove this dock pane from BorderPane diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java index b3071a118c..b4a1efb195 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java @@ -417,4 +417,18 @@ else if (node instanceof SplitDock) else buf.append(node).append("\n"); } + + private static void deferUntilAllPanesOfStageHaveScenes(Runnable runnable, List remainingPanes) { + // This is a helper function for implementing deferUntilAllPanesOfStageHaveScenes(Stage stage, Runnable runnable). + if (remainingPanes.size() == 0) { + runnable.run(); + } else { + var pane = remainingPanes.get(0); + pane.deferUntilInScene(scene -> deferUntilAllPanesOfStageHaveScenes(runnable, remainingPanes.subList(1, remainingPanes.size()))); + } + } + + public static void deferUntilAllPanesOfStageHaveScenes(Stage stage, Runnable runnable) { + deferUntilAllPanesOfStageHaveScenes(runnable, getDockPanes(stage)); + } } diff --git a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties index 6cb4f2feac..5bc1052edb 100644 --- a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties +++ b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties @@ -1,3 +1,4 @@ +AddLayout=Add Layout AllFiles=All Files AlwaysShowTabs=Always Show Tabs Applications=Applications