From ffe810a8b56103ca4fb6eb9b9cf1e24fdc92a909 Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Thu, 27 Jul 2023 14:22:23 +0200 Subject: [PATCH 01/14] CSSTUDIO-1987 Remove unused code. --- .../runtime/app/DockItemRepresentation.java | 20 ------------------- .../builder/runtime/script/ScriptUtil.java | 18 ----------------- 2 files changed, 38 deletions(-) diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java index 5a13862a88..fc23c0dce4 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DockItemRepresentation.java @@ -141,24 +141,4 @@ public void representModel(final Parent model_parent, final DisplayModel model) app_instance.trackCurrentModel(model); super.representModel(model_parent, model); } - - @Override - public void closeWindow(final DisplayModel model) throws Exception - { - // Is called from ScriptUtil, i.e. scripts, from background thread - final Parent model_parent = Objects.requireNonNull(model.getUserData(Widget.USER_DATA_TOOLKIT_PARENT)); - if (model_parent.getProperties().get(DisplayRuntimeInstance.MODEL_PARENT_DISPLAY_RUNTIME) == app_instance) - { - // Prepare-to-close, which might take time and must be called off the UI thread - final DisplayRuntimeInstance instance = (DisplayRuntimeInstance) app_instance.getRepresentation().getModelParent().getProperties().get(DisplayRuntimeInstance.MODEL_PARENT_DISPLAY_RUNTIME); - if (instance != null) - instance.getDockItem().prepareToClose(); - else - logger.log(Level.SEVERE, "Missing DisplayRuntimeInstance to prepare closing", new Exception("Stack Trace")); - // 'close' on the UI thread - execute(() -> app_instance.close()); - } - else - throw new Exception("Wrong model"); - } } diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java index 7f6b4b2a60..600fff19bd 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/script/ScriptUtil.java @@ -141,24 +141,6 @@ public static void openDisplay(final Widget widget, final String file, final Str ActionUtil.handleAction(widget, open); } - /** Close a display - * - * @param widget Widget within the display to close - */ - public static void closeDisplay(final Widget widget) - { - try - { - final DisplayModel model = widget.getTopDisplayModel(); - final ToolkitRepresentation toolkit = ToolkitRepresentation.getToolkit(model); - toolkit.closeWindow(model); - } - catch (Throwable ex) - { - logger.log(Level.WARNING, "Cannot close display", ex); - } - } - // ==================== // public alert dialog utils From af5b4fa8f722c96faca24cee7ba36144e596bb6e Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Thu, 27 Jul 2023 15:49:18 +0200 Subject: [PATCH 02/14] CSSTUDIO-1987 Make DockItemWithInput.okToClose() "public". --- .../src/main/java/org/phoebus/ui/docking/DockItemWithInput.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java index 2b8c7d05a5..fd99eeabff 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java @@ -209,7 +209,7 @@ public boolean isSaveAsSupported() /** Called when user tries to close the tab * @return Should the tab close? Otherwise it stays open. */ - private Future okToClose() + public Future okToClose() { if (! isDirty()) return CompletableFuture.completedFuture(true); From afdb5cb19b9ef7f08c976efee1487670b907bdc6 Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Thu, 27 Jul 2023 15:51:24 +0200 Subject: [PATCH 03/14] CSSTUDIO-1987 Improve Messages.DockAlertMsg. --- .../src/main/java/org/phoebus/ui/docking/DockItemWithInput.java | 2 +- .../resources/org/phoebus/ui/application/messages.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java index fd99eeabff..c8576092c5 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java @@ -215,7 +215,7 @@ public Future okToClose() return CompletableFuture.completedFuture(true); final FutureTask promptToSave = new FutureTask(() -> { - final String text = MessageFormat.format(Messages.DockAlertMsg, getLabel()); + final String text = MessageFormat.format(Messages.DockAlertMsg, getApplication().getAppDescriptor().getDisplayName(), getLabel()); final Alert prompt = new Alert(AlertType.NONE, text, ButtonType.NO, ButtonType.CANCEL, ButtonType.YES); diff --git a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties index 61d4ef4bd5..0d7d8893c8 100644 --- a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties +++ b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties @@ -9,7 +9,7 @@ CloseAllTabs=Close All Tabs DeleteLayouts=Delete Layouts... DeleteLayoutsConfirmFmt=Delete {0} selected layouts? DeleteLayoutsInfo=Select layouts to delete -DockAlertMsg=The {0} has been modified.\n\nSave before closing? +DockAlertMsg=The {0} instance \"{1}\" has been modified.\n\nSave before closing? DockAlertTitle=Save File DockAll=All DockAppName=Application Name: From b9fbcdef5e077bca8203e55d38f3bfb17996ee45 Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Wed, 2 Aug 2023 14:22:01 +0200 Subject: [PATCH 04/14] CSSTUDIO-1987 New unsaved changes confirmation window when closing tabs or windows. --- .../editor/app/DisplayEditorInstance.java | 1 - .../runtime/app/DisplayRuntimeInstance.java | 21 +- .../ui/application/PhoebusApplication.java | 461 ++++++++++++++++-- .../java/org/phoebus/ui/docking/DockItem.java | 70 ++- .../phoebus/ui/docking/DockItemWithInput.java | 2 - .../java/org/phoebus/ui/docking/DockPane.java | 8 +- .../org/phoebus/ui/docking/DockStage.java | 15 +- 7 files changed, 485 insertions(+), 93 deletions(-) diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java index d25333644e..90b108f426 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java @@ -121,7 +121,6 @@ public class DisplayEditorInstance implements AppInstance menu_node.setOnContextMenuRequested(event -> handleContextMenu(menu, event)); menu_node.setContextMenu(menu); - dock_item.addCloseCheck(this::okToClose); dock_item.addClosedNotification(this::dispose); } diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java index dcf60af364..d13f2b9d41 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java @@ -19,6 +19,7 @@ import java.util.concurrent.TimeoutException; import java.util.logging.Level; +import javafx.stage.Window; import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.Preferences; import org.csstudio.display.builder.model.Widget; @@ -33,6 +34,7 @@ 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.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.docking.DockItem; import org.phoebus.ui.docking.DockItemWithInput; @@ -335,15 +337,18 @@ public void loadDisplayFile(final DisplayInfo info) display_info = Optional.empty(); - if (dock_item.prepareToClose()) - Platform.runLater(() -> - { - final Parent parent = representation.getModelParent(); - JFXRepresentation.getChildren(parent).clear(); + boolean shouldClose = dock_item.okToClose().get(); - close(); - }); - return; + if (shouldClose) { + dock_item.prepareToClose(); + Platform.runLater(() -> + { + final Parent parent = representation.getModelParent(); + JFXRepresentation.getChildren(parent).clear(); + + close(); + }); + } } }); } 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 ed20e97dca..6f403bcbf0 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 @@ -5,14 +5,48 @@ import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import javafx.event.ActionEvent; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Dialog; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.ToolBar; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.jobs.JobMonitor; import org.phoebus.framework.jobs.SubJobMonitor; @@ -48,30 +82,18 @@ import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Node; -import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; -import javafx.scene.control.CheckMenuItem; -import javafx.scene.control.Dialog; -import javafx.scene.control.Menu; -import javafx.scene.control.MenuBar; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.ToolBar; -import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.stage.Window; +import javax.tools.Tool; + /** * Primary UI for a phoebus application * @@ -1054,31 +1076,41 @@ private void replaceLayout(final MementoTree memento) { JobManager.schedule("Close all stages", monitor -> { - for (Stage stage : stages) - if (!DockStage.prepareToCloseItems(stage)) - return; + boolean shouldExit = confirmationDialogWhenUnsavedChangesExist(stages, + "Would you like to save any changes before replacing the layout?", + "replace", + main_stage, + monitor); - // All stages OK to close - Platform.runLater(() -> - { + if (shouldExit) { for (Stage stage : stages) { - DockStage.closeItems(stage); - // Don't wait for Platform.runLater-based tab handlers - // that will merge splits and eventually close the empty panes, - // but close all non-main stages right away - if (stage != main_stage) - stage.close(); + if (!DockStage.prepareToCloseItems(stage)) { + return; + } } - // Go into the main stage and close all of the tabs. If any of them refuse, return. - final Node node = DockStage.getPaneOrSplit(main_stage); - if (!MementoHelper.closePaneOrSplit(node)) - return; + // All stages OK to close + Platform.runLater(() -> + { + for (Stage stage : stages) { + DockStage.closeItems(stage); + // Don't wait for Platform.runLater-based tab handlers + // that will merge splits and eventually close the empty panes, + // but close all non-main stages right away + if (stage != main_stage) + stage.close(); + } + + // Go into the main stage and close all of the tabs. If any of them refuse, return. + final Node node = DockStage.getPaneOrSplit(main_stage); + if (!MementoHelper.closePaneOrSplit(node)) + return; - // Allow handlers for tab changes etc. to run as everything closed. - // On next UI tick, load content from memento file. - Platform.runLater(() -> restoreState(memento)); - }); + // Allow handlers for tab changes etc. to run as everything closed. + // On next UI tick, load content from memento file. + Platform.runLater(() -> restoreState(memento)); + }); + } }); } @@ -1263,22 +1295,340 @@ private void closeMainStage() { JobManager.schedule("Close all stages", monitor -> { - // closeStages() - for (Stage stage : stages) - if (!DockStage.prepareToCloseItems(stage)) - return; + boolean shouldExit = confirmationDialogWhenUnsavedChangesExist(stages, + "Would you like to save any changes before exiting?", + "exit", + main_stage, + monitor); - // All stages OK to close - Platform.runLater(() -> - { + if (shouldExit) { for (Stage stage : stages) - DockStage.closeItems(stage); + if (!DockStage.prepareToCloseItems(stage)) { + return; + } - stop(); - }); + exitPhoebus(); + } }); } + private static SortedMap> listOfDockItems2ApplicationNameToDockItemsWithInput(List dockItems) { + SortedMap> applicationNameToDockItemsWithInput = new TreeMap<>(); + for (DockItem dockItem : dockItems) { + if (dockItem instanceof DockItemWithInput) { + DockItemWithInput dockItemWithInput = (DockItemWithInput) dockItem; + if (dockItemWithInput.isDirty()) { + String applicationName = dockItemWithInput.getApplication().getAppDescriptor().getDisplayName(); + if (!applicationNameToDockItemsWithInput.containsKey(applicationName)) { + applicationNameToDockItemsWithInput.put(applicationName, new LinkedList<>()); + } + + applicationNameToDockItemsWithInput.get(applicationName).add(dockItemWithInput); + } + } + } + return applicationNameToDockItemsWithInput; + } + + private static SortedMap>> stages2WindowNameToApplicationNameToDockItemsWithInput(List stages) { + SortedMap>> windowNameToApplicationNameToDockItemsWithInput = new TreeMap<>(); + { + int currentWindowNr = 1; + + for (Stage stage : stages) { + String currentWindowName; + if (stage == DockStage.getDockStages().get(0)) { + currentWindowName = "Main window"; + } + else { + currentWindowName = "Secondary window " + currentWindowNr; + currentWindowNr++; + } + + List dockItems = DockStage.getDockPanes(stage).stream().flatMap(dockPane -> dockPane.getDockItems().stream()).collect(Collectors.toList()); + SortedMap> applicationNameToDockItemsWithInput = PhoebusApplication.listOfDockItems2ApplicationNameToDockItemsWithInput(dockItems); + windowNameToApplicationNameToDockItemsWithInput.put(currentWindowName, applicationNameToDockItemsWithInput); + } + } + return windowNameToApplicationNameToDockItemsWithInput; + } + + public static boolean confirmationDialogWhenUnsavedChangesExist(Stage stage, + String question, + String closeActionName, + JobMonitor monitor) throws ExecutionException, InterruptedException { + List dockItems = DockStage.getDockPanes(stage).stream().flatMap(dockPane -> dockPane.getDockItems().stream()).collect(Collectors.toList()); + SortedMap> applicationNameToDockItemsWithInput = PhoebusApplication.listOfDockItems2ApplicationNameToDockItemsWithInput(dockItems); + SortedMap>> windowNameToApplicationNameToDockItemsWithInput = new TreeMap<>(); + windowNameToApplicationNameToDockItemsWithInput.put("Window", applicationNameToDockItemsWithInput); + + return confirmationDialogWhenUnsavedChangesExist(windowNameToApplicationNameToDockItemsWithInput, + question, + closeActionName, + stage, + monitor); + } + + public static boolean confirmationDialogWhenUnsavedChangesExist(ArrayList dockItems, // "ArrayList" is used instead of "List" to prevent a conflict with "List" after type erasure. + String question, + String closeActionName, + Stage stage, + JobMonitor monitor) throws ExecutionException, InterruptedException { + SortedMap> applicationNameToDockItemsWithInput = PhoebusApplication.listOfDockItems2ApplicationNameToDockItemsWithInput(dockItems); + SortedMap>> windowNameToApplicationNameToDockItemsWithInput = new TreeMap<>(); + windowNameToApplicationNameToDockItemsWithInput.put("Window", applicationNameToDockItemsWithInput); + + return confirmationDialogWhenUnsavedChangesExist(windowNameToApplicationNameToDockItemsWithInput, + question, + closeActionName, + stage, + monitor); + } + + public static boolean confirmationDialogWhenUnsavedChangesExist(List stages, + String question, + String closeActionName, + Stage stage, + JobMonitor monitor) throws ExecutionException, InterruptedException { + return confirmationDialogWhenUnsavedChangesExist(stages2WindowNameToApplicationNameToDockItemsWithInput(stages), + question, + closeActionName, + stage, + monitor); + } + + public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap>> windowNrToApplicationNameToDockItemsWithInput, + String question, + String closeActionName, + Stage stage, + JobMonitor monitor) throws ExecutionException, InterruptedException { + + if (windowNrToApplicationNameToDockItemsWithInput.isEmpty() || windowNrToApplicationNameToDockItemsWithInput.values().stream().allMatch(sortedMap -> sortedMap.values().stream().allMatch(Collection::isEmpty))) { + // No unsaved changes. + return true; + } + + Stage stageToPositionTheConfirmationDialogOver; + if (stage != null) { + stageToPositionTheConfirmationDialogOver = stage; + } + else { + stageToPositionTheConfirmationDialogOver = INSTANCE.main_stage; + } + + ButtonType clearSelectionOfCheckboxes = new ButtonType("Clear"); + ButtonType selectAllCheckboxes = new ButtonType("Select all"); + ButtonType saveSelectedItems = new ButtonType("Save"); + ButtonType exitPhoebusWithoutSavingUnsavedChanges = new ButtonType("Discard & " + closeActionName); + + FutureTask displayConfirmationWindow = new FutureTask(() -> { + Alert prompt = new Alert(AlertType.CONFIRMATION); + + prompt.getDialogPane().getButtonTypes().remove(ButtonType.OK); + ((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(clearSelectionOfCheckboxes); + prompt.getDialogPane().getButtonTypes().add(selectAllCheckboxes); + prompt.getDialogPane().getButtonTypes().add(saveSelectedItems); + prompt.getDialogPane().getButtonTypes().add(exitPhoebusWithoutSavingUnsavedChanges); + + Button cancel_button = (Button) prompt.getDialogPane().lookupButton(ButtonType.CANCEL); + cancel_button.setTooltip(new Tooltip(cancel_button.getText())); + + Button clearSelectionOfCheckboxes_button = (Button) prompt.getDialogPane().lookupButton(clearSelectionOfCheckboxes); + clearSelectionOfCheckboxes_button.setTooltip(new Tooltip(clearSelectionOfCheckboxes_button.getText())); + + Button selectAllCheckboxes_button = (Button) prompt.getDialogPane().lookupButton(selectAllCheckboxes); + selectAllCheckboxes_button.setTooltip(new Tooltip(selectAllCheckboxes_button.getText())); + + Button saveSelectedItems_button = (Button) prompt.getDialogPane().lookupButton(saveSelectedItems); + saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); + + Button exitPhoebusWithoutSavingUnsavedChanges_button = (Button) prompt.getDialogPane().lookupButton(exitPhoebusWithoutSavingUnsavedChanges); + exitPhoebusWithoutSavingUnsavedChanges_button.setTooltip(new Tooltip(exitPhoebusWithoutSavingUnsavedChanges_button.getText())); + List> setCheckBoxStatusActions = new LinkedList<>(); + List> getCheckBoxStatusActions = new LinkedList<>(); + List> saveActions = new LinkedList<>(); + + Runnable enableAndDisableButtons = () -> { + if (getCheckBoxStatusActions.stream().anyMatch(getCheckBoxStatus -> getCheckBoxStatus.get())) { + clearSelectionOfCheckboxes_button.setDisable(false); + saveSelectedItems_button.setDisable(false); + exitPhoebusWithoutSavingUnsavedChanges_button.setDisable(true); + } + else { + clearSelectionOfCheckboxes_button.setDisable(true); + saveSelectedItems_button.setDisable(true); + exitPhoebusWithoutSavingUnsavedChanges_button.setDisable(false); + } + + if (getCheckBoxStatusActions.stream().allMatch(getCheckBoxStatus -> getCheckBoxStatus.get())) { + selectAllCheckboxes_button.setDisable(true); + saveSelectedItems_button.setText("Save & " + closeActionName); + saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); + } + else { + selectAllCheckboxes_button.setDisable(false); + saveSelectedItems_button.setText("Save"); + saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); + } + }; + + GridPane gridPane = new GridPane(); + gridPane.setVgap(4); + int currentRow = 0; + for (String windowName : windowNrToApplicationNameToDockItemsWithInput.keySet()) { + var applicationNameToDockItemsWithInput = windowNrToApplicationNameToDockItemsWithInput.get(windowName); + + if (applicationNameToDockItemsWithInput.size() > 0) { // Only print unsaved changes for a window if it actually containts any unsaved changes. + if (windowNrToApplicationNameToDockItemsWithInput.size() >= 2) { // Only print the window names if two or more windows are in the process of being closed. + Text windowTitle = new Text(windowName); + windowTitle.setStyle("-fx-font-size: 16; -fx-font-weight: bold"); + gridPane.add(windowTitle, 0, currentRow); + currentRow++; + } + + for (var applicationName : applicationNameToDockItemsWithInput.keySet()) { + for (var dockItemWithInput : applicationNameToDockItemsWithInput.get(applicationName)) { + CheckBox checkBox = new CheckBox(); + checkBox.selectedProperty().addListener((observableValue, old_value, new_value) -> enableAndDisableButtons.run()); + + Text applicationName_text = new Text(applicationName + ":"); + applicationName_text.setStyle("-fx-font-weight: bold"); + Text instanceName_text = new Text(dockItemWithInput.getLabel()); + + HBox hBox = new HBox(checkBox, applicationName_text, instanceName_text); + hBox.setSpacing(4); + gridPane.add(hBox, 0, currentRow); + + Consumer setCheckboxStatus = bool -> checkBox.setSelected(bool); + setCheckBoxStatusActions.add(setCheckboxStatus); + + Supplier getCheckBoxStatus = () -> checkBox.isSelected(); + getCheckBoxStatusActions.add(getCheckBoxStatus); + + hBox.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseEvent -> checkBox.setSelected(!checkBox.isSelected())); // Enable toggling checkbox by clicking on its label. + + Supplier actionSaveIfCheckboxEnabled = () -> { + if (checkBox.isSelected()) { + + Text saving = new Text("[Saving...]"); + saving.setFill(Color.ORANGE); + saving.setStyle("-fx-font-weight: bold;"); + hBox.getChildren().set(0, saving); + boolean saveSuccessful = dockItemWithInput.save(monitor); + + if (saveSuccessful) { + // The functions setCheckboxStatus() and getCheckBoxStatus should not be available anymore: + setCheckBoxStatusActions.remove(setCheckboxStatus); + getCheckBoxStatusActions.remove(getCheckBoxStatus); + setCheckboxStatus.accept(false); + + Text saved = new Text("[Saved]"); + saved.setFill(Color.GREEN); + saved.setStyle("-fx-font-weight: bold;"); + hBox.getChildren().set(0, saved); + return true; + } + else { + Text savingFailed_text = new Text("[Saving failed]"); + savingFailed_text.setFill(Color.RED); + savingFailed_text.setStyle("-fx-font-weight: bold;"); + + HBox savingFailed = new HBox(checkBox, savingFailed_text); + savingFailed.setSpacing(6); + hBox.getChildren().set(0, savingFailed); + return false; + } + } + else { + return false; + } + }; + saveActions.add(actionSaveIfCheckboxEnabled); + + currentRow++; + } + } + } + } + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setContent(gridPane); + + prompt.getDialogPane().setContent(scrollPane); + + clearSelectionOfCheckboxes_button.addEventFilter(ActionEvent.ACTION, event -> { + event.consume(); + + setCheckBoxStatusActions.forEach(setCheckboxAction -> { + setCheckboxAction.accept(false); + }); + }); + + selectAllCheckboxes_button.addEventFilter(ActionEvent.ACTION, event -> { + event.consume(); + + setCheckBoxStatusActions.forEach(setCheckboxAction -> { + setCheckboxAction.accept(true); + }); + }); + + saveSelectedItems_button.addEventFilter(ActionEvent.ACTION, event -> { + event.consume(); + + List> saveActionsThatHaveBeenCompleted = new LinkedList<>(); + for (var saveAction : saveActions) { + boolean result = saveAction.get(); + if (result) { + saveActionsThatHaveBeenCompleted.add(saveAction); + } + } + + for (var saveActionThatHasBeenCompleted : saveActionsThatHaveBeenCompleted) { + saveActions.remove(saveActionThatHasBeenCompleted); + } + + if (saveActions.size() == 0) { + exitPhoebusWithoutSavingUnsavedChanges_button.fire(); + } + }); + + // Initialize state of buttons: + enableAndDisableButtons.run(); + + prompt.setHeaderText("The following application instances have unsaved changes. " + question); + prompt.setTitle("Unsaved changes"); + + int prefWidth = 750; + int prefHeight = 400; + prompt.getDialogPane().setPrefSize(prefWidth, prefHeight); + prompt.setResizable(false); + + DialogHelper.positionDialog(prompt, stageToPositionTheConfirmationDialogOver.getScene().getRoot(), -prefWidth/2, -prefHeight/2); + + return prompt.showAndWait().orElse(ButtonType.CANCEL) == exitPhoebusWithoutSavingUnsavedChanges ? true : false; + }); + + if (!windowNrToApplicationNameToDockItemsWithInput.isEmpty()) { // Only show confirmation window if there are any items with unsaved changes. + Platform.runLater(displayConfirmationWindow); + boolean shouldClose = (boolean) displayConfirmationWindow.get(); + return shouldClose; + } + else { + return true; + } + } + + private void exitPhoebus() { + Platform.runLater(() -> + { + for (Stage stage : DockStage.getDockStages()) { + DockStage.closeItems(stage); + } + stop(); + }); + }; + /** * Start all applications * @@ -1328,16 +1678,21 @@ public static void closeAllTabs(){ final List stages = DockStage.getDockStages(); JobManager.schedule("Close All Tabs", monitor -> { - for (Stage stage : stages){ - if (!DockStage.prepareToCloseItems(stage)){ - return; + boolean shouldCloseTabs = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(stages, + "Would you like to save any changes before closing all tabs?", + "close", + PhoebusApplication.INSTANCE.main_stage, + monitor); + + if (shouldCloseTabs) { + for (Stage stage : stages){ + if (!DockStage.prepareToCloseItems(stage)){ + return; + } } - } - Platform.runLater(() -> - { - stages.forEach(stage -> DockStage.closeItems(stage)); - }); + Platform.runLater(() -> stages.forEach(stage -> DockStage.closeItems(stage))); + } }); } } 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 cdca334c52..f1bb6cd6e4 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 @@ -24,11 +24,13 @@ import java.util.logging.Level; import java.util.stream.Collectors; +import javafx.event.Event; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.spi.AppDescriptor; import org.phoebus.framework.spi.AppInstance; import org.phoebus.security.authorization.AuthorizationService; import org.phoebus.ui.application.Messages; +import org.phoebus.ui.application.PhoebusApplication; import org.phoebus.ui.application.SaveLayoutHelper; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; @@ -175,9 +177,27 @@ public DockItem(final String label) createContextMenu(); + setOnCloseRequest(event -> handleCloseRequest(event)); setOnClosed(event -> handleClosed()); } + private void handleCloseRequest(Event event) { + // For now, prevent closing + event.consume(); + + // Invoke all the ok-to-close checks in background threads + // since those that save files might take time. + JobManager.schedule("Close " + getLabel(), monitor -> + { + boolean shouldClose = this instanceof DockItemWithInput ? ((DockItemWithInput) this).okToClose().get() : true; + + if (shouldClose) { + prepareToClose(); + Platform.runLater(() -> close()); + } + }); + } + /** This tab should be in a DockPane, not a plain TabPane * @return DockPane that holds this tab */ @@ -219,14 +239,16 @@ else if (stagesContainingActiveDockPane.size() == 0) { }); final MenuItem close = new MenuItem(Messages.DockClose, new ImageView(DockPane.close_icon)); - close.setOnAction(event -> close(List.of(this))); + ArrayList arrayList = new ArrayList(); + arrayList.add(this); + close.setOnAction(event -> close(arrayList)); final MenuItem close_other = new MenuItem(Messages.DockCloseOthers, new ImageView(close_many_icon)); close_other.setOnAction(event -> { // Close all other tabs in non-fixed panes of this window final Stage stage = (Stage) getDockPane().getScene().getWindow(); - final List tabs = new ArrayList<>(); + final ArrayList tabs = new ArrayList<>(); for (DockPane pane : getDockPanes(stage)) if (! pane.isFixed()) for (Tab tab : new ArrayList<>(pane.getTabs())) @@ -240,7 +262,7 @@ else if (stagesContainingActiveDockPane.size() == 0) { { // Close all tabs in non-fixed panes of this window final Stage stage = (Stage) getDockPane().getScene().getWindow(); - final List tabs = new ArrayList<>(); + final ArrayList tabs = new ArrayList<>(); for (DockPane pane : getDockPanes(stage)) if (! pane.isFixed()) for (Tab tab : new ArrayList<>(pane.getTabs())) @@ -285,19 +307,28 @@ else if (stagesContainingActiveDockPane.size() == 0) { } /** @param tabs Tabs to prepare and then close */ - private static void close(final List tabs) + private void close(final ArrayList tabs) { JobManager.schedule("Close", monitor -> { - for (DockItem tab : tabs) - if (! tab.prepareToClose()) - return; - - Platform.runLater(() -> - { + Window window = getDockPane().getScene().getWindow(); + boolean shouldCloseTabs = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(tabs, + "Would you like to save any changes before closing the tabs?", + "close", + window instanceof Stage ? (Stage) window : null, + monitor); + + if (shouldCloseTabs) { for (DockItem tab : tabs) - tab.close(); - }); + if (! tab.prepareToClose()) + return; + + Platform.runLater(() -> + { + for (DockItem tab : tabs) + tab.close(); + }); + } }); } @@ -593,21 +624,6 @@ public void select() */ public void addCloseCheck(final Supplier> ok_to_close) { - if (getOnCloseRequest() == null) - setOnCloseRequest(event -> - { - // For now, prevent closing - event.consume(); - - // Invoke all the ok-to-close checks in background threads - // since those that save files might take time. - JobManager.schedule("Close " + getLabel(), monitor -> - { - if (prepareToClose()) - Platform.runLater(() -> close()); - }); - }); - close_check.add(ok_to_close); } diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java index c8576092c5..c9930a331d 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java @@ -103,8 +103,6 @@ public DockItemWithInput(final AppInstance application, final Node content, fina this.file_extensions = file_extensions; this.save_handler = save_handler; setInput(input); - - addCloseCheck(this::okToClose); } // Override to include 'dirty' tab 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 e08435de32..0a5a207497 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 @@ -18,8 +18,10 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.stage.Window; import org.phoebus.framework.jobs.JobManager; import org.phoebus.ui.application.Messages; +import org.phoebus.ui.application.PhoebusApplication; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.ui.javafx.Styles; @@ -343,8 +345,12 @@ else if (key == KeyCode.W) { JobManager.schedule("Close " + item.getLabel(), monitor -> { - if (item.prepareToClose()) + boolean shouldClose = item instanceof DockItemWithInput ? ((DockItemWithInput) item).okToClose().get() : true; + + if (shouldClose) { + item.prepareToClose(); Platform.runLater(item::close); + } }); } event.consume(); 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 b4a1efb195..2d57b5017b 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 @@ -16,8 +16,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.UUID; import java.util.logging.Level; +import java.util.stream.Collectors; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.workbench.Locations; @@ -191,8 +194,18 @@ else if(layout.getChildren().get(0) instanceof SplitPane){ // and on success close them JobManager.schedule("Close " + stage.getTitle(), monitor -> { - if (prepareToCloseItems(stage)) + boolean shouldCloseStage = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(stage, + "Would you like to save any changes before closing the window?", + "close", + monitor); + + if (shouldCloseStage) { + if (!DockStage.prepareToCloseItems(stage)) { + return; + } + Platform.runLater(() -> closeItems(stage)); + } }); }); From d7fbfd32d913a5ff0562a35bff7f2c3cc1642a9b Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Wed, 2 Aug 2023 16:21:21 +0200 Subject: [PATCH 05/14] CSSTUDIO-1987 Add UI text to messages.properties. --- .../org/phoebus/ui/application/Messages.java | 20 ++++++++++ .../ui/application/PhoebusApplication.java | 40 +++++++++---------- .../java/org/phoebus/ui/docking/DockItem.java | 4 +- .../org/phoebus/ui/docking/DockStage.java | 4 +- .../ui/application/messages.properties | 20 ++++++++++ 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java index 8708f628ea..9f01d78745 100644 --- a/core/ui/src/main/java/org/phoebus/ui/application/Messages.java +++ b/core/ui/src/main/java/org/phoebus/ui/application/Messages.java @@ -146,6 +146,26 @@ public class Messages public static String TimeYear; public static String TopResources; public static String UnLockPane; + public static String UnsavedChanges; + public static String UnsavedChanges_clearButtonText; + public static String UnsavedChanges_close; + public static String UnsavedChanges_discardButtonText_discardAnd; + public static String UnsavedChanges_exit; + public static String UnsavedChanges_mainWindow; + public static String UnsavedChanges_replace; + public static String UnsavedChanges_saveButtonText; + public static String UnsavedChanges_saveButtonText_saveAnd; + public static String UnsavedChanges_saved; + public static String UnsavedChanges_saving; + public static String UnsavedChanges_savingFailed; + public static String UnsavedChanges_secondaryWindow; + public static String UnsavedChanges_selectAllButtonText; + public static String UnsavedChanges_theFollowingApplicationInstancesHaveUnsavedChanges; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingAllTabs; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheTabs; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheWindow; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeExiting; + public static String UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout; public static String WebBrowser; public static String Welcome; public static String Window; 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 6f403bcbf0..ff9d6fe288 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 @@ -92,8 +92,6 @@ import javafx.stage.Stage; import javafx.stage.Window; -import javax.tools.Tool; - /** * Primary UI for a phoebus application * @@ -1077,8 +1075,8 @@ private void replaceLayout(final MementoTree memento) { JobManager.schedule("Close all stages", monitor -> { boolean shouldExit = confirmationDialogWhenUnsavedChangesExist(stages, - "Would you like to save any changes before replacing the layout?", - "replace", + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout, + Messages.UnsavedChanges_replace, main_stage, monitor); @@ -1296,8 +1294,8 @@ private void closeMainStage() { JobManager.schedule("Close all stages", monitor -> { boolean shouldExit = confirmationDialogWhenUnsavedChangesExist(stages, - "Would you like to save any changes before exiting?", - "exit", + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeExiting, + Messages.UnsavedChanges_exit, main_stage, monitor); @@ -1338,10 +1336,10 @@ private static SortedMap>> sta for (Stage stage : stages) { String currentWindowName; if (stage == DockStage.getDockStages().get(0)) { - currentWindowName = "Main window"; + currentWindowName = Messages.UnsavedChanges_mainWindow; } else { - currentWindowName = "Secondary window " + currentWindowNr; + currentWindowName = Messages.UnsavedChanges_secondaryWindow + " " + currentWindowNr; currentWindowNr++; } @@ -1416,10 +1414,10 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap { Alert prompt = new Alert(AlertType.CONFIRMATION); @@ -1463,12 +1461,12 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap getCheckBoxStatus.get())) { selectAllCheckboxes_button.setDisable(true); - saveSelectedItems_button.setText("Save & " + closeActionName); + saveSelectedItems_button.setText(Messages.UnsavedChanges_saveButtonText_saveAnd + " " + closeActionName); saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); } else { selectAllCheckboxes_button.setDisable(false); - saveSelectedItems_button.setText("Save"); + saveSelectedItems_button.setText(Messages.UnsavedChanges_saveButtonText); saveSelectedItems_button.setTooltip(new Tooltip(saveSelectedItems_button.getText())); } }; @@ -1511,7 +1509,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap actionSaveIfCheckboxEnabled = () -> { if (checkBox.isSelected()) { - Text saving = new Text("[Saving...]"); + Text saving = new Text("[" + Messages.UnsavedChanges_saving + "]"); saving.setFill(Color.ORANGE); saving.setStyle("-fx-font-weight: bold;"); hBox.getChildren().set(0, saving); @@ -1523,14 +1521,14 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap { boolean shouldCloseTabs = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(stages, - "Would you like to save any changes before closing all tabs?", - "close", + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingAllTabs, + Messages.UnsavedChanges_close, PhoebusApplication.INSTANCE.main_stage, monitor); 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 f1bb6cd6e4..f81a399efe 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 @@ -313,8 +313,8 @@ private void close(final ArrayList tabs) { Window window = getDockPane().getScene().getWindow(); boolean shouldCloseTabs = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(tabs, - "Would you like to save any changes before closing the tabs?", - "close", + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheTabs, + Messages.UnsavedChanges_close, window instanceof Stage ? (Stage) window : null, monitor); 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 2d57b5017b..12e3ca6769 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 @@ -195,8 +195,8 @@ else if(layout.getChildren().get(0) instanceof SplitPane){ JobManager.schedule("Close " + stage.getTitle(), monitor -> { boolean shouldCloseStage = PhoebusApplication.confirmationDialogWhenUnsavedChangesExist(stage, - "Would you like to save any changes before closing the window?", - "close", + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheWindow, + Messages.UnsavedChanges_close, monitor); if (shouldCloseStage) { diff --git a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties index 0d7d8893c8..db0e2e1867 100644 --- a/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties +++ b/core/ui/src/main/resources/org/phoebus/ui/application/messages.properties @@ -132,6 +132,26 @@ TimeTime=Time: TimeYear=Year: TopResources=Top Resources UnLockPane=Un-lock Pane +UnsavedChanges=Unsaved changes +UnsavedChanges_clearButtonText=Clear +UnsavedChanges_close=close +UnsavedChanges_discardButtonText_discardAnd=Discard & +UnsavedChanges_exit=exit +UnsavedChanges_mainWindow=Main window +UnsavedChanges_replace=replace +UnsavedChanges_saveButtonText=Save +UnsavedChanges_saveButtonText_saveAnd=Save & +UnsavedChanges_saved=Saved +UnsavedChanges_saving=Saving... +UnsavedChanges_savingFailed=Saving failed +UnsavedChanges_secondaryWindow=Secondary window +UnsavedChanges_selectAllButtonText=Select all +UnsavedChanges_theFollowingApplicationInstancesHaveUnsavedChanges=The following application instances have unsaved changes. +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingAllTabs=Would you like to save any changes before closing all tabs? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheTabs=Would you like to save any changes before closing the tabs? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeClosingTheWindow=Would you like to save any changes before closing the window? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeExiting=Would you like to save any changes before exiting? +UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout=Would you like to save any changes before replacing the layout? WebBrowser=Web Browser Welcome=Welcome Window=Window From c3c8f7de35502c8a346d3f42f557459d81ec56ce Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Thu, 3 Aug 2023 09:53:48 +0200 Subject: [PATCH 06/14] CSSTUDIO-1987 Rename "actionSaveIfCheckboxEnabled" to "saveIfCheckboxEnabled". --- .../java/org/phoebus/ui/application/PhoebusApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ff9d6fe288..ca9d562600 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 @@ -1506,7 +1506,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap checkBox.setSelected(!checkBox.isSelected())); // Enable toggling checkbox by clicking on its label. - Supplier actionSaveIfCheckboxEnabled = () -> { + Supplier saveIfCheckboxEnabled = () -> { if (checkBox.isSelected()) { Text saving = new Text("[" + Messages.UnsavedChanges_saving + "]"); @@ -1542,7 +1542,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap Date: Thu, 3 Aug 2023 10:36:46 +0200 Subject: [PATCH 07/14] CSSTUDIO-1987 Add the parameter "parentWindow" to the functions DockItemWithInput.save_as() and DockItemWithInput.save(). --- .../builder/editor/app/DisplayEditorInstance.java | 2 +- .../phoebus/ui/application/PhoebusApplication.java | 6 +++--- .../org/phoebus/ui/docking/DockItemWithInput.java | 14 +++++++------- .../main/java/org/phoebus/ui/docking/DockPane.java | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java index 90b108f426..c290bb344c 100644 --- a/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java +++ b/app/display/editor/src/main/java/org/csstudio/display/builder/editor/app/DisplayEditorInstance.java @@ -464,7 +464,7 @@ void doSave(final JobMonitor monitor) throws Exception else { // Save-As with proper file name dock_item.setInput(proper.toURI()); - if (! dock_item.save_as(monitor)) + if (! dock_item.save_as(monitor, dock_item.getTabPane().getScene().getWindow())) dock_item.setInput(orig_input); } } 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 ca9d562600..8c2fff8455 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 @@ -497,10 +497,10 @@ private MenuBar createMenu(final Stage stage) { top_resources_menu.setDisable(true); final MenuItem file_save = new MenuItem(Messages.Save, ImageCache.getImageView(getClass(), "/icons/save_edit.png")); - file_save.setOnAction(event -> JobManager.schedule(Messages.Save, monitor -> active_item_with_input.get().save(monitor))); + file_save.setOnAction(event -> JobManager.schedule(Messages.Save, monitor -> active_item_with_input.get().save(monitor, active_item_with_input.get().getTabPane().getScene().getWindow()))); final MenuItem file_save_as = new MenuItem(Messages.SaveAs, ImageCache.getImageView(getClass(), "/icons/saveas_edit.png")); - file_save_as.setOnAction(event -> JobManager.schedule(Messages.SaveAs, monitor -> active_item_with_input.get().save_as(monitor))); + file_save_as.setOnAction(event -> JobManager.schedule(Messages.SaveAs, monitor -> active_item_with_input.get().save_as(monitor, active_item_with_input.get().getTabPane().getScene().getWindow()))); final MenuItem exit = new MenuItem(Messages.Exit); exit.setOnAction(event -> closeMainStage()); @@ -1513,7 +1513,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap okToClose() final CompletableFuture done = new CompletableFuture<>(); JobManager.schedule(Messages.Save, monitor -> { - save(monitor); + save(monitor, getTabPane().getScene().getWindow()); // Indicate if we may close, or need to stay open because of error done.complete(!isDirty()); }); @@ -261,7 +261,7 @@ public Future okToClose() * @param monitor {@link JobMonitor} for reporting progress * @return true on success */ - public final boolean save(final JobMonitor monitor) + public final boolean save(final JobMonitor monitor, Window parentWindow) { // 'final' because any save customization should be possible // inside the save_handler @@ -272,7 +272,7 @@ public final boolean save(final JobMonitor monitor) // call save_as to prompt for file File file = ResourceParser.getFile(getInput()); if (file == null) - return save_as(monitor); + return save_as(monitor, parentWindow); if (file.exists() && !file.canWrite()) @@ -291,7 +291,7 @@ public final boolean save(final JobMonitor monitor) // If user doesn't want to overwrite, abort the save if (response.get() == ButtonType.OK) - return save_as(monitor); + return save_as(monitor, getTabPane().getScene().getWindow()); return false; } @@ -372,7 +372,7 @@ private static File setFileExtension(final File file, final List valid) * @param monitor {@link JobMonitor} for reporting progress * @return true on success */ - public final boolean save_as(final JobMonitor monitor) + public final boolean save_as(final JobMonitor monitor, Window parentWindow) { // 'final' because any save customization should be possible // inside the save_handler @@ -380,7 +380,7 @@ public final boolean save_as(final JobMonitor monitor) { // Prompt for file final File initial = ResourceParser.getFile(getInput()); - final File file = new SaveAsDialog().promptForFile(getTabPane().getScene().getWindow(), + final File file = new SaveAsDialog().promptForFile(parentWindow, Messages.SaveAs, initial, file_extensions); if (file == null) return false; @@ -426,7 +426,7 @@ else if (response == ButtonType.NO) // Update input setInput(ResourceParser.getURI(actual_file.get())); // Save in that file - return save(monitor); + return save(monitor, getTabPane().getScene().getWindow()); } catch (Exception ex) { 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 0a5a207497..c7de0ba996 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 @@ -335,7 +335,7 @@ private void handleGlobalKeys(final KeyEvent event) { final DockItemWithInput active_item_with_input = (DockItemWithInput) item; if (active_item_with_input.isDirty()) - JobManager.schedule(Messages.Save, monitor -> active_item_with_input.save(monitor)); + JobManager.schedule(Messages.Save, monitor -> active_item_with_input.save(monitor, active_item_with_input.getTabPane().getScene().getWindow())); } event.consume(); } From 319b0840f0b65011a020b4fa9c66683f597b045b Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Tue, 8 Aug 2023 10:17:01 +0200 Subject: [PATCH 08/14] CSSTUDIO-1987 Set min-size of confirmation dialog. --- .../main/java/org/phoebus/ui/application/PhoebusApplication.java | 1 + 1 file changed, 1 insertion(+) 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 8c2fff8455..c9cfc263ab 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 @@ -1600,6 +1600,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap Date: Tue, 8 Aug 2023 11:00:56 +0200 Subject: [PATCH 09/14] CSSTUDIO-1987 Fix file-extension dialog when running on the JavaFX Application Thread. --- .../org/phoebus/ui/docking/DockItemWithInput.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java index 1e00ff1cce..d4f029fc07 100644 --- a/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java +++ b/core/ui/src/main/java/org/phoebus/ui/docking/DockItemWithInput.java @@ -400,7 +400,8 @@ public final boolean save_as(final JobMonitor monitor, Window parentWindow) file, valid.stream().collect(Collectors.joining(", ")), suggestion); - Platform.runLater(() -> + + Runnable confirmFileExtension = () -> { final Alert dialog = new Alert(AlertType.CONFIRMATION, prompt, ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); dialog.setTitle(Messages.SaveAs); @@ -417,7 +418,15 @@ else if (response == ButtonType.NO) actual_file.complete(file); else actual_file.complete(null); - }); + }; + + if (Platform.isFxApplicationThread()) { + confirmFileExtension.run(); + } + else { + Platform.runLater(confirmFileExtension); + } + // In background thread, wait for the result if (actual_file.get() == null) return false; From 6c6c64d5f6dad992a7993ad948348c7bdec58644 Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Tue, 8 Aug 2023 11:30:53 +0200 Subject: [PATCH 10/14] CSSTUDIO-1987 Improve the check for whether there are unsaved changes or not. --- .../ui/application/PhoebusApplication.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) 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 c9cfc263ab..c4d3c7ca20 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 @@ -1401,11 +1401,6 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap sortedMap.values().stream().allMatch(Collection::isEmpty))) { - // No unsaved changes. - return true; - } - Stage stageToPositionTheConfirmationDialogOver; if (stage != null) { stageToPositionTheConfirmationDialogOver = stage; @@ -1608,14 +1603,15 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap sortedMap.values().stream().allMatch(Collection::isEmpty))) { + // No unsaved changes. + return true; + } + else { + Platform.runLater(displayConfirmationWindow); + boolean shouldClose = (boolean) displayConfirmationWindow.get(); + return shouldClose; + } } private void exitPhoebus() { From 680f578c5e1548044b7d0ccc593254189feb6156 Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Tue, 8 Aug 2023 15:37:38 +0200 Subject: [PATCH 11/14] CSSTUDIO-1987 Remove unused imports. --- .../display/builder/runtime/app/DisplayRuntimeInstance.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java index d13f2b9d41..46e145c5bf 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java @@ -19,7 +19,6 @@ import java.util.concurrent.TimeoutException; import java.util.logging.Level; -import javafx.stage.Window; import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.Preferences; import org.csstudio.display.builder.model.Widget; @@ -34,7 +33,6 @@ 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.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.docking.DockItem; import org.phoebus.ui.docking.DockItemWithInput; From 80f2e2535494fa3fe7340b13033303da77f66c59 Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Wed, 9 Aug 2023 10:39:16 +0200 Subject: [PATCH 12/14] CSSTUDIO-1987 If a save operation fails, do not process the remaining save actions. --- .../ui/application/PhoebusApplication.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) 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 c4d3c7ca20..9f48eb7baa 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 @@ -1395,6 +1395,12 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(List stag monitor); } + private enum SaveStatus { + SUCCESS, + FAILURE, + NOTHING + }; + public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap>> windowNrToApplicationNameToDockItemsWithInput, String question, String closeActionName, @@ -1440,7 +1446,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap> setCheckBoxStatusActions = new LinkedList<>(); List> getCheckBoxStatusActions = new LinkedList<>(); - List> saveActions = new LinkedList<>(); + List> saveActions = new LinkedList<>(); Runnable enableAndDisableButtons = () -> { if (getCheckBoxStatusActions.stream().anyMatch(getCheckBoxStatus -> getCheckBoxStatus.get())) { @@ -1501,7 +1507,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap checkBox.setSelected(!checkBox.isSelected())); // Enable toggling checkbox by clicking on its label. - Supplier saveIfCheckboxEnabled = () -> { + Supplier saveIfCheckboxEnabled = () -> { if (checkBox.isSelected()) { Text saving = new Text("[" + Messages.UnsavedChanges_saving + "]"); @@ -1520,7 +1526,7 @@ public static boolean confirmationDialogWhenUnsavedChangesExist(SortedMap { event.consume(); - List> saveActionsThatHaveBeenCompleted = new LinkedList<>(); + List> saveActionsThatHaveBeenCompleted = new LinkedList<>(); for (var saveAction : saveActions) { - boolean result = saveAction.get(); - if (result) { + SaveStatus result = saveAction.get(); + if (result == SaveStatus.SUCCESS) { saveActionsThatHaveBeenCompleted.add(saveAction); } + else if (result == SaveStatus.FAILURE) { + break; + } + // If result == SaveStatus.NOTHING, continue. } for (var saveActionThatHasBeenCompleted : saveActionsThatHaveBeenCompleted) { From 559cd57767fd1355771ee65c161d279fc63f168b Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Wed, 9 Aug 2023 13:31:18 +0200 Subject: [PATCH 13/14] CSSTUDIO-1987 Improve variable name. --- .../phoebus/ui/application/PhoebusApplication.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 9f48eb7baa..ec1a7c13f4 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 @@ -1074,13 +1074,13 @@ private void replaceLayout(final MementoTree memento) { JobManager.schedule("Close all stages", monitor -> { - boolean shouldExit = confirmationDialogWhenUnsavedChangesExist(stages, - Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout, - Messages.UnsavedChanges_replace, - main_stage, - monitor); + boolean shouldReplaceLayout = confirmationDialogWhenUnsavedChangesExist(stages, + Messages.UnsavedChanges_wouldYouLikeToSaveAnyChangesBeforeReplacingTheLayout, + Messages.UnsavedChanges_replace, + main_stage, + monitor); - if (shouldExit) { + if (shouldReplaceLayout) { for (Stage stage : stages) { if (!DockStage.prepareToCloseItems(stage)) { return; From 64648c74729c34f8f5a5037c9ba304b74488154d Mon Sep 17 00:00:00 2001 From: Abraham Wolk Date: Fri, 11 Aug 2023 08:29:34 +0200 Subject: [PATCH 14/14] CSSTUDIO-1997 Fix concurrency bug in tab selection: run the tab selection on the JavaFX-thread. --- .../java/org/phoebus/ui/docking/DockItem.java | 21 ++++++++++++------- .../java/org/phoebus/ui/docking/DockPane.java | 12 ++++++----- 2 files changed, 20 insertions(+), 13 deletions(-) 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 15c074663a..eed796221a 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 @@ -192,14 +192,17 @@ private void handleCloseRequest(Event event) { boolean shouldClose = this instanceof DockItemWithInput ? ((DockItemWithInput) this).okToClose().get() : true; if (shouldClose) { - // Select the previously selected tab: var dockPane = getDockPane(); - dockPane.tabsInOrderOfFocus.remove(this); - if (dockPane.tabsInOrderOfFocus.size() > 0) { - var tabToSelect = dockPane.tabsInOrderOfFocus.getFirst(); - var selectionModel = dockPane.getSelectionModel(); - selectionModel.select(tabToSelect); - } + Platform.runLater(() -> { + // Select the previously selected tab: + dockPane.tabsInOrderOfFocus.remove(this); + + if (dockPane.tabsInOrderOfFocus.size() > 0) { + var tabToSelect = dockPane.tabsInOrderOfFocus.getFirst(); + var selectionModel = dockPane.getSelectionModel(); + selectionModel.select(tabToSelect); + } + }); // Close the tab: prepareToClose(); @@ -723,7 +726,9 @@ protected void handleClosed() // calling the OnCloseRequest event-handler.) var dockPane = getDockPane(); if (dockPane != null) { - dockPane.tabsInOrderOfFocus.remove(this); + Platform.runLater(() -> { + dockPane.tabsInOrderOfFocus.remove(this); + }); } } 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 daa3762bd8..b6926e4d8b 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 @@ -227,11 +227,13 @@ public static void alwaysShowTabs(final boolean do_show_single_tabs) setOnContextMenuRequested(this::showContextMenu); getSelectionModel().selectedItemProperty().addListener((observable, previous_item, new_item) -> { - // Keep track of the order of focus of tabs: - if (new_item != null) { - tabsInOrderOfFocus.remove(new_item); - tabsInOrderOfFocus.push((DockItem) new_item); - } + Platform.runLater(() -> { + // Keep track of the order of focus of tabs: + if (new_item != null) { + tabsInOrderOfFocus.remove(new_item); + tabsInOrderOfFocus.push((DockItem) new_item); + } + }); }); }