package envoy.client.ui; import java.io.IOException; import java.util.Stack; import java.util.logging.Level; import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.*; import javafx.scene.input.*; import javafx.stage.Stage; import envoy.client.data.Settings; import envoy.client.event.*; import envoy.util.EnvoyLog; import dev.kske.eventbus.*; /** * Manages a stack of scenes. The most recently added scene is displayed inside * a stage. When a scene is removed from the stack, its predecessor is * displayed. *

* When a scene is loaded, the style sheet for the current theme is applied to * it. *

* Project: envoy-client
* File: SceneContext.java
* Created: 06.06.2020
* * @author Kai S. K. Engelbart * @since Envoy Client v0.1-beta */ public final class SceneContext implements EventListener { /** * Contains information about different scenes and their FXML resource files. * * @author Kai S. K. Engelbart * @since Envoy Client v0.1-beta */ public enum SceneInfo { /** * The main scene in which the chat screen is displayed. * * @since Envoy Client v0.1-beta */ CHAT_SCENE("/fxml/ChatScene.fxml"), /** * The scene in which the settings screen is displayed. * * @since Envoy Client v0.1-beta */ SETTINGS_SCENE("/fxml/SettingsScene.fxml"), /** * The scene in which the login screen is displayed. * * @since Envoy Client v0.1-beta */ LOGIN_SCENE("/fxml/LoginScene.fxml"); /** * The path to the FXML resource. */ public final String path; SceneInfo(String path) { this.path = path; } } private final Stage stage; private final FXMLLoader loader = new FXMLLoader(); private final Stack sceneStack = new Stack<>(); private final Stack controllerStack = new Stack<>(); private static final Settings settings = Settings.getInstance(); /** * Initializes the scene context. * * @param stage the stage in which scenes will be displayed * @since Envoy Client v0.1-beta */ public SceneContext(Stage stage) { this.stage = stage; EventBus.getInstance().registerListener(this); } /** * Loads a new scene specified by a scene info. * * @param sceneInfo specifies the scene to load * @throws RuntimeException if the loading process fails * @since Envoy Client v0.1-beta */ public void load(SceneInfo sceneInfo) { loader.setRoot(null); loader.setController(null); try { final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path)); final var scene = new Scene(rootNode); controllerStack.push(loader.getController()); sceneStack.push(scene); stage.setScene(scene); // Adding the option to exit Linux-like with "Control" + "Q" scene.getAccelerators() .put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), () -> { // Presumably no Settings are loaded in the login scene, hence Envoy is closed // directly if (sceneInfo != SceneInfo.LOGIN_SCENE && settings.isHideOnClose()) stage.setIconified(true); else { EventBus.getInstance().dispatch(new EnvoyCloseEvent()); System.exit(0); } }); // The LoginScene is the only scene not intended to be resized // As strange as it seems, this is needed as otherwise the LoginScene won't be // displayed on some OS (...Debian...) stage.sizeToScene(); Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE)); applyCSS(); stage.show(); } catch (final IOException e) { EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e); throw new RuntimeException(e); } } /** * Removes the current scene and displays the previous one. * * @since Envoy Client v0.1-beta */ public void pop() { // Pop scene and controller sceneStack.pop(); controllerStack.pop(); // Apply new scene if present if (!sceneStack.isEmpty()) { final var newScene = sceneStack.peek(); stage.setScene(newScene); applyCSS(); stage.sizeToScene(); // If the controller implements the Restorable interface, // the actions to perform on restoration will be executed here final var controller = controllerStack.peek(); if (controller instanceof Restorable) ((Restorable) controller).onRestore(); } stage.show(); } private void applyCSS() { if (!sceneStack.isEmpty()) { final var styleSheets = stage.getScene().getStylesheets(); final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css"; styleSheets.clear(); styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), getClass().getResource(themeCSS).toExternalForm()); } } @Event(priority = 150, eventType = ThemeChangeEvent.class) private void onThemeChange() { applyCSS(); } /** * @param the type of the controller * @return the controller used by the current scene * @since Envoy Client v0.1-beta */ public T getController() { return (T) controllerStack.peek(); } /** * @return the stage in which the scenes are displayed * @since Envoy Client v0.1-beta */ public Stage getStage() { return stage; } /** * @return whether the scene stack is empty * @since Envoy Client v0.2-beta */ public boolean isEmpty() { return sceneStack.isEmpty(); } }