package net.krazyweb.starmodmanager.view; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.DragEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.stage.WindowEvent; import javafx.util.Duration; import net.krazyweb.helpers.CSSHelper; import net.krazyweb.helpers.FXHelper; import net.krazyweb.starmodmanager.ModManager; import net.krazyweb.starmodmanager.data.Localizer; import net.krazyweb.starmodmanager.data.LocalizerFactory; import net.krazyweb.starmodmanager.data.LocalizerModelInterface; import net.krazyweb.starmodmanager.data.Observable; import net.krazyweb.starmodmanager.data.Observer; import net.krazyweb.starmodmanager.data.SettingsFactory; import net.krazyweb.starmodmanager.data.SettingsModelInterface; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class MainView implements Observer { @SuppressWarnings("unused") private static final Logger log = LogManager.getLogger(MainView.class); private MainViewController controller; private Scene scene; private VBox root; private StackPane stackPane; private ScrollPane mainContentPane; private Text appName; private Text versionName; private GridPane pageTabs; private Button modListButton; private Button backupListButton; private Button settingsButton; private Button aboutButton; private Button quickBackupButton; private Button addModButton; private Button lockButton; private Button refreshButton; private Button expandButton; private boolean firstRunComplete = false; private SettingsModelInterface settings; private LocalizerModelInterface localizer; protected MainView(final MainViewController c) { this.controller = c; settings = new SettingsFactory().getInstance(); localizer = new LocalizerFactory().getInstance(); localizer.addObserver(this); } protected void build() { appName = new Text(); appName.setId("topbar-title"); versionName = new Text(); versionName.setId("topbar-version"); AnchorPane.setTopAnchor(appName, 15.0); AnchorPane.setBottomAnchor(appName, 15.0); AnchorPane.setLeftAnchor(appName, 19.0); AnchorPane.setRightAnchor(versionName, 19.0); AnchorPane.setTopAnchor(versionName, 15.0); AnchorPane topBar = new AnchorPane(); topBar.getChildren().addAll(appName, versionName); topBar.setId("topbar"); root = new VBox(); root.getChildren().add(topBar); pageTabs = new GridPane(); HBox buttons = new HBox(); buildTabs(); buildActionButtons(); pageTabs.add(modListButton, 0, 0); pageTabs.add(backupListButton, 1, 0); pageTabs.add(settingsButton, 2, 0); pageTabs.add(aboutButton, 3, 0); pageTabs.setHgap(36); GridPane.setValignment(modListButton, VPos.CENTER); GridPane.setHalignment(modListButton, HPos.CENTER); GridPane.setValignment(backupListButton, VPos.CENTER); GridPane.setHalignment(backupListButton, HPos.CENTER); GridPane.setValignment(settingsButton, VPos.CENTER); GridPane.setHalignment(settingsButton, HPos.CENTER); GridPane.setValignment(aboutButton, VPos.CENTER); GridPane.setHalignment(aboutButton, HPos.CENTER); buttons.setSpacing(25); buttons.getChildren().addAll( //quickBackupButton, TODO Implement later addModButton, lockButton, refreshButton, expandButton ); AnchorPane.setLeftAnchor(pageTabs, 35.0); AnchorPane.setTopAnchor(pageTabs, 29.0); AnchorPane.setBottomAnchor(pageTabs, 25.0); AnchorPane.setTopAnchor(buttons, 29.0); AnchorPane.setRightAnchor(buttons, 24.0); AnchorPane tabsBar = new AnchorPane(); tabsBar.setId("tabsbar"); tabsBar.getChildren().addAll(pageTabs, buttons); mainContentPane = new ScrollPane(); mainContentPane.setFitToHeight(true); mainContentPane.setFitToWidth(true); mainContentPane.setFocusTraversable(false); mainContentPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); mainContentPane.setHbarPolicy(ScrollBarPolicy.NEVER); VBox v = new VBox(); v.getChildren().add(mainContentPane); v.setPadding(new Insets(20, 20, 20, 1)); VBox.setVgrow(mainContentPane, Priority.ALWAYS); VBox.setVgrow(v, Priority.ALWAYS); root.getChildren().add(tabsBar); root.getChildren().add(v); stackPane = new StackPane(); stackPane.getChildren().add(root); scene = new Scene(stackPane, Math.max(settings.getPropertyDouble("windowwidth"), settings.getPropertyDouble("enforcedminwidth")), Math.max(settings.getPropertyDouble("windowheight"), settings.getPropertyDouble("enforcedminheight"))); scene.getStylesheets().add(MainView.class.getClassLoader().getResource("theme_base.css").toString()); scene.getStylesheets().add(MainView.class.getClassLoader().getResource(settings.getPropertyString("theme")).toString()); Stage stage = ModManager.getPrimaryStage(); /* * Similar to pre-rendering the views of each tab, to set the * correct window size (including borders, making it an unknown value) * to maintain a minimum canvas size (known), an empty scene with the * wanted values must be rendered in a fully transparent window. * The stage width/height is then the real minimum size that is * desired, so that value is plugged in for future use. * Once all that is done, hide the stage, change the opacity back to 1.0, * then add in the real scene. */ stage.setOpacity(0.0); stage.setScene(new Scene(new VBox(), settings.getPropertyDouble("enforcedminwidth"), settings.getPropertyDouble("enforcedminheight"))); stage.show(); stage.setMinWidth(stage.getWidth()); stage.setMinHeight(stage.getHeight()); stage.hide(); stage.setOpacity(1.0); stage.setScene(scene); setSceneEvents(scene, stackPane, root); setStageEvents(); updateStrings(); updateRefreshButton(); updateAddModButton(); } private void buildTabs() { modListButton = new Button(); modListButton.setId("pagetab-selected"); modListButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { modListButton.setId("pagetab-selected"); backupListButton.setId("pagetab"); settingsButton.setId("pagetab"); aboutButton.setId("pagetab"); controller.modTabClicked(); } }); backupListButton = new Button(); backupListButton.setId("pagetab"); backupListButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { modListButton.setId("pagetab"); backupListButton.setId("pagetab-selected"); settingsButton.setId("pagetab"); aboutButton.setId("pagetab"); controller.backupsTabClicked(); } }); settingsButton = new Button(); settingsButton.setId("pagetab"); settingsButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { modListButton.setId("pagetab"); backupListButton.setId("pagetab"); settingsButton.setId("pagetab-selected"); aboutButton.setId("pagetab"); controller.settingsTabClicked(); } }); aboutButton = new Button(); aboutButton.setId("pagetab"); aboutButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { modListButton.setId("pagetab"); backupListButton.setId("pagetab"); settingsButton.setId("pagetab"); aboutButton.setId("pagetab-selected"); controller.aboutTabClicked(); } }); } private void buildActionButtons() { quickBackupButton = new Button(); quickBackupButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("quick-backup-icon.png")))); quickBackupButton.setId("mainview-action-button"); quickBackupButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { controller.backupButtonClicked(); e.consume(); } }); addModButton = new Button(); addModButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("add-mods-icon.png")))); addModButton.setId("mainview-action-button"); addModButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { controller.addModsButtonClicked(); e.consume(); } }); lockButton = new Button(); lockButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("unlocked-list-icon.png")))); lockButton.setId("mainview-action-button"); lockButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { controller.lockButtonClicked(); e.consume(); } }); refreshButton = new Button(); refreshButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("refresh-list-icon.png")))); refreshButton.setId("mainview-action-button"); refreshButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { controller.refreshButtonClicked(); e.consume(); } }); expandButton = new Button(); expandButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("expand-list-icon.png")))); expandButton.setId("mainview-action-button"); expandButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent e) { controller.expandButtonClicked(); e.consume(); } }); } protected void updateAddModButton() { Color color = CSSHelper.getColor("mod-list-button-color", settings.getPropertyString("theme")); FXHelper.setColor(addModButton.getGraphic(), color); } protected void updateLockButton(final boolean locked) { if (locked) { lockButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("locked-list-icon.png")))); Color color = CSSHelper.getColor("mod-list-locked-button-color", settings.getPropertyString("theme")); FXHelper.setColor(lockButton.getGraphic(), color); } else { lockButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("unlocked-list-icon.png")))); Color color = CSSHelper.getColor("mod-list-button-color", settings.getPropertyString("theme")); FXHelper.setColor(lockButton.getGraphic(), color); } } protected void updateRefreshButton() { Color color = CSSHelper.getColor("mod-list-button-color", settings.getPropertyString("theme")); FXHelper.setColor(refreshButton.getGraphic(), color); if (!firstRunComplete) { firstRunComplete = true; return; } Timeline refreshAnimation = new Timeline(); refreshAnimation.setCycleCount(1); refreshAnimation.setOnFinished(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { refreshButton.setRotate(0); } }); final KeyValue kv1 = new KeyValue(refreshButton.rotateProperty(), 1440, Interpolator.EASE_BOTH); final KeyFrame kf1 = new KeyFrame(Duration.millis(4500), kv1); refreshAnimation.getKeyFrames().addAll(kf1); refreshAnimation.playFromStart(); } protected void updateExpandButton(final boolean expanded) { if (expanded) { expandButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("collapse-list-icon.png")))); } else { expandButton.setGraphic(new ImageView(new Image(MainView.class.getClassLoader().getResourceAsStream("expand-list-icon.png")))); } Color color = CSSHelper.getColor("mod-list-button-color", settings.getPropertyString("theme")); FXHelper.setColor(expandButton.getGraphic(), color); } private void setSceneEvents(final Scene scene, final StackPane stackPane, final VBox root) { scene.setOnDragOver(new EventHandler<DragEvent>() { @Override public void handle(final DragEvent event) { controller.filesDraggedOver(event); } }); scene.setOnDragExited(new EventHandler<DragEvent>() { @Override public void handle(final DragEvent event) { controller.dragExited(); } }); scene.setOnDragDropped(new EventHandler<DragEvent>() { @Override public void handle(final DragEvent event) { controller.filesDropped(event); } }); } private void setStageEvents() { ModManager.getPrimaryStage().setOnCloseRequest(new EventHandler<WindowEvent>() { @Override public void handle(final WindowEvent event) { controller.closeRequested(event); } }); } protected void showOverlay(final String messageTitle, final String message) { Text textTitle = new Text(messageTitle); Text textMessage = new Text(message); textTitle.setId("add-mods-overlay-text"); textMessage.setId("add-mods-overlay-text"); VBox box = new VBox(); box.setAlignment(Pos.CENTER); box.getChildren().addAll(textTitle, textMessage); stackPane.getChildren().addAll(new Rectangle(root.getWidth(), root.getHeight(), new Color(0.0, 0.0, 0.0, 0.8)), box); } protected void hideOverlay() { stackPane.getChildren().clear(); stackPane.getChildren().add(root); } protected void show() { ModManager.getPrimaryStage().show(); } protected void setContent(final Node content) { mainContentPane.setContent(content); } protected ScrollPane getContent() { return mainContentPane; } protected Scene getScene() { return scene; } private void updateStrings() { ModManager.getPrimaryStage().setTitle(localizer.formatMessage("windowtitle", settings.getVersion())); appName.setText(localizer.getMessage("appname").toUpperCase()); versionName.setText(settings.getVersion()); modListButton.setText(localizer.getMessage("navbartabs.mods")); backupListButton.setText(localizer.getMessage("navbartabs.backups")); settingsButton.setText(localizer.getMessage("navbartabs.settings")); aboutButton.setText(localizer.getMessage("navbartabs.about")); modListButton.setEllipsisString(localizer.getMessage("navbartabs.mods")); backupListButton.setEllipsisString(localizer.getMessage("navbartabs.backups")); settingsButton.setEllipsisString(localizer.getMessage("navbartabs.settings")); aboutButton.setEllipsisString(localizer.getMessage("navbartabs.about")); /* * JavaFX's GridPane pushes around elements when the size of a column changes. * When changing the styling of the text for highlighted buttons, this becomes * a problem, as the whole layout shifts every click. To get around this, it's * necessary to compute the pixel width of the text in each button, then * constrain the columns to be the maximum size of the text-no more, no less. * Accurate HGaps can then be applied and the elements will not move around. * * To compute the actual size of the text in the scene, it's necessary to * create an invisible stage, add a Text node with the font and size * equivalent to the stylesheet's, then get the node's layout width. */ ColumnConstraints col1 = new ColumnConstraints(); ColumnConstraints col2 = new ColumnConstraints(); ColumnConstraints col3 = new ColumnConstraints(); ColumnConstraints col4 = new ColumnConstraints(); Text test = new Text(); test.setFont(Font.loadFont(MainView.class.getClassLoader().getResourceAsStream("Lato-Medium.ttf"), 18)); test.setId("pagetab-selected"); VBox t = new VBox(); t.getChildren().add(test); Stage s = new Stage(); s.setOpacity(0); s.setScene(new Scene(t, 500, 500)); s.show(); test.setText(localizer.getMessage("navbartabs.mods")); col1.setPrefWidth(test.getLayoutBounds().getWidth()); test.setText(localizer.getMessage("navbartabs.backups")); col2.setPrefWidth(test.getLayoutBounds().getWidth()); test.setText(localizer.getMessage("navbartabs.settings")); col3.setPrefWidth(test.getLayoutBounds().getWidth()); test.setText(localizer.getMessage("navbartabs.about")); col4.setPrefWidth(test.getLayoutBounds().getWidth()); s.close(); pageTabs.getColumnConstraints().clear(); pageTabs.getColumnConstraints().addAll(col1, col2, col3, col4); } @Override public void update(final Observable observable, final Object message) { if (observable instanceof Localizer && message.equals("localechanged")) { updateStrings(); } } }