diff --git a/api/src/main/resources/edu/wpi/first/shuffleboard/api/base.css b/api/src/main/resources/edu/wpi/first/shuffleboard/api/base.css index 01f9de10f..49e62febe 100644 --- a/api/src/main/resources/edu/wpi/first/shuffleboard/api/base.css +++ b/api/src/main/resources/edu/wpi/first/shuffleboard/api/base.css @@ -235,3 +235,11 @@ /* Bring the content to all edges */ -fx-padding: 0; /* 10 0 0 10 */ } + +.dialog-pane .header-panel { + -fx-background-color: -swatch-500; +} + +.dialog-pane .header-panel .label { + -fx-text-fill: white; +} diff --git a/app/src/main/java/edu/wpi/first/shuffleboard/app/MainWindowController.java b/app/src/main/java/edu/wpi/first/shuffleboard/app/MainWindowController.java index 5874ab941..2aa06e5f2 100644 --- a/app/src/main/java/edu/wpi/first/shuffleboard/app/MainWindowController.java +++ b/app/src/main/java/edu/wpi/first/shuffleboard/app/MainWindowController.java @@ -1,13 +1,11 @@ package edu.wpi.first.shuffleboard.app; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import com.google.common.io.Files; - import edu.wpi.first.shuffleboard.api.DashboardMode; import edu.wpi.first.shuffleboard.api.components.SourceTreeTable; +import edu.wpi.first.shuffleboard.api.components.WidgetPropertySheet; import edu.wpi.first.shuffleboard.api.dnd.DataFormats; import edu.wpi.first.shuffleboard.api.plugin.Plugin; +import edu.wpi.first.shuffleboard.api.prefs.FlushableProperty; import edu.wpi.first.shuffleboard.api.sources.DataSource; import edu.wpi.first.shuffleboard.api.sources.SourceEntry; import edu.wpi.first.shuffleboard.api.sources.recording.Recorder; @@ -19,13 +17,15 @@ import edu.wpi.first.shuffleboard.app.components.DashboardTab; import edu.wpi.first.shuffleboard.app.components.DashboardTabPane; import edu.wpi.first.shuffleboard.app.components.WidgetGallery; -import edu.wpi.first.shuffleboard.api.components.WidgetPropertySheet; import edu.wpi.first.shuffleboard.app.json.JsonBuilder; import edu.wpi.first.shuffleboard.app.plugin.PluginLoader; import edu.wpi.first.shuffleboard.app.prefs.AppPreferences; -import edu.wpi.first.shuffleboard.api.prefs.FlushableProperty; import edu.wpi.first.shuffleboard.app.sources.recording.Playback; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.Files; + import org.fxmisc.easybind.EasyBind; import java.io.File; @@ -68,6 +68,8 @@ import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; +import javafx.stage.Window; +import javafx.stage.WindowEvent; import static edu.wpi.first.shuffleboard.api.components.SourceTreeTable.alphabetical; import static edu.wpi.first.shuffleboard.api.components.SourceTreeTable.branchesFirst; @@ -292,10 +294,17 @@ public void setDashboard(DashboardTabPane dashboard) { centerSplitPane.getItems().add(dashboard); } + /** + * Closes from interacting with the "Close" menu item. + */ @FXML public void close() { log.info("Exiting app"); - System.exit(0); + + // Attempt to close the main window. This lets window closing handlers run. Calling System.exit() or Platform.exit() + // will more-or-less immediately terminate the application without calling these handlers. + Window window = root.getScene().getWindow(); + window.fireEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSE_REQUEST)); } /** diff --git a/app/src/main/java/edu/wpi/first/shuffleboard/app/Shuffleboard.java b/app/src/main/java/edu/wpi/first/shuffleboard/app/Shuffleboard.java index 769ce47e5..1d0c8c2d2 100644 --- a/app/src/main/java/edu/wpi/first/shuffleboard/app/Shuffleboard.java +++ b/app/src/main/java/edu/wpi/first/shuffleboard/app/Shuffleboard.java @@ -28,6 +28,8 @@ import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.scene.layout.Pane; import javafx.stage.Screen; import javafx.stage.Stage; @@ -90,6 +92,30 @@ public void start(Stage primaryStage) throws IOException { primaryStage.setMinHeight(480); primaryStage.setWidth(Screen.getPrimary().getVisualBounds().getWidth()); primaryStage.setHeight(Screen.getPrimary().getVisualBounds().getHeight()); + primaryStage.setOnCloseRequest(closeEvent -> { + if (!AppPreferences.getInstance().isConfirmExit()) { + // Don't show the confirmation dialog, just exit + return; + } + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Save layout"); + alert.getDialogPane().getScene().getStylesheets().setAll(mainPane.getStylesheets()); + alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); + alert.getDialogPane().setHeaderText("Save the current layout before closing?"); + alert.showAndWait().ifPresent(bt -> { + if (bt == ButtonType.YES) { + try { + mainWindowController.save(); + } catch (IOException ex) { + logger.log(Level.WARNING, "Could not save the layout", ex); + } + } else if (bt == ButtonType.CANCEL) { + // cancel the close request by consuming the event + closeEvent.consume(); + } + // Don't need to check for NO because it just lets the window close normally + }); + }); primaryStage.show(); Time.setStartTime(Time.now()); } diff --git a/app/src/main/java/edu/wpi/first/shuffleboard/app/prefs/AppPreferences.java b/app/src/main/java/edu/wpi/first/shuffleboard/app/prefs/AppPreferences.java index 5d3162289..d1fd122f6 100644 --- a/app/src/main/java/edu/wpi/first/shuffleboard/app/prefs/AppPreferences.java +++ b/app/src/main/java/edu/wpi/first/shuffleboard/app/prefs/AppPreferences.java @@ -18,8 +18,7 @@ import javafx.beans.property.SimpleObjectProperty; /** - * Contains the user preferences for the app. These preferences are user-specific and are saved - * to the users home directory and are not contained in save files. + * Contains the user preferences for the app. These preferences are user-specific and are not contained in save files. */ public final class AppPreferences { @@ -28,6 +27,8 @@ public final class AppPreferences { private final Property saveFile = new SimpleObjectProperty<>(this, "saveFile", null); private final BooleanProperty autoLoadLastSaveFile = new SimpleBooleanProperty(this, "automaticallyLoadLastSaveFile", true); + private final BooleanProperty confirmExit = + new SimpleBooleanProperty(this, "showConfirmationDialogWhenExiting", true); @VisibleForTesting static AppPreferences instance = new AppPreferences(); @@ -41,11 +42,13 @@ public AppPreferences() { PreferencesUtils.read(defaultTileSize, preferences); PreferencesUtils.read(saveFile, preferences, File::new); PreferencesUtils.read(autoLoadLastSaveFile, preferences); + PreferencesUtils.read(confirmExit, preferences); theme.addListener(__ -> PreferencesUtils.save(theme, preferences, Theme::getName)); defaultTileSize.addListener(__ -> PreferencesUtils.save(defaultTileSize, preferences)); saveFile.addListener(__ -> PreferencesUtils.save(saveFile, preferences, File::getAbsolutePath)); autoLoadLastSaveFile.addListener(__ -> PreferencesUtils.save(autoLoadLastSaveFile, preferences)); + confirmExit.addListener(__ -> PreferencesUtils.save(confirmExit, preferences)); } public static AppPreferences getInstance() { @@ -59,7 +62,8 @@ public ImmutableList> getProperties() { return ImmutableList.of( theme, defaultTileSize, - autoLoadLastSaveFile + autoLoadLastSaveFile, + confirmExit ); } @@ -110,4 +114,16 @@ public BooleanProperty autoLoadLastSaveFileProperty() { public void setAutoLoadLastSaveFile(boolean autoLoadLastSaveFile) { this.autoLoadLastSaveFile.set(autoLoadLastSaveFile); } + + public boolean isConfirmExit() { + return confirmExit.get(); + } + + public BooleanProperty confirmExitProperty() { + return confirmExit; + } + + public void setConfirmExit(boolean confirmExit) { + this.confirmExit.set(confirmExit); + } }