Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSSTUDIO-1761 (Part 1/2) Add functionality to the menu of the main window for adding a layout to the current layout. #2550

Merged
merged 8 commits into from
Feb 27, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -592,6 +591,7 @@ void createLoadLayoutsMenu() {

final List<MenuItem> menuItemList = new ArrayList<>();
final List<MenuItem> toolbarMenuItemList = new ArrayList<>();
final List<MenuItem> addLayoutMenuItemList = new ArrayList<>();

final Map<String, File> layoutFiles = new HashMap<String, File>();

Expand Down Expand Up @@ -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);
}
});
}
Expand All @@ -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());
});
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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<Runnable> 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<Runnable> 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
*
Expand Down
65 changes: 36 additions & 29 deletions core/ui/src/main/java/org/phoebus/ui/docking/DockPane.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -394,38 +394,45 @@ private StackPane findTabHeader()
return null;
}

/** Somewhat hacky:
* Need the scene of this dock pane to adjust the style sheet
private Deque<Consumer<Scene>> 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
* The relative ordering in time of deferred function calls is preserved: if
* f1() is deferred before f2() is deferred, then f1() will be called before
* f2() is called.
* @param function Something that needs to run once there is a scene
*/
public void deferUntilInScene(final Consumer<Scene> 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<Scene> 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<Scene> 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<Scene> f = functionsDeferredUntilInScene.removeFirst();
f.accept((Scene) newValue);
}
sceneProperty().removeListener(this);
}
}
};
sceneProperty().addListener(changeListener);
changeListenerAdded = true;
}
}
}

/** Hide or show tabs
Expand Down Expand Up @@ -579,7 +586,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
Expand Down
14 changes: 14 additions & 0 deletions core/ui/src/main/java/org/phoebus/ui/docking/DockStage.java
Original file line number Diff line number Diff line change
Expand Up @@ -417,4 +417,18 @@ else if (node instanceof SplitDock)
else
buf.append(node).append("\n");
}

private static void deferUntilAllPanesOfStageHaveScenes(Runnable runnable, List<DockPane> 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));
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
AddLayout=Add Layout
AllFiles=All Files
AlwaysShowTabs=Always Show Tabs
Applications=Applications
Expand Down