From 7d504806282f5f863f57d29c698ac831015d8cae Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Tue, 16 Jan 2024 10:45:45 +0100 Subject: [PATCH 001/132] CSSTUDIO-1989 Add support for navigator to Phoebus. --- .../databrowser3/DataBrowserInstance.java | 2 +- .../ui/application/PhoebusApplication.java | 4 ++ .../java/org/phoebus/ui/docking/DockItem.java | 4 ++ .../java/org/phoebus/ui/docking/DockPane.java | 43 ++++++++++++------- .../org/phoebus/ui/docking/DockStage.java | 35 ++++++++++----- .../org/phoebus/ui/docking/SplitDock.java | 11 +++++ 6 files changed, 71 insertions(+), 28 deletions(-) diff --git a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java index 8fb3365b46..27b014db3c 100644 --- a/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java +++ b/app/databrowser/src/main/java/org/csstudio/trends/databrowser3/DataBrowserInstance.java @@ -178,7 +178,7 @@ public void raise() dock_item.select(); } - void loadResource(final URI input) + public void loadResource(final URI input) { // Set input ASAP so that other requests to open this // resource will find this instance and not start 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 03b5d8ecec..3a42fdad85 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 @@ -334,6 +334,10 @@ private void backgroundStartup(final JobMonitor monitor, final Splash splash) th }); } + public ToolBar getToolbar() { + return toolbar; + } + private void startUI(final MementoTree memento, final JobMonitor monitor) throws Exception { monitor.beginTask(Messages.MonitorTaskUi, 4); diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java index e44a4ee891..35302a6950 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItem.java @@ -358,6 +358,10 @@ public INST getApplication() return (INST) getProperties().get(KEY_APPLICATION); } + public void setKeyApplication(INST applicationInstance) { + getProperties().put(KEY_APPLICATION, applicationInstance); + } + /** Label of this item */ public String getLabel() { 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 b6926e4d8b..92fc85ca56 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 @@ -22,6 +22,7 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.scene.control.*; import javafx.stage.Window; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.application.Messages; @@ -36,13 +37,6 @@ import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.ButtonType; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.ContextMenuEvent; @@ -192,7 +186,7 @@ public static void alwaysShowTabs(final boolean do_show_single_tabs) * @param tabs */ // Only accessible within this package (DockStage) - DockPane(final DockItem... tabs) + public DockPane(final DockItem... tabs) { super(tabs); @@ -295,11 +289,11 @@ private void showContextMenu(final ContextMenuEvent event) } /** @param dock_parent {@link BorderPane}, {@link SplitDock} or null */ - void setDockParent(final Parent dock_parent) + public void setDockParent(final Parent dock_parent) { if (dock_parent == null || dock_parent instanceof BorderPane || - dock_parent instanceof SplitDock) + dock_parent instanceof SplitPane) this.dock_parent = dock_parent; else throw new IllegalArgumentException("Expect BorderPane or SplitDock, got " + dock_parent); @@ -625,7 +619,22 @@ private void handleDrop(final DragEvent event) public SplitDock split(final boolean horizontally) { final SplitDock split; - if (dock_parent instanceof BorderPane) + + if (dock_parent instanceof SplitDock) + { + final SplitDock parent = (SplitDock) dock_parent; + // Remove this dock pane from parent + final boolean first = parent.removeItem(this); + // Place in split alongside a new dock pane + final DockPane new_pane = new DockPane(); + dockPaneEmptyListeners.stream().forEach(new_pane::addDockPaneEmptyListener); + split = new SplitDock(parent, horizontally, this, new_pane); + setDockParent(split); + new_pane.setDockParent(split); + // Place that new split in the border pane + parent.addItem(first, split); + } + else if (dock_parent instanceof BorderPane) { final BorderPane parent = (BorderPane) dock_parent; // Remove this dock pane from BorderPane @@ -639,11 +648,12 @@ public SplitDock split(final boolean horizontally) // Place that new split in the border pane parent.setCenter(split); } - else if (dock_parent instanceof SplitDock) + else if (dock_parent instanceof SplitPane) { - final SplitDock parent = (SplitDock) dock_parent; - // Remove this dock pane from parent - final boolean first = parent.removeItem(this); + final SplitPane parent = (SplitPane) dock_parent; + // Remove this dock pane from BorderPane + double dividerPosition = parent.getDividerPositions()[0]; + parent.getItems().remove(this); // Place in split alongside a new dock pane final DockPane new_pane = new DockPane(); dockPaneEmptyListeners.stream().forEach(new_pane::addDockPaneEmptyListener); @@ -651,7 +661,8 @@ else if (dock_parent instanceof SplitDock) setDockParent(split); new_pane.setDockParent(split); // Place that new split in the border pane - parent.addItem(first, split); + parent.getItems().add(split); + parent.setDividerPosition(0, dividerPosition); } else throw new IllegalStateException("Cannot split, dock_parent is " + dock_parent); 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 5aa70fb99e..fb8ee23f95 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 @@ -22,7 +22,8 @@ import java.util.logging.Level; import java.util.stream.Collectors; -import javafx.scene.input.MouseEvent; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Border; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.workbench.Locations; import org.phoebus.ui.application.Messages; @@ -132,15 +133,6 @@ public static DockPane configureStage(final Stage stage, final DockItem... tabs) */ public static DockPane configureStage(final Stage stage, final Geometry geometry, final DockItem... tabs) { - stage.addEventFilter(MouseEvent.MOUSE_MOVED, mouseEvent -> { - // Filtering MOUSE_MOVED events from unfocused windows prevents tooltips - // from displaying in unfocused windows. This, in turn, prevents unfocused - // windows from receiving the focus as a consequence on Windows and Mac OS. - if (!stage.focusedProperty().get()) { - mouseEvent.consume(); - } - }); - stage.getProperties().put(KEY_ID, createID("DockStage")); final DockPane pane = new DockPane(tabs); @@ -166,6 +158,13 @@ else if(layout.getChildren().get(0) instanceof SplitPane){ }); final Scene scene = new Scene(layout, geometry.width, geometry.height); + + scene.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> { + if (keyEvent.isAltDown()) { + keyEvent.consume(); + } + }); + stage.setScene(scene); stage.setTitle(Messages.FixedTitle); // Set position @@ -323,6 +322,13 @@ public static BorderPane getLayout(final Stage stage) final Parent layout = stage.getScene().getRoot(); if (layout instanceof BorderPane) return (BorderPane) layout; + if (layout instanceof SplitPane) { + SplitPane splitPane = (SplitPane) layout; + var maybeBorderPane = splitPane.getItems().stream().filter(item -> item instanceof BorderPane).findFirst(); + if (maybeBorderPane.isPresent()) { + return (BorderPane) maybeBorderPane.get(); + } + } throw new IllegalStateException("Expect BorderPane, got " + layout); } @@ -332,7 +338,14 @@ public static BorderPane getLayout(final Stage stage) */ public static Node getPaneOrSplit(final Stage stage) { - final Node container = getLayout(stage).getCenter(); + Node container = getLayout(stage).getCenter(); + if (container instanceof SplitPane) { + SplitPane splitPane = (SplitPane) container; + var maybeDockPaneOrSplitDock = splitPane.getItems().stream().filter(item -> item instanceof DockPane || item instanceof SplitDock).findFirst(); + if (maybeDockPaneOrSplitDock.isPresent()) { + return maybeDockPaneOrSplitDock.get(); + } + } if (container instanceof DockPane || container instanceof SplitDock) return container; diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java b/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java index 8c7b28f67c..90e102032f 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/SplitDock.java @@ -249,4 +249,15 @@ public String toString() { return "SplitDock for " + getItems(); } + + /** @param dock_parent {@link BorderPane}, {@link SplitDock} or null */ + public void setDockParent(final Parent dock_parent) + { + if (dock_parent == null || + dock_parent instanceof BorderPane || + dock_parent instanceof SplitPane) + this.dock_parent = dock_parent; + else + throw new IllegalArgumentException("Expect BorderPane or SplitDock, got " + dock_parent); + } } From 2349a21a815595844596c5d2ef1fd23e882696ef Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Fri, 26 Jan 2024 08:55:43 +0100 Subject: [PATCH 002/132] CSSTUDIO-1989 Add application 'app-display-navigator'. --- app/display/navigator/build.xml | 10 + app/display/navigator/pom.xml | 64 + .../display/navigator/AddMenuEntry.java | 32 + .../display/navigator/Messages.java | 42 + .../NavigatorAppResourceDescriptor.java | 86 + .../navigator/NavigatorController.java | 1692 +++++++++++++++++ .../display/navigator/NavigatorInstance.java | 165 ++ .../navigator/NavigatorSelectionTreeNode.java | 34 + .../display/navigator/NavigatorTreeNode.java | 224 +++ .../display/navigator/Preferences.java | 17 + ...hoebus.framework.spi.AppResourceDescriptor | 1 + .../services/org.phoebus.ui.spi.MenuEntry | 1 + .../main/resources/icons/closed_folder.png | Bin 0 -> 490 bytes .../src/main/resources/icons/collapse_all.png | Bin 0 -> 151 bytes .../src/main/resources/icons/expand_all.png | Bin 0 -> 154 bytes .../src/main/resources/icons/folder.png | Bin 0 -> 595 bytes .../resources/icons/locate_current_file.png | Bin 0 -> 185 bytes .../src/main/resources/icons/menu.png | Bin 0 -> 121 bytes .../src/main/resources/icons/navigator.png | Bin 0 -> 175 bytes .../navigator_preferences.properties | 14 + .../display/navigator/messages.properties | 29 + .../display/navigator/ui/Navigator.fxml | 51 + .../java/org/phoebus/ui/docking/DockItem.java | 4 - .../java/org/phoebus/ui/docking/DockPane.java | 13 +- .../org/phoebus/ui/docking/DockStage.java | 17 +- .../org/phoebus/ui/docking/SplitDock.java | 11 - phoebus-product/pom.xml | 5 + 27 files changed, 2486 insertions(+), 26 deletions(-) create mode 100644 app/display/navigator/build.xml create mode 100644 app/display/navigator/pom.xml create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/AddMenuEntry.java create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Messages.java create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorAppResourceDescriptor.java create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorController.java create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorInstance.java create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorSelectionTreeNode.java create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorTreeNode.java create mode 100644 app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Preferences.java create mode 100644 app/display/navigator/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppResourceDescriptor create mode 100644 app/display/navigator/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry create mode 100644 app/display/navigator/src/main/resources/icons/closed_folder.png create mode 100644 app/display/navigator/src/main/resources/icons/collapse_all.png create mode 100644 app/display/navigator/src/main/resources/icons/expand_all.png create mode 100644 app/display/navigator/src/main/resources/icons/folder.png create mode 100644 app/display/navigator/src/main/resources/icons/locate_current_file.png create mode 100644 app/display/navigator/src/main/resources/icons/menu.png create mode 100644 app/display/navigator/src/main/resources/icons/navigator.png create mode 100644 app/display/navigator/src/main/resources/navigator_preferences.properties create mode 100644 app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/messages.properties create mode 100644 app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/ui/Navigator.fxml diff --git a/app/display/navigator/build.xml b/app/display/navigator/build.xml new file mode 100644 index 0000000000..47066600e6 --- /dev/null +++ b/app/display/navigator/build.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/display/navigator/pom.xml b/app/display/navigator/pom.xml new file mode 100644 index 0000000000..28218555f1 --- /dev/null +++ b/app/display/navigator/pom.xml @@ -0,0 +1,64 @@ + + + + app-display + org.phoebus + 4.7.4-SNAPSHOT + + 4.0.0 + app-display-navigator + + + org.phoebus + core-framework + 4.7.4-SNAPSHOT + + + org.phoebus + core-util + 4.7.4-SNAPSHOT + + + org.phoebus + core-ui + 4.7.4-SNAPSHOT + + + org.phoebus + app-display-model + 4.7.4-SNAPSHOT + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.phoebus + app-display-representation-javafx + 4.7.4-SNAPSHOT + compile + + + org.phoebus + app-display-editor + 4.7.4-SNAPSHOT + compile + + + org.phoebus + app-display-runtime + 4.7.4-SNAPSHOT + compile + + + org.phoebus + app-filebrowser + 4.7.4-SNAPSHOT + compile + + + diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/AddMenuEntry.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/AddMenuEntry.java new file mode 100644 index 0000000000..81aef0eafe --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/AddMenuEntry.java @@ -0,0 +1,32 @@ +package org.phoebus.applications.display.navigator; + +import org.phoebus.framework.workbench.ApplicationService; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.spi.MenuEntry; + +import javafx.scene.image.Image; + +public class AddMenuEntry implements MenuEntry { + + @Override + public Void call() { + ApplicationService.createInstance("navigator"); + return null; + } + + @Override + public String getName() { + return "Navigator"; + } + + @Override + public String getMenuPath() { + return "Display"; + } + + @Override + public Image getIcon() { + return ImageCache.getImage(getClass(), "/icons/navigator.png"); + } + +} \ No newline at end of file diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Messages.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Messages.java new file mode 100644 index 0000000000..996a97d7f5 --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Messages.java @@ -0,0 +1,42 @@ +package org.phoebus.applications.display.navigator; + +import org.phoebus.framework.nls.NLS; + +public class Messages { + public static String LocateCurrentFile; + public static String NavigatorTooltip; + public static String ExpandAll; + public static String CollapseAll; + public static String NavigatorMenu; + public static String NewNavigatorNamePrompt; + public static String RenameNavigator; + public static String CreateNewFolder; + public static String NewFolderNamePrompt; + public static String GenericDataBrowserName; + public static String UnknownFileExtensionWarning; + public static String FileIsNotInTheNavigatorDataDirectoryWarning; + public static String FileNotFoundWarning; + public static String ErrorLoadingTheNavigatorWarning; + public static String NewNavigatorDefaultName; + public static String OpenInNewTab; + public static String OpenInBackgroundTab; + public static String DeleteItem; + public static String DeletePrompt; + public static String RenameFolderPrompt; + public static String RenameFolder; + public static String UnknownNodeTypeWarning; + public static String CreateNewSubFolder; + public static String ErrorCreatingNewFolderWarning; + public static String CreateNewNavigator; + public static String ErrorRenamingFolderWarning; + public static String RenameParentFolderPrompt; + public static String ErrorCreatingNewNavigatorFileWarning; + public static String TheSpecifiedInitialNavigatorDoesntExist; + + private Messages() { } + + static + { + NLS.initializeMessages(Messages.class); + } +} diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorAppResourceDescriptor.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorAppResourceDescriptor.java new file mode 100644 index 0000000000..7a0a1a80c4 --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorAppResourceDescriptor.java @@ -0,0 +1,86 @@ +package org.phoebus.applications.display.navigator; + +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.layout.Region; +import org.phoebus.framework.spi.AppInstance; +import org.phoebus.framework.spi.AppResourceDescriptor; +import org.phoebus.ui.dialog.DialogHelper; +import org.phoebus.ui.docking.DockPane; + +import java.net.URI; +import java.net.URL; +import java.util.List; + +public class NavigatorAppResourceDescriptor implements AppResourceDescriptor { + + protected static NavigatorInstance instance = null; + @Override + public String getName() { + return "navigator"; + } + + @Override + public String getDisplayName() { + return "Navigator"; + } + + @Override + public URL getIconURL() { + return getClass().getResource("/icons/navigator.png"); + } + + @Override + public void start() { + return; + } + + @Override + public AppInstance create() { + instance = new NavigatorInstance(this); + return instance; + } + + @Override + public void stop() { + if (instance != null && instance.controller.unsavedChanges) { + ButtonType saveAndExit = new ButtonType("Save & Exit"); + ButtonType discardAndExit = new ButtonType("Discard & Exit"); + + Alert prompt = new Alert(Alert.AlertType.CONFIRMATION); + prompt.getDialogPane().getButtonTypes().remove(ButtonType.OK); + prompt.getDialogPane().getButtonTypes().remove(ButtonType.CANCEL); + ((ButtonBar) prompt.getDialogPane().lookup(".button-bar")).setButtonOrder(ButtonBar.BUTTON_ORDER_NONE); // Set the button order manually (since they are non-standard) + prompt.getDialogPane().getButtonTypes().add(saveAndExit); + prompt.getDialogPane().getButtonTypes().add(discardAndExit); + + int prefWidth = 400; + int prefHeight = 160; + prompt.getDialogPane().setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE); + prompt.getDialogPane().setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + prompt.setResizable(false); + DialogHelper.positionDialog(prompt, DockPane.getActiveDockPane(), -prefWidth, -prefHeight); + prompt.initOwner(DockPane.getActiveDockPane().getScene().getWindow()); + prompt.setResizable(true); + prompt.setTitle("Unsaved Changes in a Navigator"); + prompt.setHeaderText("There are unsaved changes in the navigator '" + instance.controller.navigatorName_original + "'. Do you want to save or discard the changes?"); + ButtonType result = prompt.showAndWait().orElse(discardAndExit); + + if (result == saveAndExit) { + instance.controller.saveNavigatorAction(null); + } + } + } + + @Override + public List supportedFileExtentions() { + return List.of("navigator"); + } + + @Override + public AppInstance create(URI resource) { + instance = new NavigatorInstance(this); + return instance; + } +} diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorController.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorController.java new file mode 100644 index 0000000000..4880a3dc91 --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorController.java @@ -0,0 +1,1692 @@ +package org.phoebus.applications.display.navigator; + +import com.google.common.collect.Streams; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.DataFormat; +import javafx.scene.input.Dragboard; +import javafx.scene.input.KeyCode; +import javafx.scene.input.MouseButton; +import javafx.scene.input.TransferMode; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.util.Pair; +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.persist.ModelLoader; +import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.csstudio.trends.databrowser3.DataBrowserInstance; +import org.csstudio.trends.databrowser3.model.Model; +import org.csstudio.trends.databrowser3.persistence.XMLPersistence; +import org.phoebus.framework.spi.AppInstance; +import org.phoebus.framework.util.ResourceParser; +import org.phoebus.ui.docking.DockItem; +import org.phoebus.ui.docking.DockItemWithInput; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.docking.DockStage; +import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.javafx.ToolbarHelper; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.ResourceBundle; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import static org.phoebus.applications.display.navigator.NavigatorInstance.LOGGER; + +public class NavigatorController implements Initializable { + private final int NAVIGATOR_WIDTH_AT_STARTUP = 300; + private static final String NAVIGATOR_SELECTOR_BUTTONS_CSS = "-fx-font-family: 'Source Sans Pro Semibold'; -fx-background-radius: 3; -fx-padding: 0 2 0 2; -fx-alignment: center; -fx-font-size: 15; -fx-font-weight: normal; "; + private static final String NAVIGATOR_SELECTOR_MENU_ITEMS_CSS = "-fx-font-weight: normal; -fx-font-size: 13; "; + private static String NAVIGATOR_ROOT; + protected static String OPI_ROOT; + static private DataFormat navigationTreeDataFormat = new DataFormat("application/navigation-tree-data-format"); + @FXML + public MenuItem enterEditModeMenuItem; + @FXML + Region topBarSpring; + + public NavigatorController() { } + + @Override + public void initialize(URL location, ResourceBundle resources) { + NAVIGATOR_ROOT = Preferences.navigator_root; + OPI_ROOT = Preferences.opi_root; + navigator.setPrefWidth(NAVIGATOR_WIDTH_AT_STARTUP); + + topBar.setBorder(emptyBorder); + HBox.setHgrow(topBarSpring, Priority.ALWAYS); + + VBox.setVgrow(treeView, Priority.ALWAYS); + treeView.setCellFactory(tree_view -> new NavigatorController.NavigationTree_TreeCellClass()); + treeView.showRootProperty().set(false); + treeView.setOnKeyPressed(keyEvent -> { + TreeItem treeItem = treeView.getSelectionModel().getSelectedItem(); + if (treeItem != null) { + var navigatorTreeNode = treeItem.getValue(); + if (navigatorTreeNode != null) { + var nodeType = navigatorTreeNode.getNodeType(); + var keyCode = keyEvent.getCode(); + if (keyCode == KeyCode.ENTER) { + if ( nodeType == NavigatorTreeNode.NodeType.DisplayRuntime + || nodeType == NavigatorTreeNode.NodeType.DataBrowser) { + if (keyEvent.isControlDown()) { + treeItem.getValue().getAction().accept(NavigatorTreeNode.Target.NewTab_InBackground); + } + else { + treeItem.getValue().getAction().accept(NavigatorTreeNode.Target.CurrentTab); + } + keyEvent.consume(); + } + else if (nodeType == NavigatorTreeNode.NodeType.VirtualFolder) { + treeItem.setExpanded(!treeItem.expandedProperty().get()); + treeView.refresh(); + keyEvent.consume(); + } + } + } + } + }); + + { + var locateCurrentFile = ImageCache.getImageView(NavigatorInstance.class, "/icons/locate_current_file.png"); + locateCurrentFileButton.setGraphic(locateCurrentFile); + Tooltip.install(locateCurrentFileButton, new Tooltip(Messages.LocateCurrentFile)); + } + + { + var expandAllIcon = ImageCache.getImageView(NavigatorInstance.class, "/icons/expand_all.png"); + expandAllButton.setGraphic(expandAllIcon); + Tooltip.install(expandAllButton, new Tooltip(Messages.ExpandAll)); + } + + { + var collapseAllIcon = ImageCache.getImageView(NavigatorInstance.class, "/icons/collapse_all.png"); + collapseAllButton.setGraphic(collapseAllIcon); + Tooltip.install(collapseAllButton, new Tooltip(Messages.CollapseAll)); + } + + { + var menuIcon = ImageCache.getImageView(NavigatorInstance.class, "/icons/menu.png"); + navigatorMenuButton.setGraphic(menuIcon); + Tooltip.install(navigatorMenuButton, new Tooltip(Messages.NavigatorMenu)); + } + + try { + rebuildNavigatorSelector(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + navigatorLabel.textProperty().addListener((property, oldValue, newValue) -> { + navigatorLabel.setTooltip(new Tooltip(newValue)); + }); + + String initialNavigatorRelativePath = Preferences.initial_navigator; + + if (initialNavigatorRelativePath.isEmpty()) { + loadTopMostNavigator(); + } + else { + String initialNavigatorAbsolutePath = NAVIGATOR_ROOT + initialNavigatorRelativePath; + File initialNavigatorFile = new File(initialNavigatorAbsolutePath); + if (initialNavigatorFile.exists()) { + loadNavigator(initialNavigatorFile); + } + else { + LOGGER.log(Level.WARNING, "The specified initial navigator doesn't exist."); + displayWarning(Messages.TheSpecifiedInitialNavigatorDoesntExist + " " + initialNavigatorAbsolutePath); + loadTopMostNavigator(); + } + } + } + + private void loadTopMostNavigator() { + var topMostNavigator = getTopMostItem(navigatorSelectionTree); + if (topMostNavigator != null) { + topMostNavigator.getValue().getAction().run(); + } + } + + @FXML + VBox navigator; + @FXML + HBox topBar; + @FXML + Label navigatorLabel; + @FXML + MenuItem leaveEditModeMenuItem; + @FXML + MenuItem saveChangesMenuItem; + @FXML + MenuItem discardChangesMenuItem; + + @FXML + public Button locateCurrentFileButton; + @FXML + Button expandAllButton; + @FXML + Button collapseAllButton; + @FXML + MenuButton navigatorMenuButton; + @FXML + public TreeView treeView; + @FXML + VBox userInputVBox; + + @FXML + void enterEditModeAction(ActionEvent actionEvent) { + editModeEnabled = true; + topBar.setStyle("-fx-alignment: center; -fx-background-color: fuchsia; "); + enableDragNDropToTopBar(); + leaveEditModeMenuItem.setDisable(true); + saveChangesMenuItem.setDisable(true); + discardChangesMenuItem.setDisable(true); + setUnsavedChanges(false); + enterEditModeMenuItem.setDisable(true); + reloadNavigator(); + } + + @FXML + protected void saveNavigatorAction(ActionEvent actionEvent) { + if (currentlySelectedNavigator != null) { + if (renameNavigator_thunk != null) { + boolean success = renameNavigator_thunk.get(); // renameNavigator_thunk() handles the saving of the navigator also. + if (success) { + setUnsavedChanges(false); + } + else { + LOGGER.log(Level.SEVERE, "An error occurred during the save operation of the navigator."); + } + } + else { + writeNavigatorToXML(treeView.getRoot(), currentlySelectedNavigator); + setUnsavedChanges(false); + reloadNavigator(); + } + } + } + + @FXML + void leaveEditModeAction(ActionEvent actionEvent) { + editModeEnabled = false; + topBar.setStyle("-fx-alignment: center; -fx-background-color: #483d8b; "); + disableDragNDropToTopBar(); + leaveEditModeMenuItem.setDisable(false); + saveChangesMenuItem.setDisable(true); + discardChangesMenuItem.setDisable(true); + reloadNavigator(); + enterEditModeMenuItem.setDisable(false); + } + + void renameNavigatorAction(ActionEvent actionEvent) { + Consumer renameNavigator = newNavigatorName -> { + var directoryOfCurrentlySelectedNavigator = currentlySelectedNavigator.getParent(); + var newFile = new File(directoryOfCurrentlySelectedNavigator, newNavigatorName + ".navigator"); + boolean newFileExists = newFile.exists(); + boolean newNameEqualsOriginalName = navigatorName_original.equals(newNavigatorName); + if (!newFileExists && !newNameEqualsOriginalName) { + navigatorName_displayed = newNavigatorName; + navigatorLabel.setText(newNavigatorName); + setUnsavedChanges(true); + + renameNavigator_thunk = () -> { + boolean navigatorWasRenamed = false; + if (!newFile.exists()) { + navigatorWasRenamed = currentlySelectedNavigator.renameTo(newFile); + if (navigatorWasRenamed) { + currentlySelectedNavigator = newFile; + writeNavigatorToXML(treeView.getRoot(), currentlySelectedNavigator); + try { + rebuildNavigatorSelector(); + } catch (Exception e) { + throw new RuntimeException(e); + } + reloadNavigator(); + renameNavigator_thunk = null; + } + else { + displayWarning("Error: renaming of file was unsuccessful!"); + LOGGER.log(Level.WARNING, "Error: renaming of file was unsuccessful!"); + } + } + else { + displayWarning("Error: there already exists a navigator with that name!"); + } + return navigatorWasRenamed; + }; + } + else if (newNameEqualsOriginalName) { + navigatorName_displayed = newNavigatorName; + navigatorLabel.setText(newNavigatorName); + renameNavigator_thunk = null; + setUnsavedChanges(unsavedChanges); + } + else { + // File exists + displayWarning("Error renaming the navigator: a file with the chosen filename exists already!"); + LOGGER.log(Level.WARNING, "Error renaming the navigator: a file with the chosen filename exists already!"); + } + }; + promptForTextInput(Messages.NewNavigatorNamePrompt, navigatorName_displayed, renameNavigator); + } + + @FXML + void discardChangesAction(ActionEvent actionEvent) { + renameNavigator_thunk = null; + reloadNavigator(); + setUnsavedChanges(false); + } + + @FXML + void locateCurrentInstanceAction(ActionEvent actionEvent) { + var activeDockPane = DockPane.getActiveDockPane(); + if (activeDockPane == null) { + return; + } + var activeDockItem = (DockItem) activeDockPane.getSelectionModel().getSelectedItem(); + if (activeDockItem == null) { + return; + } + + String relativePathOfCurrentInstance; + if (activeDockItem instanceof DockItemWithInput) { + DockItemWithInput activeDockItemWithInput = (DockItemWithInput) activeDockItem; + URI uri = activeDockItemWithInput.getInput(); + String queryOfCurrentInstance = uri.getQuery(); + + if (queryOfCurrentInstance != null && !queryOfCurrentInstance.isEmpty()) { + // Queries (i.e. macro values) are ignored currently + } + + String absolutePathOfCurrentInstance = uri.getPath(); + + if (!absolutePathOfCurrentInstance.startsWith(OPI_ROOT)) { + return; + } + relativePathOfCurrentInstance = absolutePathOfCurrentInstance.substring(OPI_ROOT.length()); + } + else { + return; + } + + AppInstance activeAppInstance = activeDockItem.getApplication(); + if (activeAppInstance == null) { + return; + } + + NavigatorTreeNode.NodeType nodeTypeOfCurrentInstance; + if (activeAppInstance instanceof DisplayRuntimeInstance) { + nodeTypeOfCurrentInstance = NavigatorTreeNode.NodeType.DisplayRuntime; + } + else if (activeAppInstance instanceof DataBrowserInstance) { + nodeTypeOfCurrentInstance = NavigatorTreeNode.NodeType.DataBrowser; + } + else { + return; + } + + List previouslySelectedIndices = new LinkedList<>(treeView.getSelectionModel().getSelectedIndices()); + treeView.getSelectionModel().clearSelection(); + List> locatedTreeItems = locateCurrentInstanceAction_recursor(treeView.getRoot(), + nodeTypeOfCurrentInstance, + relativePathOfCurrentInstance); + Collections.reverse(locatedTreeItems); + + if (locatedTreeItems.size() > 0) { + for (var locatedTreeItem : locatedTreeItems) { + int index = treeView.getRow(locatedTreeItem); + treeView.getSelectionModel().select(index); + } + treeView.requestFocus(); + } + else { + for (int index : previouslySelectedIndices) { + treeView.getSelectionModel().select(index); + } + } + } + + private void setExpandedStatusOnTree(TreeItem treeItemToSet, + TreeItem treeItemWithExpandedStatuses) { + for (var itemToSetAndItemWithExpandedStatus : Streams.zip(treeItemToSet.getChildren().stream(), + treeItemWithExpandedStatuses.getChildren().stream(), + (a, b) -> new Pair, TreeItem>(a, b)) + .collect(Collectors.toList())) { + var itemToSet = itemToSetAndItemWithExpandedStatus.getKey(); + var itemWithExpandedStatus = itemToSetAndItemWithExpandedStatus.getValue(); + if (!itemToSet.isLeaf() && !itemWithExpandedStatus.isLeaf()) { + itemToSet.setExpanded(itemWithExpandedStatus.isExpanded()); + setExpandedStatusOnTree(itemToSet, itemWithExpandedStatus); + } + else { + return; + } + } + } + + private void reloadNavigator() { + var oldRoot = treeView.getRoot(); + loadNavigator(currentlySelectedNavigator); + setExpandedStatusOnTree(treeView.getRoot(), oldRoot); + } + + private List> locateCurrentInstanceAction_recursor(TreeItem treeItem, + NavigatorTreeNode.NodeType nodeTypeOfCurrentInstance, + String relativePathOfCurrentInstance) { + NavigatorTreeNode navigatorTreeNode = treeItem.getValue(); + if (navigatorTreeNode.getNodeType() == NavigatorTreeNode.NodeType.VirtualFolder) { + List> locatedTreeItems = new LinkedList(); + for (var child : treeItem.getChildren()) { + var locatedTreeItems_child = locateCurrentInstanceAction_recursor(child, nodeTypeOfCurrentInstance, relativePathOfCurrentInstance); + locatedTreeItems.addAll(locatedTreeItems_child); + } + if (locatedTreeItems.size() > 0) { + treeItem.setExpanded(true); + } + return locatedTreeItems; + } + else if ( navigatorTreeNode.getNodeType() == nodeTypeOfCurrentInstance + && navigatorTreeNode.getRelativePath().equals(relativePathOfCurrentInstance)) { + List> newList = new LinkedList(); + newList.add(treeItem); + return newList; + } + else { + return new LinkedList<>(); + } + } + + @FXML + void expandAllAction(ActionEvent actionEvent) { + for (var child : treeView.getRoot().getChildren()) { + setExpandedPropertyOnAllNodes(child, true); + } + } + + @FXML + void collapseAllAction(ActionEvent actionEvent) { + for (var child : treeView.getRoot().getChildren()) { + setExpandedPropertyOnAllNodes(child, false); + } + } + + TreeItem navigatorSelectionTree; + @FXML + HBox navigatorSelector; + private File currentlySelectedNavigator; + Supplier renameNavigator_thunk = null; + private boolean editModeEnabled = false; + + private String navigatorName_displayed; + protected String navigatorName_original; + protected boolean unsavedChanges = false; + private Color dragAndDropColor = Color.PALEGREEN; + private Color neutralColor = Color.TRANSPARENT; + + private Border emptyBorder = new Border(new BorderStroke(neutralColor, + BorderStrokeStyle.NONE, + new CornerRadii(0), + new BorderWidths(2))); + + private Border bottomBorder = new Border(new BorderStroke(Color.TRANSPARENT, + Color.TRANSPARENT, + dragAndDropColor, + Color.TRANSPARENT, + BorderStrokeStyle.NONE, + BorderStrokeStyle.NONE, + BorderStrokeStyle.SOLID, + BorderStrokeStyle.NONE, + new CornerRadii(0), + new BorderWidths(2), + new Insets(0))); + + private void enableDragNDropToTopBar() { + topBar.setOnDragOver(dragEvent -> { + dragEvent.acceptTransferModes(TransferMode.COPY); + dragEvent.consume(); + }); + + + topBar.setOnDragDetected(mouseEvent -> { + Dragboard dragboard = topBar.startDragAndDrop(TransferMode.COPY); + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.put(navigationTreeDataFormat, ""); + dragboard.setContent(clipboardContent); + mouseEvent.consume(); + }); + + topBar.setOnDragOver(mouseEvent -> { + topBar.setBorder(bottomBorder); + mouseEvent.acceptTransferModes(TransferMode.COPY); + }); + + topBar.setOnDragExited(mouseEvent -> { + topBar.setBorder(emptyBorder); + }); + + topBar.setOnDragDropped(dragEvent -> { + Dragboard dragboard = dragEvent.getDragboard(); + if (dragboard.hasFiles()) { + List files = dragboard.getFiles(); + List> resourceNavigatorTreeItems = new LinkedList<>(); + for (File file : files) { + String absolutePath = file.getAbsolutePath(); + NavigatorTreeNode resourceNavigatorTreeNode; + try { + resourceNavigatorTreeNode = createResourceNavigatorTreeNode(absolutePath); + } catch (Exception e) { + throw new RuntimeException(e); + } + TreeItem newTreeItem = new TreeItem(resourceNavigatorTreeNode); + resourceNavigatorTreeItems.add(newTreeItem); + } + + Collections.reverse(resourceNavigatorTreeItems); + + for (var newTreeItem : resourceNavigatorTreeItems) { + treeView.getRoot().getChildren().add(newTreeItem); + moveTreeItem(treeView.getRoot(), newTreeItem); + } + setUnsavedChanges(true); + } + + if (dragboard.hasContent(navigationTreeDataFormat)) { + var droppedTreeItem = treeView.getSelectionModel().getSelectedItem(); + moveTreeItem(treeView.getRoot(), droppedTreeItem); + } + }); + + MenuItem menuItem_renameNavigator = new MenuItem(Messages.RenameNavigator); + menuItem_renameNavigator.setOnAction(actionEvent -> { + renameNavigatorAction(actionEvent); + }); + + MenuItem menuItem_createNewFolder = new MenuItem(Messages.CreateNewFolder); + menuItem_createNewFolder.setOnAction(actionEvent -> { + TreeItem newFolder = createFolderTreeItem(NavigatorTreeNode.createVirtualFolderNode("New Folder")); + newFolder.setExpanded(true); + + treeView.getRoot().getChildren().add(0, newFolder); + + Consumer setNewFolderName = newFolderName -> { + if (!newFolder.getValue().getLabel().equals(newFolderName)) { + setUnsavedChanges(true); + newFolder.getValue().setLabel(newFolderName); + treeView.refresh(); + } + }; + + promptForTextInput(Messages.NewFolderNamePrompt, newFolder.getValue().getLabel(), setNewFolderName); + setUnsavedChanges(true); + treeView.refresh(); + }); + + ContextMenu topBar_contextMenu = new ContextMenu(menuItem_renameNavigator, + menuItem_createNewFolder); + + topBar.setOnContextMenuRequested(eventHandler -> { + topBar_contextMenu.show(topBar.getScene().getWindow(), eventHandler.getScreenX(), eventHandler.getScreenY()); + }); + } + + private boolean checkFileExtension(String fileExtension_expected, String filename) { + if (filename.contains(".")) { + int startIndex = filename.lastIndexOf(".") + 1; + int endIndex; + if (filename.contains("?") && filename.lastIndexOf("&") > startIndex) { + endIndex = filename.lastIndexOf("&"); + } else { + endIndex = filename.length(); + } + String fileExtension_filename = filename.substring(startIndex, endIndex); + + if (fileExtension_filename.equalsIgnoreCase(fileExtension_expected)) { + return true; + } + else { + return false; + } + } + else { + return false; + } + } + + private NavigatorTreeNode createOPINavigatorTreeNode(String relativePath) throws XMLStreamException { + if (!checkFileExtension("bob", relativePath)) { + throw new XMLStreamException("Unexpected file extension: " + relativePath + " (Expected a filename with the file extension '.bob'."); + } + DisplayModel displayModel; + try { + String absolutePath = OPI_ROOT + relativePath; + displayModel = ModelLoader.resolveAndLoadModel("", absolutePath); + } + catch (Exception exception) { + displayWarning(exception.getMessage()); + throw new XMLStreamException(exception.getMessage(), exception); + } + String opiName = displayModel.getDisplayName(); + + NavigatorTreeNode opiNode = NavigatorTreeNode.createDisplayRuntimeNode(opiName, relativePath, this); + return opiNode; + } + + private NavigatorTreeNode createDataBrowserNavigatorTreeNode(String relativePath) throws XMLStreamException { + if (!checkFileExtension("plt", relativePath)) { + throw new XMLStreamException("Unexpected file extension: " + relativePath + "(Expected a filename with the file extension '.plt'."); + } + InputStream stream; + try { + String absolutePath = OPI_ROOT + relativePath; + stream = ResourceParser.getContent(new URI("file:" + absolutePath)); + Model model = new Model(); + XMLPersistence.load(model, stream); + String dataBrowserName = model.getTitle().orElse(Messages.GenericDataBrowserName); + + NavigatorTreeNode dataBrowserNode = NavigatorTreeNode.createDataBrowserNode(dataBrowserName, relativePath, this); + return dataBrowserNode; + } + catch (Exception exception) { + throw new XMLStreamException(exception.getMessage()); + } + } + + private NavigatorTreeNode createResourceNavigatorTreeNode(String absolutePath) { + if (absolutePath.startsWith(OPI_ROOT)) { + String relativePath = absolutePath.substring(OPI_ROOT.length()); + try { + if (relativePath.contains(".")) { + int startIndex = relativePath.lastIndexOf(".") + 1; + int endIndex; + if (relativePath.contains("?") && relativePath.lastIndexOf("&") > startIndex) { + endIndex = relativePath.lastIndexOf("&"); + } + else { + endIndex = relativePath.length(); + } + String fileExtension = relativePath.substring(startIndex, endIndex); + + if (fileExtension.equalsIgnoreCase("bob")) { + return createOPINavigatorTreeNode(relativePath); + } + else if (fileExtension.equalsIgnoreCase("plt")) { + return createDataBrowserNavigatorTreeNode(relativePath); + } + else { + displayWarning(Messages.UnknownFileExtensionWarning + " " + fileExtension); + LOGGER.log(Level.WARNING, "Unknown file extension: " + fileExtension); + throw new Exception("Unknown file extension: " + fileExtension); + } + } + } catch (Exception exception) { + displayWarning(exception.getMessage()); + LOGGER.log(Level.WARNING, exception.getMessage(), exception); + } + } + else { + String warningText = Messages.FileIsNotInTheNavigatorDataDirectoryWarning + " '" + absolutePath + "'."; + displayWarning(warningText); + LOGGER.log(Level.WARNING, warningText); + } + + return null; + } + + private void disableEverythingExceptUserInput() { + for (var child : navigator.getChildren()) { + if (child != userInputVBox) { + child.setDisable(true); + } + } + } + + private void enableEverythingExceptUserInput() { + for (var child : navigator.getChildren()) { + if (child != userInputVBox) { + child.setDisable(false); + } + } + } + + protected void disableNavigator() { + BorderPane borderPane = DockStage.getLayout((Stage) DockPane.getActiveDockPane().getScene().getWindow()); + navigator.setDisable(true); + } + + protected void enableNavigator() { + BorderPane borderPane = DockStage.getLayout((Stage) DockPane.getActiveDockPane().getScene().getWindow()); + navigator.setDisable(false); + } + + private void promptForTextInput(String prompt, + String defaultInput, + Consumer continuation) { + disableEverythingExceptUserInput(); + + userInputVBox.setStyle("-fx-background-color: palegreen; "); + Label promptLabel = new Label(prompt); + promptLabel.setStyle("-fx-font-weight: bold; "); + TextField inputField = new TextField(defaultInput); + + inputField.setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ENTER) { + userInputVBox.getChildren().clear(); + inputField.setDisable(true); + enableEverythingExceptUserInput(); + treeView.requestFocus(); + continuation.accept(inputField.getText()); + } + else if (keyEvent.getCode() == KeyCode.ESCAPE) { + inputField.setDisable(true); + userInputVBox.getChildren().clear(); + treeView.requestFocus(); + enableEverythingExceptUserInput(); + } + }); + + userInputVBox.getChildren().clear(); + userInputVBox.getChildren().addAll(promptLabel, inputField); + inputField.selectAll(); + inputField.requestFocus(); + } + + private void loadNavigator(File navigatorFile) { + { + Runnable disableMenuButtons = () -> { + navigatorMenuButton.setDisable(true); + collapseAllButton.setDisable(true); + expandAllButton.setDisable(true); + }; + + TreeItem navigatorTreeRoot; + try { + navigatorTreeRoot = convertXMLToNavigator(navigatorFile); + } catch (FileNotFoundException fileNotFoundException) { + if (currentlySelectedNavigator == null) { + disableMenuButtons.run(); + } + String warningMessage = Messages.FileNotFoundWarning + " '" + navigatorFile.getAbsolutePath() + "'."; + displayWarning(warningMessage); + LOGGER.log(Level.WARNING, warningMessage); + return; + } catch (XMLStreamException xmlStreamException) { + if (currentlySelectedNavigator == null) { + disableMenuButtons.run(); + } + String warningMessage = Messages.ErrorLoadingTheNavigatorWarning + " '" + navigatorName_original + "': " + xmlStreamException.getMessage(); + displayWarning(warningMessage); + LOGGER.log(Level.WARNING, warningMessage); + return; + } + if (navigatorTreeRoot != null) { + { + currentlySelectedNavigator = navigatorFile; + TreeItem selectedNavigator = getCurrentSelectedNavigatorTreeItem(navigatorSelectionTree); + + navigatorSelector.getChildren().clear(); + List treePathWidgetNodes = createTreePathWidgetNodes(selectedNavigator, + editModeEnabled, + NAVIGATOR_SELECTOR_BUTTONS_CSS, + NAVIGATOR_SELECTOR_MENU_ITEMS_CSS); + navigatorSelector.getChildren().addAll(treePathWidgetNodes); + Node spring = ToolbarHelper.createSpring(); + navigatorSelector.getChildren().add(spring); + + navigatorName_original = navigatorName_displayed = selectedNavigator.getValue().getLabel(); + navigatorLabel.setText(navigatorName_displayed); + } + + navigatorTreeRoot.setExpanded(true); + treeView.setRoot(navigatorTreeRoot); + + // Enable buttons: + navigatorMenuButton.setDisable(false); + collapseAllButton.setDisable(false); + expandAllButton.setDisable(false); + + if (!navigatorFile.canWrite()) { + enterEditModeMenuItem.setDisable(true); + leaveEditModeMenuItem.setDisable(true); + } else if (editModeEnabled) { + enterEditModeMenuItem.setDisable(true); + leaveEditModeMenuItem.setDisable(false); + } else { + enterEditModeMenuItem.setDisable(false); + leaveEditModeMenuItem.setDisable(true); + } + treeView.requestFocus(); + } + } + } + + private void setUnsavedChanges(boolean newValue) { + unsavedChanges = newValue; + saveChangesMenuItem.setDisable(!newValue); + discardChangesMenuItem.setDisable(!newValue); + leaveEditModeMenuItem.setDisable(newValue); + navigatorSelector.setDisable(newValue); + + if (newValue) { + navigatorLabel.setText(navigatorName_displayed + "*"); + } + } + + private void writeNavigatorToXML(TreeItem treeItem, + File file) { + XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); + try { + FileWriter fileWriter = new FileWriter(file); + XMLStreamWriter xmlStreamWriter = xmlOutputFactory.createXMLStreamWriter(fileWriter); + + convertCurrentNavigatorToXML_recurse(treeItem, xmlStreamWriter, 0); + + xmlStreamWriter.flush(); + xmlStreamWriter.close(); + + return; + } + catch (Exception exception) { + LOGGER.log(Level.SEVERE, "Error writing the navigator to file: " + exception.getMessage()); + return; + } + } + + private void disableDragNDropToTopBar() { + topBar.setOnDragOver(dragEvent -> { }); + topBar.setOnDragDetected(mouseEvent -> { }); + topBar.setOnDragOver(mouseEvent -> { }); + topBar.setOnDragExited(mouseEvent -> { }); + topBar.setOnDragDropped(dragEvent -> { }); + + topBar.setOnContextMenuRequested(eventHandler -> { }); + } + + private void rebuildNavigatorSelector() throws Exception { + File locationOfNavigators = new File(NAVIGATOR_ROOT); + + try { + navigatorSelectionTree = buildNavigatorSelectionTree(locationOfNavigators); + } catch (Exception exception) { + displayWarning(exception.getMessage()); + throw exception; + } + + var selectedNavigatorTreeItem = getCurrentSelectedNavigatorTreeItem(navigatorSelectionTree); + if (selectedNavigatorTreeItem == null) { + selectedNavigatorTreeItem = getTopMostItem(navigatorSelectionTree); + } + if (selectedNavigatorTreeItem == null) { + // There is no navigator at NAVIGATOR_ROOT + String newNavigatorName = Messages.NewNavigatorDefaultName; + File newNavigatorFile = new File(locationOfNavigators, newNavigatorName + ".navigator"); + createNewNavigator(newNavigatorFile); + rebuildNavigatorSelector(); + return; + } + + navigatorSelector.getChildren().clear(); + navigatorSelector.getChildren().add(createTreePathWidget(selectedNavigatorTreeItem, + editModeEnabled, + NAVIGATOR_SELECTOR_BUTTONS_CSS, + NAVIGATOR_SELECTOR_MENU_ITEMS_CSS)); + } + + private void setExpandedPropertyOnAllNodes(TreeItem treeItem, boolean newValue) { + treeItem.setExpanded(newValue); + ((ObservableList) treeItem.getChildren()).forEach(child -> setExpandedPropertyOnAllNodes(child, newValue)); + } + + private void moveTreeItem(TreeItem targetTreeItem, + TreeItem treeItemToMove) { + if (isDescendantOf(targetTreeItem, treeItemToMove)) { + return; + } + setUnsavedChanges(true); + var siblingsOfTreeItemToMove = treeItemToMove.getParent().getChildren(); + int indexOfTreeItemToMove = siblingsOfTreeItemToMove.indexOf(treeItemToMove); + TreeItem temporaryMarker = new TreeItem<>(NavigatorTreeNode.createTemporaryMarker()); + siblingsOfTreeItemToMove.add(indexOfTreeItemToMove, temporaryMarker); + siblingsOfTreeItemToMove.remove(treeItemToMove); + + if (targetTreeItem == null) { + treeView.getRoot().getChildren().add(treeItemToMove); + } + else if (targetTreeItem.getValue().getNodeType() == NavigatorTreeNode.NodeType.VirtualFolder && targetTreeItem.isExpanded()) { + targetTreeItem.getChildren().add(0, treeItemToMove); + } else { + var siblingsOfTargetTreeItem = targetTreeItem.getParent().getChildren(); + int indexOfTargetTreeItem = siblingsOfTargetTreeItem.indexOf(targetTreeItem); + if (indexOfTargetTreeItem < 0) { + indexOfTargetTreeItem = siblingsOfTargetTreeItem.indexOf(temporaryMarker); + } + siblingsOfTargetTreeItem.add(indexOfTargetTreeItem + 1, treeItemToMove); + } + siblingsOfTreeItemToMove.remove(temporaryMarker); + treeView.refresh(); + } + + private boolean isDescendantOf(TreeItem tree1, TreeItem tree2) { + if (tree1 == tree2) { + return true; + } + else if (tree2.getValue().getNodeType() == NavigatorTreeNode.NodeType.VirtualFolder) { + return tree2.getChildren().stream().anyMatch(subtree -> isDescendantOf(tree1, subtree)); + } + else { + return false; + } + } + + protected class NavigationTree_TreeCellClass extends TreeCell { + boolean dragAndDropIndicatorAbove = false; + ContextMenu contextMenu = new ContextMenu(); + + public NavigationTree_TreeCellClass() { + setContextMenu(contextMenu); + } + + @Override + protected void updateItem(NavigatorTreeNode newSelectionTreeNode, boolean empty) { + + super.updateItem(newSelectionTreeNode, empty); + setBorder(emptyBorder); + + if (!empty && newSelectionTreeNode != null && newSelectionTreeNode.getAction() != null) { + Consumer action = newSelectionTreeNode.getAction(); + + setOnMousePressed(mouseEvent -> { + treeView.requestFocus(); + if (mouseEvent.getButton() == MouseButton.PRIMARY) { + if (mouseEvent.isControlDown()) { + action.accept(NavigatorTreeNode.Target.NewTab_InBackground); + } else { + action.accept(NavigatorTreeNode.Target.CurrentTab); + } + } + }); + } + else { + setOnMousePressed(mouseEvent -> { }); + } + + { + setOnDragOver(dragEvent -> { + if (editModeEnabled) { + dragEvent.acceptTransferModes(TransferMode.COPY); + dragEvent.consume(); + } + }); + + setOnDragDetected(mouseEvent -> { + if (editModeEnabled && !empty) { + Dragboard dragboard = startDragAndDrop(TransferMode.COPY); + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.put(navigationTreeDataFormat, ""); + dragboard.setContent(clipboardContent); + mouseEvent.consume(); + } + }); + + setOnDragOver(mouseEvent -> { + if (editModeEnabled) { + borderProperty().set(bottomBorder); + mouseEvent.acceptTransferModes(TransferMode.COPY); + } + }); + + setOnDragExited(mouseEvent -> { + if (editModeEnabled) { + dragAndDropIndicatorAbove = false; + borderProperty().set(emptyBorder); + } + }); + + setOnDragDropped(dragEvent -> { + if (editModeEnabled) { + Dragboard dragboard = dragEvent.getDragboard(); + if (dragboard.hasFiles()) { + List files = dragboard.getFiles(); + List> resourceNavigatorTreeItems = new LinkedList<>(); + for (File file : files) { + String absolutePath = file.getAbsolutePath(); + NavigatorTreeNode resourceNavigatorTreeNode; + try { + resourceNavigatorTreeNode = createResourceNavigatorTreeNode(absolutePath); + } catch (Exception e) { + throw new RuntimeException(e); + } + TreeItem newTreeItem = new TreeItem(resourceNavigatorTreeNode); + resourceNavigatorTreeItems.add(newTreeItem); + } + + Collections.reverse(resourceNavigatorTreeItems); + + for (var newTreeItem : resourceNavigatorTreeItems) { + treeView.getRoot().getChildren().add(newTreeItem); + moveTreeItem(getTreeItem(), newTreeItem); + } + setUnsavedChanges(true); + } + + if (dragboard.hasContent(navigationTreeDataFormat)) { + var droppedTreeItem = treeView.getSelectionModel().getSelectedItem(); + moveTreeItem(getTreeItem(), droppedTreeItem); + } + } + }); + } + + { // Create the context menu: + contextMenu.getItems().clear(); + if (!empty && newSelectionTreeNode != null && (newSelectionTreeNode.getNodeType() == NavigatorTreeNode.NodeType.DisplayRuntime || newSelectionTreeNode.getNodeType() == NavigatorTreeNode.NodeType.DataBrowser)) { + MenuItem menuItem_openInNewTab = new MenuItem(Messages.OpenInNewTab); + menuItem_openInNewTab.setOnAction(actionEvent -> { + getTreeItem().getValue().getAction().accept(NavigatorTreeNode.Target.NewTab); + }); + contextMenu.getItems().add(menuItem_openInNewTab); + + MenuItem menuItem_openInBackgroundTab = new MenuItem(Messages.OpenInBackgroundTab); + menuItem_openInBackgroundTab.setOnAction(actionEvent -> { + getTreeItem().getValue().getAction().accept(NavigatorTreeNode.Target.NewTab_InBackground); + }); + contextMenu.getItems().add(menuItem_openInBackgroundTab); + } + if (!empty && newSelectionTreeNode != null && editModeEnabled) { + MenuItem menuItem_deleteItem = new MenuItem(Messages.DeleteItem); + menuItem_deleteItem.setOnAction(actionEvent -> { + var treeItem = getTreeItem(); + Runnable deleteAction = () -> { + disableNavigator(); + setUnsavedChanges(true); + treeItem.getParent().getChildren().remove(treeItem); + treeView.refresh(); + enableNavigator(); + }; + var treeItemName = treeItem.getValue().getLabel(); + promptForYesNo(Messages.DeletePrompt + " '" + treeItemName + "'?", deleteAction); + }); + + if (contextMenu.getItems().size() > 0) { + contextMenu.getItems().add(new SeparatorMenuItem()); + } + contextMenu.getItems().add(menuItem_deleteItem); + contextMenu.getItems().add(new SeparatorMenuItem()); + + { + MenuItem menuItem_createNewFolder = new MenuItem(Messages.CreateNewFolder); + menuItem_createNewFolder.setOnAction(actionEvent -> { + TreeItem newFolder = createFolderTreeItem(NavigatorTreeNode.createVirtualFolderNode("New Folder")); + newFolder.setExpanded(true); + + if (newSelectionTreeNode.getNodeType() == NavigatorTreeNode.NodeType.VirtualFolder && getTreeItem().isExpanded()) { + getTreeItem().getChildren().add(0, newFolder); + } else { + var siblings = getTreeItem().getParent().getChildren(); + int indexOfTreeItem = siblings.indexOf(getTreeItem()); + siblings.add(indexOfTreeItem + 1, newFolder); + } + + Consumer setNewFolderName = newFolderName -> { + if (!newFolder.getValue().getLabel().equals(newFolderName)) { + setUnsavedChanges(true); + newFolder.getValue().setLabel(newFolderName); + treeView.refresh(); + } + }; + + promptForTextInput(Messages.NewFolderNamePrompt, newFolder.getValue().getLabel(), setNewFolderName); + setUnsavedChanges(true); + treeView.refresh(); + }); + contextMenu.getItems().add(menuItem_createNewFolder); + + if (!empty && newSelectionTreeNode != null && newSelectionTreeNode.getNodeType() == NavigatorTreeNode.NodeType.VirtualFolder) { + Runnable renameFolder = () -> promptForTextInput(Messages.RenameFolderPrompt, + newSelectionTreeNode.getLabel(), + newName -> { + if (!newSelectionTreeNode.getLabel().equals(newName)) { + setUnsavedChanges(true); + newSelectionTreeNode.setLabel(newName); + treeView.refresh(); + } + }); + + MenuItem menuItem_renameFolder = new MenuItem(Messages.RenameFolder); + + menuItem_renameFolder.setOnAction(actionEvent -> { renameFolder.run(); }); + + contextMenu.getItems().add(menuItem_renameFolder); + } + } + } + + if (!empty && newSelectionTreeNode != null) { + super.setText(newSelectionTreeNode.getLabel()); + if (newSelectionTreeNode.getNodeType() == NavigatorTreeNode.NodeType.VirtualFolder) { + if (getTreeItem().expandedProperty().get()) { + var folderIcon = ImageCache.getImageView(NavigatorInstance.class, "/icons/folder.png"); + super.setGraphic(folderIcon); + } + else { + var closedFolderIcon = ImageCache.getImageView(NavigatorInstance.class, "/icons/closed_folder.png"); + super.setGraphic(closedFolderIcon); + } + } + else { + super.setGraphic(newSelectionTreeNode.getIcon()); + } + } + else { + super.setText(null); + super.setGraphic(null); + } + } + setContentDisplay(ContentDisplay.LEFT); + } + } + + private TreeItem convertXMLToNavigator(File file) throws XMLStreamException, FileNotFoundException { + XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + + FileReader fileReader = new FileReader(file); + XMLStreamReader xmlStreamReader; + { + XMLStreamReader xmlStreamReader_unfiltered = xmlInputFactory.createXMLStreamReader(fileReader); + xmlStreamReader = xmlInputFactory.createFilteredReader(xmlStreamReader_unfiltered, + reader -> !reader.isWhiteSpace()); + } + + if (xmlStreamReader.getEventType() == XMLStreamConstants.START_DOCUMENT) { + xmlStreamReader.next(); + TreeItem treeItem = convertXMLToNavigator_recurse(xmlStreamReader); + if (xmlStreamReader.getEventType() == XMLStreamConstants.END_DOCUMENT) { + return treeItem; + } + } + throw new XMLStreamException("Error parsing the XML of '" + file.getAbsolutePath() + "'."); + } + + private TreeItem convertXMLToNavigator_recurse(XMLStreamReader xmlStreamReader) throws XMLStreamException { + if (xmlStreamReader.getEventType() == XMLStreamConstants.START_ELEMENT) { + String tagName = xmlStreamReader.getLocalName(); + + if (tagName.equalsIgnoreCase("Folder")) { + consumeStartElement("Folder", xmlStreamReader); + + String folderName = consumeTextInsidePair("Name", xmlStreamReader); + NavigatorTreeNode folderNavigatorTreeNode = NavigatorTreeNode.createVirtualFolderNode(folderName); + TreeItem folderTreeItem = createFolderTreeItem(folderNavigatorTreeNode); + + while (xmlStreamReader.getEventType() == XMLStreamConstants.START_ELEMENT) { + TreeItem childTreeItem = convertXMLToNavigator_recurse(xmlStreamReader); + folderTreeItem.getChildren().add(childTreeItem); + } + consumeEndElement("Folder", xmlStreamReader); + return folderTreeItem; + } + else if (tagName.equalsIgnoreCase("DisplayRuntime")) { + String relativePath = xmlStreamReader.getElementText(); + + NavigatorTreeNode opiNode = createOPINavigatorTreeNode(relativePath); + TreeItem opiTreeItem = new TreeItem(opiNode); + consumeEndElement(tagName, xmlStreamReader); + return opiTreeItem; + } + else if (tagName.equalsIgnoreCase("DataBrowser")) { + String filename = xmlStreamReader.getElementText(); + NavigatorTreeNode dataBrowserNode = createDataBrowserNavigatorTreeNode(filename); + TreeItem dataBrowserTreeItem = new TreeItem(dataBrowserNode); + consumeEndElement(tagName, xmlStreamReader); + return dataBrowserTreeItem; + } + else { + throw new XMLStreamException("Unexpected <" + tagName + "> tag."); + } + } + throw new XMLStreamException("Expected a start tag."); + } + + private String consumeTextInsidePair(String tagName, XMLStreamReader xmlStreamReader) throws XMLStreamException { + String contents; + String localName = xmlStreamReader.getLocalName(); + if (xmlStreamReader.getEventType() == XMLStreamConstants.START_ELEMENT && localName.equalsIgnoreCase(tagName)) { + contents = xmlStreamReader.getElementText(); + } + else { + throw new XMLStreamException("Expected a <" + localName + "> tag."); + } + consumeEndElement(tagName, xmlStreamReader); + return contents; + } + + private void consumeStartElement(String localNameToConsume, XMLStreamReader xmlStreamReader) throws XMLStreamException { + int eventType = xmlStreamReader.getEventType(); + String localName = xmlStreamReader.getLocalName(); + if (eventType == XMLStreamConstants.START_ELEMENT && localName.equalsIgnoreCase(localNameToConsume)) { + xmlStreamReader.next(); + return; + } + throw new XMLStreamException("Expected a <" + localName + "> tag."); + } + + private void consumeEndElement(String localNameToConsume, XMLStreamReader xmlStreamReader) throws XMLStreamException { + int eventType = xmlStreamReader.getEventType(); + String localName = xmlStreamReader.getLocalName(); + if (eventType == XMLStreamConstants.END_ELEMENT && localName.equalsIgnoreCase(localNameToConsume)) { + xmlStreamReader.next(); + return; + } + throw new XMLStreamException("Unmatched <" + localName + "> tag."); + } + + + private void convertCurrentNavigatorToXML_recurse(TreeItem treeItem, + XMLStreamWriter xmlStreamWriter, + int indentationLevel) throws XMLStreamException { + NavigatorTreeNode navigatorTreeNode = treeItem.getValue(); + if (navigatorTreeNode.getNodeType() == NavigatorTreeNode.NodeType.VirtualFolder) { + newline(xmlStreamWriter, indentationLevel); + xmlStreamWriter.writeStartElement("Folder"); + + newline(xmlStreamWriter, indentationLevel+1); + xmlStreamWriter.writeStartElement("Name"); + xmlStreamWriter.writeCharacters(navigatorTreeNode.getLabel()); + xmlStreamWriter.writeEndElement(); + + for (var child : treeItem.getChildren()) { + convertCurrentNavigatorToXML_recurse(child, xmlStreamWriter, indentationLevel+1); + } + newline(xmlStreamWriter, indentationLevel); + xmlStreamWriter.writeEndElement(); + } + else if (navigatorTreeNode.getNodeType() == NavigatorTreeNode.NodeType.DisplayRuntime) { + newline(xmlStreamWriter, indentationLevel); + xmlStreamWriter.writeStartElement("DisplayRuntime"); + xmlStreamWriter.writeCharacters(navigatorTreeNode.getRelativePath()); + xmlStreamWriter.writeEndElement(); + } + else if (navigatorTreeNode.getNodeType() == NavigatorTreeNode.NodeType.DataBrowser) { + newline(xmlStreamWriter, indentationLevel); + xmlStreamWriter.writeStartElement("DataBrowser"); + xmlStreamWriter.writeCharacters(navigatorTreeNode.getRelativePath()); + xmlStreamWriter.writeEndElement(); + } + else { + String warningMessage = Messages.UnknownNodeTypeWarning + " '" + navigatorTreeNode.getNodeType() + "'."; + displayWarning(warningMessage); + LOGGER.log(Level.WARNING, warningMessage); + } + } + + private static void newline(XMLStreamWriter xmlStreamWriter, int indentationLevel) throws XMLStreamException { + xmlStreamWriter.writeCharacters("\n"); + for (int i = 0; i< indentationLevel; i++) { + xmlStreamWriter.writeCharacters(" "); + } + } + + private List> getPath(TreeItem treeItem) { + List> path = new LinkedList<>(); + while (treeItem != null && treeItem.getParent() != null) { + path.add(treeItem); + treeItem = treeItem.getParent(); + } + Collections.reverse(path); + return path; + } + + private TreeItem getCurrentSelectedNavigatorTreeItem(TreeItem treeItem) { + if (treeItem.getValue().getFile().equals(currentlySelectedNavigator)) { + return treeItem; + } + else { + for (var childTreeItem : treeItem.getChildren()) { + var currentlySelectedNavigator = getCurrentSelectedNavigatorTreeItem(childTreeItem); + if (currentlySelectedNavigator != null) { + return currentlySelectedNavigator; + } + } + return null; + } + } + + private List createTreePathWidgetNodes(TreeItem treeItem, + boolean editable, + String buttonStyle, + String menuStyle) { + List> pathElements = getPath(treeItem); + + List treePathWidgetNodes = new LinkedList<>(); + boolean firstItem = true; + for (TreeItem pathElementTreeItem : pathElements) { + if (firstItem) { + firstItem = false; + } + else { + var separator = createMenuSeparator(buttonStyle); + treePathWidgetNodes.add(separator); + } + Node menuBar = createNavigatorSelector(pathElementTreeItem, editable, buttonStyle, menuStyle); + treePathWidgetNodes.add(menuBar); + } + return treePathWidgetNodes; + } + + private Label createMenuSeparator(String buttonStyle) { + Label label = new Label("⮞"); + label.setStyle("-fx-padding: 0 0 0 0; -fx-alignment: center; -fx-font-size: 20; -fx-opacity: 0.5; "); + return label; + } + + private Node createNavigatorSelector(TreeItem navigationTree, + boolean editable, + String buttonStyle, + String menuStyle) { + MenuItem navigationMenuItem = navigationTreeToMenuItem(navigationTree.getParent(), + editable, + true, + menuStyle); + navigationMenuItem.setText(navigationTree.getValue().getLabel()); + MenuButton menuButton = new MenuButton(navigationTree.getValue().getLabel()); + + Menu navigationMenu = (Menu) navigationMenuItem; + menuButton.getItems().addAll(navigationMenu.getItems()); + + menuButton.setStyle("-fx-background-color: transparent; -fx-background-radius: 3; -fx-padding: 0 4 0 4; -fx-border-width: 1; -fx-border-style: solid; -fx-border-radius: 3; -fx-border-color: transparent; " + buttonStyle); + return menuButton; + } + + private MenuItem navigationTreeToMenuItem(TreeItem navigationTree, + boolean editable, + boolean isRootFolder, + String menuStyle) { + if (navigationTree.getValue().getFile().isDirectory()) { + Menu newMenu = new Menu(navigationTree.getValue().getLabel()); + for (var subTree : navigationTree.getChildren()) { + var subMenuItem = navigationTreeToMenuItem(subTree, editable, false, menuStyle); + newMenu.getItems().add(subMenuItem); + } + + if (editable) { + newMenu.getItems().add(new SeparatorMenuItem()); + MenuItem menuItem_newFolder = new MenuItem(Messages.CreateNewSubFolder); + menuItem_newFolder.setStyle(menuStyle); + + { + File currentDirectory = navigationTree.getValue().getFile(); + Consumer createNewFolder = newFolderName -> { + File newDirectory = new File(currentDirectory, newFolderName); + boolean newFolderWasCreated = newDirectory.mkdir(); + if (!newFolderWasCreated) { + String warningMessage = Messages.ErrorCreatingNewFolderWarning; + displayWarning(warningMessage); + LOGGER.log(Level.WARNING, warningMessage); + } + else { + try { + rebuildNavigatorSelector(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + menuItem_newFolder.setOnAction(actionEvent -> { + promptForTextInput(Messages.NewFolderNamePrompt, + Messages.NewNavigatorDefaultName, + createNewFolder); + }); + } + newMenu.getItems().add(menuItem_newFolder); + + MenuItem menuItem_newNavigator = new MenuItem(Messages.CreateNewNavigator); + menuItem_newNavigator.setStyle(menuStyle); + + { + menuItem_newNavigator.setOnAction(actionEvent -> { + promptForTextInput(Messages.NewNavigatorNamePrompt, + Messages.NewNavigatorDefaultName, + newNavigatorName -> { + File newNavigatorFile = new File(navigationTree.getValue().getFile(), newNavigatorName + ".navigator"); + createNewNavigator(newNavigatorFile); + try { + rebuildNavigatorSelector(); + } catch (Exception e) { + throw new RuntimeException(e); + } + loadNavigator(newNavigatorFile); + }); + }); + } + + newMenu.getItems().add(menuItem_newNavigator); + + if (!isRootFolder) { + MenuItem menuItem_renameParentFolder = new MenuItem(Messages.RenameFolder); + menuItem_renameParentFolder.setStyle(menuStyle); + + { + File currentDirectory = navigationTree.getValue().getFile(); + Consumer renameFolder = newFolderName -> { + File parentDirectory = currentDirectory.getParentFile(); + if (parentDirectory != null && parentDirectory.isDirectory()) { + File destination = new File(parentDirectory, newFolderName); + boolean directoryWasMoved = currentDirectory.renameTo(destination); + if (!directoryWasMoved) { + String warningMessage = Messages.ErrorRenamingFolderWarning; + displayWarning(warningMessage); + LOGGER.log(Level.WARNING, warningMessage); + } else { + // If the folder containing the current navigator is renamed, then the current location needs to be updated: + String canonicalPathOfCurrentlySelectedNavigator = currentlySelectedNavigator.getPath(); + String newCanonicalPathOfCurrentlySelectedNavigator = canonicalPathOfCurrentlySelectedNavigator.replace(currentDirectory.getPath(), destination.getPath()); + currentlySelectedNavigator = new File(newCanonicalPathOfCurrentlySelectedNavigator); + try { + rebuildNavigatorSelector(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + }; + menuItem_renameParentFolder.setOnAction(actionEvent -> { + promptForTextInput(Messages.RenameParentFolderPrompt, + navigationTree.getValue().getLabel(), + renameFolder); + }); + } + newMenu.getItems().add(menuItem_renameParentFolder); + } + } + + newMenu.setStyle(menuStyle); + return newMenu; + } + else { + MenuItem newMenuItem = new MenuItem(navigationTree.getValue().getLabel()); + newMenuItem.setOnAction(actionEvent -> { + navigationTree.getValue().getAction().run(); + }); + newMenuItem.setStyle(menuStyle); + return newMenuItem; + } + } + + private void createNewNavigator(File newNavigatorFile) { + boolean newNavigatorFileWasCreated; + try { + newNavigatorFileWasCreated = newNavigatorFile.createNewFile(); + } catch (Exception exception) { + String warningMessage = Messages.ErrorCreatingNewNavigatorFileWarning; + displayWarning(warningMessage); + LOGGER.log(Level.WARNING, warningMessage); + return; + } + + if (!newNavigatorFileWasCreated) { + String warningMessage = Messages.ErrorCreatingNewNavigatorFileWarning; + displayWarning(warningMessage); + LOGGER.log(Level.WARNING, warningMessage); + return; + } else { + TreeItem newNavigator = createEmptyNavigator(); + writeNavigatorToXML(newNavigator, newNavigatorFile); + } + } + + + private TreeItem createEmptyNavigator() { + NavigatorTreeNode rootFolder = NavigatorTreeNode.createVirtualFolderNode("Root"); + TreeItem treeItem = createFolderTreeItem(rootFolder); + return treeItem; + } + + TreeItem buildNavigatorSelectionTree(File locationOfNavigators) throws Exception { + + if (!locationOfNavigators.exists()) { + String errorMessage = "The specified option org.phoebus.applications.display.navigator/navigator_root=" + NAVIGATOR_ROOT + " is doesn't exist!"; + LOGGER.log(Level.SEVERE, errorMessage); + throw new Exception(errorMessage); + } + else if (!locationOfNavigators.isDirectory()) { + String errorMessage = "The specified location org.phoebus.applications.display.navigator/navigator_root=" + NAVIGATOR_ROOT + " is not a directory!"; + LOGGER.log(Level.SEVERE, errorMessage); + throw new Exception(errorMessage); + } + TreeItem navigatorSelectionTreeRoot = buildNavigatorSelectionTree_recursor(locationOfNavigators); + if (navigatorSelectionTreeRoot == null) { + throw new Exception("Error building the navigator selection tree!"); + } + return navigatorSelectionTreeRoot; + } + + TreeItem getTopMostItem(TreeItem treeItem) { + if (treeItem.getValue().getAction() != null) { + return treeItem; + } + else { + for (var childTreeItem : treeItem.getChildren()) { + var topMostItem = getTopMostItem(childTreeItem); + if (topMostItem != null) { + return topMostItem; + } + } + return null; + } + } + + HBox createTreePathWidget(TreeItem treeItem, + boolean editable, + String buttonStyle, + String menuStyle) { + HBox hBox = new HBox(); + hBox.setStyle("-fx-alignment: center; "); + List treePathWidgetNodes = createTreePathWidgetNodes(treeItem, editable, buttonStyle, menuStyle); + treePathWidgetNodes.forEach(treePathWidgetNode -> hBox.getChildren().add(treePathWidgetNode)); + Node spring = ToolbarHelper.createSpring(); + hBox.getChildren().add(spring); + return hBox; + + } + + private void promptForYesNo(String prompt, + Runnable continuation) { + disableEverythingExceptUserInput(); + + userInputVBox.setStyle("-fx-background-color: palegreen"); + Label promptLabel = new Label(prompt); + promptLabel.setStyle("-fx-font-weight: bold; "); + Button yesButton = new Button("Yes"); + yesButton.setStyle("-fx-alignment: center; "); + Button noButton = new Button("No"); + + Runnable closeConfirmDialog = () -> { + yesButton.setDisable(true); + noButton.setDisable(true); + userInputVBox.getChildren().clear(); + enableEverythingExceptUserInput(); + }; + + yesButton.setOnAction(actionEvent -> { + yesButton.setDisable(true); + noButton.setDisable(true); + continuation.run(); + userInputVBox.getChildren().clear(); + enableEverythingExceptUserInput(); + treeView.requestFocus(); + }); + + noButton.setOnAction(actionEvent -> { + closeConfirmDialog.run(); + treeView.requestFocus(); + }); + + HBox hBox = new HBox(promptLabel, + ToolbarHelper.createSpring(), + yesButton, + noButton); + hBox.setStyle("-fx-alignment: center; "); + hBox.setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ESCAPE) { + closeConfirmDialog.run(); + } + }); + + userInputVBox.getChildren().clear(); + userInputVBox.getChildren().add(hBox); + noButton.requestFocus(); + } + + private void displayWarning(String prompt) { + disableEverythingExceptUserInput(); + + userInputVBox.setStyle("-fx-background-color: red"); + Label promptLabel = new Label(prompt); + promptLabel.setStyle("-fx-font-weight: bold; "); + Button okButton = new Button("OK"); + + Runnable closeConfirmDialog = () -> { + okButton.setDisable(true); + userInputVBox.getChildren().clear(); + enableEverythingExceptUserInput(); + }; + + okButton.setOnAction(actionEvent -> { + closeConfirmDialog.run(); + treeView.requestFocus(); + }); + + HBox hBox = new HBox(promptLabel, + ToolbarHelper.createSpring(), + okButton); + hBox.setStyle("-fx-alignment: center; "); + hBox.setOnKeyPressed(keyEvent -> { + if (keyEvent.getCode() == KeyCode.ESCAPE) { + closeConfirmDialog.run(); + } + }); + + userInputVBox.getChildren().clear(); + userInputVBox.getChildren().add(hBox); + okButton.requestFocus(); + } + + private TreeItem buildNavigatorSelectionTree_recursor(File currentLocation) throws Exception { + if (currentLocation.isDirectory()) { + NavigatorSelectionTreeNode selectionTreeNode = new NavigatorSelectionTreeNode(currentLocation.getName(), + null, + currentLocation); + TreeItem treeItem = new TreeItem<>(selectionTreeNode); + List contents = Arrays.stream(currentLocation.listFiles()) + .filter(file -> file.isDirectory() || file.isFile() && checkFileExtension("navigator", file.getName())) + .sorted((file1, file2) -> { + if (file1.isDirectory() && file2.isDirectory()) { + return file1.getName().compareTo(file2.getName()); + } + else if (file1.isDirectory() && file2.isFile()) { + return -1; + } + else if (file1.isFile() && file2.isDirectory()) { + return 1; + } + else { + String filename1_withoutSuffix; + { + String filename1 = file1.getName(); + filename1_withoutSuffix = filename1.substring(0, filename1.length() - ".navigator".length()); + } + + String filename2_withoutSuffix; + { + String filename2 = file2.getName(); + filename2_withoutSuffix = filename2.substring(0, filename2.length() - ".navigator".length()); + } + + return filename1_withoutSuffix.compareTo(filename2_withoutSuffix); + } + + }) + .collect(Collectors.toList()); + for (var fileOrDirectory : contents) { + TreeItem childTreeItem = buildNavigatorSelectionTree_recursor(fileOrDirectory); + if (childTreeItem != null) { + treeItem.getChildren().add(childTreeItem); + } + } + return treeItem; + } + else if (currentLocation.isFile()) { + String filename = currentLocation.getName(); + if (checkFileExtension("navigator", filename)) { + String navigatorName = filename.substring(0, filename.length() - 10); // Removes trailing ".navigator" + TreeItem treeItem = new TreeItem<>(); + NavigatorSelectionTreeNode selectionTreeNode = new NavigatorSelectionTreeNode(navigatorName, + () -> loadNavigator(currentLocation), + currentLocation); + treeItem.setValue(selectionTreeNode); + return treeItem; + } + else { + return null; + } + } + else { + throw new Exception("Unsupported file type: " + currentLocation.getAbsolutePath()); + } + } + + private TreeItem createFolderTreeItem(NavigatorTreeNode folderNavigatorTreeNode) { + TreeItem newFolderTreeItem = new TreeItem(folderNavigatorTreeNode) { + @Override + public boolean isLeaf() { return false; } + }; + + return newFolderTreeItem; + } +} diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorInstance.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorInstance.java new file mode 100644 index 0000000000..9125f34714 --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorInstance.java @@ -0,0 +1,165 @@ +package org.phoebus.applications.display.navigator; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.SplitPane; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToolBar; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; +import org.phoebus.framework.persistence.Memento; +import org.phoebus.framework.spi.AppDescriptor; +import org.phoebus.framework.spi.AppInstance; +import org.phoebus.ui.application.PhoebusApplication; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.docking.DockStage; +import org.phoebus.ui.javafx.ImageCache; + +import java.awt.geom.Rectangle2D; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class NavigatorInstance implements AppInstance { + protected static Logger LOGGER = Logger.getLogger(NavigatorInstance.class.getPackageName()); + private static boolean running = false; + private static ToolBar phoebusApplicationToolbar = null; + private static NavigatorAppResourceDescriptor navigator; + protected static NavigatorController controller; + + public NavigatorInstance(NavigatorAppResourceDescriptor navigatorAppResourceDescriptor) { + if (running) { + return; + } + else { + running = true; + } + + try { + FXMLLoader loader = new FXMLLoader(); + var location = NavigatorInstance.class.getResource("/org/phoebus/applications/display/navigator/ui/Navigator.fxml"); + loader.setLocation(location); + loader.load(); + controller = loader.getController(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Unable to initialize Navigator UI."); + throw new RuntimeException(e); + } + + navigator = navigatorAppResourceDescriptor; + BorderPane borderPane = DockStage.getLayout((Stage) DockPane.getActiveDockPane().getScene().getWindow()); + + Node oldCenterPane = borderPane.getCenter(); + SplitPane splitPane = new SplitPane(oldCenterPane); + borderPane.setCenter(splitPane); + if (oldCenterPane instanceof DockPane) { + DockPane dockPane = (DockPane) oldCenterPane; + dockPane.setDockParent(splitPane); + } + else { + throw new RuntimeException("Error loading navigator: object of type 'DockPane' expected, but received object of type '" + oldCenterPane.getClass().toString() + "'."); + } + + phoebusApplicationToolbar = PhoebusApplication.INSTANCE.getToolbar(); + ImageView homeIcon = ImageCache.getImageView(ImageCache.class, "/icons/navigator.png"); + homeIcon.setFitHeight(16.0); + homeIcon.setFitWidth(16.0); + ToggleButton navigatorButton = new ToggleButton(null, homeIcon); + navigatorButton.setTooltip(new Tooltip(Messages.NavigatorTooltip)); + + double[] previousDividerPosition = {0.12}; + controller.navigator.setVisible(false); + navigatorButton.setOnAction(actionEvent -> { + if (navigatorButton.isSelected()) { + controller.navigator.setVisible(true); + splitPane.getItems().add(0, controller.navigator); + splitPane.setDividerPosition(0, previousDividerPosition[0]); + } + else { + controller.navigator.setVisible(false); + previousDividerPosition[0] = splitPane.getDividerPositions()[0]; + splitPane.getItems().remove(controller.navigator); + splitPane.setDividerPosition(0, 0.0); + } + }); + + phoebusApplicationToolbar.getItems().add(0, navigatorButton); + + DockPane.getActiveDockPane().deferUntilInScene(scene -> { + var window = scene.getWindow(); + window.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> { + if (keyEvent.isControlDown() && keyEvent.isShiftDown()) { + var keyCode = keyEvent.getCode(); + if (keyCode == KeyCode.N) { + Node focusOwner = scene.getFocusOwner(); + navigatorButton.fire(); + focusOwner.requestFocus(); + keyEvent.consume(); + } + else if (keyCode == KeyCode.M) { + if (!controller.navigator.isVisible()) { + navigatorButton.fire(); + } + controller.treeView.requestFocus(); + keyEvent.consume(); + } + else if (keyCode == KeyCode.LEFT) { + if (controller.navigator.isVisible()) { + var currentPosition = splitPane.getDividerPositions()[0]; + splitPane.setDividerPosition(0, Math.max(0.0, currentPosition - 0.02)); + keyEvent.consume(); + } + } + else if (keyCode == KeyCode.RIGHT) { + if (controller.navigator.isVisible()) { + var currentPosition = splitPane.getDividerPositions()[0]; + splitPane.setDividerPosition(0, Math.min(1.0, currentPosition + 0.02)); + keyEvent.consume(); + } + } + } + }); + + controller.navigator.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> { + if (keyEvent.isControlDown()) { + var keyCode = keyEvent.getCode(); + if (keyCode == KeyCode.S) { + if (controller.unsavedChanges) { + controller.saveNavigatorAction(null); + } + keyEvent.consume(); + } + } + }); + }); + } + + @Override + public AppDescriptor getAppDescriptor() { + return navigator; + } + + @Override + public boolean isTransient() { + return AppInstance.super.isTransient(); + } + + @Override + public void restore(Memento memento) { + AppInstance.super.restore(memento); + } + + @Override + public void save(Memento memento) { + AppInstance.super.save(memento); + } + + @Override + public Optional getPositionAndSizeHint() { + return AppInstance.super.getPositionAndSizeHint(); + } +} diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorSelectionTreeNode.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorSelectionTreeNode.java new file mode 100644 index 0000000000..31b32e8ff4 --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorSelectionTreeNode.java @@ -0,0 +1,34 @@ +package org.phoebus.applications.display.navigator; + +import java.io.File; + +public class NavigatorSelectionTreeNode { + + private String label; + + public String getLabel() { + return label; + } + public void setLabel(String newLabel) { + label = newLabel; + } + + private Runnable action; + + public Runnable getAction() { + return action; + } + + private File file; + public File getFile() { + return file; + } + + public NavigatorSelectionTreeNode(String label, + Runnable action, + File file) { + this.label = label; + this.action = action; + this.file = file; + } +} \ No newline at end of file diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorTreeNode.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorTreeNode.java new file mode 100644 index 0000000000..1d33349702 --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/NavigatorTreeNode.java @@ -0,0 +1,224 @@ +package org.phoebus.applications.display.navigator; + +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.control.Tab; +import javafx.scene.image.ImageView; +import org.csstudio.display.builder.runtime.app.DisplayInfo; +import org.csstudio.display.builder.runtime.app.DisplayRuntimeApplication; +import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.csstudio.trends.databrowser3.DataBrowserApp; +import org.csstudio.trends.databrowser3.DataBrowserInstance; +import org.phoebus.framework.jobs.JobManager; +import org.phoebus.framework.macros.Macros; +import org.phoebus.ui.docking.DockItem; +import org.phoebus.ui.docking.DockItemWithInput; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.javafx.ImageCache; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.function.Consumer; + +import static org.phoebus.applications.display.navigator.NavigatorController.OPI_ROOT; + +class NavigatorTreeNode { + enum NodeType { + VirtualFolder, + TemporaryMarker, + DisplayRuntime, + DataBrowser + } + private NodeType nodeType; + public NodeType getNodeType() { + return nodeType; + } + private String label; + + public String getLabel() { + return label; + } + + public void setLabel(String newLabel) { + label = newLabel; + } + + enum Target { + CurrentTab, + NewTab, + NewTab_InBackground + } + private Consumer action; + + public Consumer getAction() { + return action; + } + + private Node icon; + + public Node getIcon() { + return icon; + } + private String relativePath; + public String getRelativePath() { + return relativePath; + } + + public static NavigatorTreeNode createVirtualFolderNode(String label) { + ImageView icon = ImageCache.getImageView(NavigatorInstance.class, "/icons/folder.png"); + Consumer action = selectionTreeNodeTreeItem -> { }; + String URI = ""; + NavigatorTreeNode navigatorTreeNode = new NavigatorTreeNode(NodeType.VirtualFolder, label, icon, URI, action); + return navigatorTreeNode; + } + + public static NavigatorTreeNode createTemporaryMarker() { + ImageView icon = null; + Consumer action = selectionTreeNodeTreeItem -> { }; + String label = ""; + String URI = ""; + NavigatorTreeNode navigatorTreeNode = new NavigatorTreeNode(NodeType.TemporaryMarker, label, icon, "", action); + return navigatorTreeNode; + } + + public static NavigatorTreeNode createDisplayRuntimeNode(String label, + String relativePath, + NavigatorController navigatorController) { + ImageView icon = ImageCache.getImageView(NavigatorInstance.class, "/icons/display.png"); + Consumer action = createLoadAction_OPI(OPI_ROOT + relativePath, navigatorController); + NavigatorTreeNode navigatorTreeNode = new NavigatorTreeNode(NodeType.DisplayRuntime, + label, + icon, + relativePath, + action); + return navigatorTreeNode; + } + + public static NavigatorTreeNode createDataBrowserNode(String label, + String relativePath, + NavigatorController navigatorController) { + ImageView icon = ImageCache.getImageView(NavigatorInstance.class, "/icons/databrowser.png"); + Consumer action = createLoadAction_DataBrowser(relativePath, navigatorController); + NavigatorTreeNode navigatorTreeNode = new NavigatorTreeNode(NodeType.DataBrowser, label, icon, relativePath, action); + return navigatorTreeNode; + } + + private static Consumer createLoadAction_DataBrowser(String relativePath, + NavigatorController navigatorController) { + Consumer loadAction = target -> { + navigatorController.disableNavigator(); + DataBrowserApp dataBrowserApp = new DataBrowserApp(); + + Runnable createDataBrowserInstance = () -> { + try { + DataBrowserInstance dataBrowserInstance = dataBrowserApp.create(); + dataBrowserInstance.loadResource(new URI("file:" + OPI_ROOT + relativePath)); + } + catch (URISyntaxException uriSyntaxException) { + throw new RuntimeException(uriSyntaxException.getMessage()); + } + }; + + openAppInstance(createDataBrowserInstance, navigatorController, target); + }; + return loadAction; + } + + private static Consumer createLoadAction_OPI(String absolutePath, + NavigatorController navigatorController) { + Consumer loadAction = target -> { + navigatorController.disableNavigator(); + var activeDockPane = DockPane.getActiveDockPane(); + var activeDockItem = (DockItem) activeDockPane.getSelectionModel().getSelectedItem(); + + if (activeDockItem == null) { + target = Target.NewTab; + } + if (target == Target.CurrentTab && activeDockItem.getApplication() instanceof DisplayRuntimeInstance) { + DisplayRuntimeInstance displayRuntimeInstance = (DisplayRuntimeInstance) activeDockItem.getApplication(); + DisplayInfo newDisplayInfo = new DisplayInfo(absolutePath, "Name", new Macros(), false); + displayRuntimeInstance.loadDisplayFile(newDisplayInfo); + navigatorController.enableNavigator(); + } + else { + Runnable createDisplayRuntimeInstance = () -> { + DisplayRuntimeApplication displayRuntimeApplication = new DisplayRuntimeApplication(); + DisplayRuntimeInstance displayRuntimeInstance = displayRuntimeApplication.create(); + displayRuntimeInstance.loadDisplayFile(new DisplayInfo(absolutePath, "Name", new Macros(), false)); + }; + + openAppInstance(createDisplayRuntimeInstance, navigatorController, target); + } + }; + return loadAction; + } + + + private static void openAppInstance(Runnable createAppInstance, + NavigatorController navigatorController, + Target target) { + DockPane activeDockPane = DockPane.getActiveDockPane(); + DockItem activeDockItem = (DockItem) activeDockPane.getSelectionModel().getSelectedItem(); + + { + ObservableList activeDockItems = activeDockPane.getTabs(); + int indexOfActiveDockItem = activeDockItems.indexOf(activeDockItem); + + JobManager.schedule("Closing", monitor -> { + boolean shouldProceed; + if (target == Target.CurrentTab && activeDockItem instanceof DockItemWithInput) { + DockItemWithInput activeDockItemWithInput = (DockItemWithInput) activeDockItem; + shouldProceed = activeDockItemWithInput.okToClose().get(); + if (shouldProceed) { + activeDockItem.prepareToClose(); + } + } + else { + shouldProceed = true; + } + + if (shouldProceed) { + Platform.runLater(() -> { + activeDockPane.setStyle("-fx-open-tab-animation: NONE; -fx-close-tab-animation: NONE;"); + createAppInstance.run(); + + int indexOfDataBrowserItem = activeDockPane.getDockItems().size() - 1; + Tab dataBrowserDockItem = activeDockItems.get(indexOfDataBrowserItem); + activeDockItems.remove(dataBrowserDockItem); + activeDockItems.add(indexOfActiveDockItem + 1, dataBrowserDockItem); + + if (target == Target.CurrentTab || target == Target.NewTab) { + activeDockPane.getSelectionModel().select(indexOfActiveDockItem + 1); + } + else if (target == Target.NewTab_InBackground) { + activeDockPane.getSelectionModel().select(indexOfActiveDockItem); + } + + if (target == Target.CurrentTab) { + activeDockItem.close(); + } + + activeDockPane.setStyle("-fx-open-tab-animation: GROW; -fx-close-tab-animation: GROW;"); + navigatorController.enableNavigator(); + }); + } + else { + navigatorController.enableNavigator(); + } + }); + } + } + + public NavigatorTreeNode(NodeType nodeType, + String label, + Node icon, + String relativePath, + Consumer action) { + this.nodeType = nodeType; + this.label = label; + this.icon = icon; + this.relativePath = relativePath; + this.action = action; + } +} diff --git a/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Preferences.java b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Preferences.java new file mode 100644 index 0000000000..bf4660ce72 --- /dev/null +++ b/app/display/navigator/src/main/java/org/phoebus/applications/display/navigator/Preferences.java @@ -0,0 +1,17 @@ +package org.phoebus.applications.display.navigator; + +import org.phoebus.framework.preferences.AnnotatedPreferences; +import org.phoebus.framework.preferences.Preference; +public class Preferences +{ + static { + AnnotatedPreferences.initialize(Preferences.class, "/navigator_preferences.properties"); + } + + @Preference + public static String navigator_root; + @Preference + public static String initial_navigator; + @Preference + public static String opi_root; +} diff --git a/app/display/navigator/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppResourceDescriptor b/app/display/navigator/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppResourceDescriptor new file mode 100644 index 0000000000..095c3010a5 --- /dev/null +++ b/app/display/navigator/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppResourceDescriptor @@ -0,0 +1 @@ +org.phoebus.applications.display.navigator.NavigatorAppResourceDescriptor diff --git a/app/display/navigator/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry b/app/display/navigator/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry new file mode 100644 index 0000000000..8401642d5a --- /dev/null +++ b/app/display/navigator/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry @@ -0,0 +1 @@ +org.phoebus.applications.display.navigator.AddMenuEntry diff --git a/app/display/navigator/src/main/resources/icons/closed_folder.png b/app/display/navigator/src/main/resources/icons/closed_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..a4ba94b760a6bf47ae4cbd9f8d548820b7fb9cab GIT binary patch literal 490 zcmVDlrc*kK@^40y))z6Clb(SmqJBrL6U!vE(8L0vGOMf z7825>7X*KRXdws|64E3nqzpEuNg+PSv(Z8&D5809ckZ=VSv51Oj0Xnx&bfSZ<}xhp zEYGx$4}OoMMnf-0R^KjsnXU8dMC)@?I{k4GE^7wnpS_>h-C6@d$386m>ZE`z6TvtO z@p)j5OuoWNXCEhr?QH-PNr^Y+U+Ys%{VxV_b@8{>A0BGq&8KhlUt-3)`Ohg~0_|f2>aG#V|azXn?uOU!_3mDr1ewFB{#eFda`^J{i6_h9@yu3Fbhj6U)w*v gzke375(SZO02))9Sc!s!>i_@%07*qoM6N<$f2BR0prEIx zi(`mIZ*sx{)q;Y8AMYPL_#i)DO3h1m;SC125=JLKyMhM{U2;6kA0(UBNiJu+!E2BR0prDVZ zi(`mIZ*qdfl>-M4KHT5je7L?QOX6du+cSBK0*2L5YnpcC98KeBV^CCoT*MjX;3Hum xn0sr1X~QFjC3Pl`j9Qkwa5ih1Fr7_~fk7!rsr`XwG|*TE22WQ%mvv4FO#uJTFuDK$ literal 0 HcmV?d00001 diff --git a/app/display/navigator/src/main/resources/icons/folder.png b/app/display/navigator/src/main/resources/icons/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..d76ca852c42818d4173a7e424dff3962e5b0910d GIT binary patch literal 595 zcmV-Z0<8UsP)DlS^n*K@^7ndE8((MM@2%QreK#M10U9x^mZw1O&x} zP&alZ?i3V31fd&2uvl?zv4VoqMGHklDgmjW78~E(dyO_K7}F-07-MpuBf04nAx4_{ zS2O1fA3y&&Lmpl`n##O?8$lCwZ0?zgpPe0T)^8?q`_!N~d~O`dhPH+$%gHBrHgmIf z43At{O1A>$ln^k5!*#5NX3x%DSWRnK{gj#q(AN@}Pzu*>g!vl*jQ2=(uc!zl3lc zg;I6{{<_)KYP4(HASfr&j&rHk7Gv~Yd@ND$z=ROkMwNAj`=s3(r6LMnJ}Qi|8NbGv z5&+>G9o^m1+mK(@YG1!RbpE~2BR0pkTJA zi(`mI@6upTp#usW!g0qge>SczY75%NrG7?5NZ`7clgqY_H$Bc1FC1p9`JwN?&3M7` zwau;*TW=rSvFS$UV$(HK{=4UxKI!cDwLP*zAo_upn(hYHb=*}6U#uR=mBd7xtC)Ix f{>m-UTfZ_mER5dzqHf6nptTI1u6{1-oD!M<$$&)i literal 0 HcmV?d00001 diff --git a/app/display/navigator/src/main/resources/icons/menu.png b/app/display/navigator/src/main/resources/icons/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a88e140e7dff97f2d8ea0abf0c67c6c0f33357 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9F5uR)lx@NDB$prE#= zi(`mIZ*qb}#(@(D4jg#!;K7IaGH2QOWD-&qt*dTRk>GXFYPjf-lEA>Q`xOrt_Zse_ PKy3`3u6{1-oD!ManMpkSh> zi(`mI@72kUTn7{cn3caIR#w%e>2s|;m3OOq#^MdhGNKuZksp<>@7TcPsU^06e{v1q zZ(bGANv6_KX=~nD&6G(=c<@$*#lp|2;jxOXQ%S2>!u;la-GBAY{pA+^ncXa1Smmtk TClI^{Xc>d2tDnm{r-UW|ABH(R literal 0 HcmV?d00001 diff --git a/app/display/navigator/src/main/resources/navigator_preferences.properties b/app/display/navigator/src/main/resources/navigator_preferences.properties new file mode 100644 index 0000000000..cfbbddb756 --- /dev/null +++ b/app/display/navigator/src/main/resources/navigator_preferences.properties @@ -0,0 +1,14 @@ +# -------------------------------------------------- +# Package org.phoebus.applications.display.navigator +# -------------------------------------------------- + +# Absolute path to the location of the navigators +navigator_root= + +# Relative path (relative to 'navigator_root') to the location of the navigator +# to be loaded when the navigator starts. If empty, then the "first" navigator +# in 'navigator_root' is loaded. +initial_navigator= + +# Absolute path to the root of the opis +opi_root= diff --git a/app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/messages.properties b/app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/messages.properties new file mode 100644 index 0000000000..433eea23ba --- /dev/null +++ b/app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/messages.properties @@ -0,0 +1,29 @@ +CollapseAll=Collapse All +CreateNewFolder=Create New Folder +CreateNewNavigator=Create New Navigator... +CreateNewSubFolder=Create New Sub-Folder... +DeleteItem=Delete Item +DeletePrompt=Delete +ErrorCreatingNewFolderWarning=An error occurred when attempting to create the new folder. +ErrorCreatingNewNavigatorFileWarning=An error occurred when attempting to create the new navigator file. +ErrorLoadingTheNavigatorWarning=Error loading the navigator +ErrorRenamingFolderWarning=An error occurred when attempting to rename the folder. +ExpandAll=Expand All +FileIsNotInTheNavigatorDataDirectoryWarning=File is not in the navigator data directory: +FileNotFoundWarning=File not found: +GenericDataBrowserName= +LocateCurrentFile=Locate Current File +NavigatorMenu=Navigator Menu +NavigatorTooltip=Navigator +NewFolderNamePrompt=New Folder Name: +NewNavigatorDefaultName=New Navigator +NewNavigatorNamePrompt=New Navigator Name: +OpenInBackgroundTab=Open in Background Tab +OpenInNewTab=Open in New Tab +RenameFolder=Rename Folder +RenameFolderPrompt=Rename Folder to: +RenameNavigator=Rename Navigator +RenameParentFolderPrompt=New Name: +TheSpecifiedInitialNavigatorDoesntExist=The specified initial navigator doesn't exist: +UnknownFileExtensionWarning=Unknown file extension: +UnknownNodeTypeWarning=Unknown node type: diff --git a/app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/ui/Navigator.fxml b/app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/ui/Navigator.fxml new file mode 100644 index 0000000000..4bd87b8cbd --- /dev/null +++ b/app/display/navigator/src/main/resources/org/phoebus/applications/display/navigator/ui/Navigator.fxml @@ -0,0 +1,51 @@ + + + + + + + +