/* Copyright (c) 2013-2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.launcher.ui; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Point2D; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.stage.DirectoryChooser; import javafx.stage.PopupWindow; import javafx.stage.WindowEvent; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.launcher.ChunkyDeployer; import se.llbit.chunky.launcher.ChunkyLauncher; import se.llbit.chunky.launcher.ConsoleLogger; import se.llbit.chunky.launcher.Dialogs; import se.llbit.chunky.launcher.JreUtil; import se.llbit.chunky.launcher.LaunchMode; import se.llbit.chunky.launcher.LauncherSettings; import se.llbit.chunky.launcher.UpdateChecker; import se.llbit.chunky.launcher.UpdateListener; import se.llbit.chunky.launcher.VersionInfo; import se.llbit.chunky.resources.MinecraftFinder; import se.llbit.chunky.resources.SettingsDirectory; import se.llbit.chunky.ui.IntegerAdjuster; import java.awt.Desktop; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; /** * JavaFX window for the Chunky launcher. */ public final class ChunkyLauncherController implements Initializable, UpdateListener { private final LauncherSettings settings; @FXML protected ComboBox<VersionInfo> version; @FXML protected Button checkForUpdate; @FXML protected ProgressIndicator busyIndicator; @FXML protected TextField minecraftDirectory; @FXML protected Button browseMinecraft; @FXML protected IntegerAdjuster memoryLimit; @FXML protected TextField javaRuntime; @FXML protected Button browseJava; @FXML protected TextField javaOptions; @FXML protected TextField chunkyOptions; @FXML protected CheckBox enableDebugConsole; @FXML protected CheckBox verboseLogging; @FXML protected CheckBox closeConsoleOnExit; @FXML protected CheckBox downloadSnapshots; @FXML protected TextField settingsDirectory; @FXML protected Button openSettingsDirectory; @FXML protected CheckBox alwaysShowLauncher; @FXML protected Button launchButton; @FXML protected Button cancelButton; @FXML protected Label launcherVersion; @FXML protected TitledPane advancedSettings; public ChunkyLauncherController(LauncherSettings settings) { this.settings = settings; } @Override public void initialize(URL location, ResourceBundle resources) { busyIndicator.setTooltip(new Tooltip("Checking for Chunky update...")); cancelButton.setTooltip(new Tooltip("Close the Chunky launcher.")); cancelButton.setOnAction(event -> cancelButton.getScene().getWindow().hide()); launchButton.setTooltip(new Tooltip("Launch Chunky using the current settings.")); launchButton.setOnAction(event -> launchChunky()); launcherVersion.setText(ChunkyLauncher.LAUNCHER_VERSION); advancedSettings.setExpanded(settings.showAdvancedSettings); advancedSettings.expandedProperty().addListener(observable -> Platform.runLater( () -> advancedSettings.getScene().getWindow().sizeToScene())); memoryLimit.setTooltip("Maximum Java heap space in megabytes (MiB).\n" + "Limited by the available memory in your computer."); memoryLimit.setRange(512, 1 << 14); memoryLimit.makeLogarithmic(); memoryLimit.set(settings.memoryLimit); memoryLimit.onValueChange(value -> settings.memoryLimit = value); alwaysShowLauncher.setSelected(settings.showLauncher); alwaysShowLauncher.selectedProperty().addListener( (observable, oldValue, newValue) -> settings.showLauncher = newValue); minecraftDirectory.setText(MinecraftFinder.getMinecraftDirectory().getAbsolutePath()); minecraftDirectory.setTooltip(new Tooltip( "The Minecraft directory is used to find Minecraft saves and the default texture pack.")); browseMinecraft.setTooltip(new Tooltip("Choose Minecraft directory.")); browseMinecraft.setOnAction(event -> { DirectoryChooser chooser = new DirectoryChooser(); chooser.setTitle("Select Minecraft Directory"); File initialDirectory = MinecraftFinder.getMinecraftDirectory(); if (initialDirectory != null && initialDirectory.isDirectory()) { chooser.setInitialDirectory(initialDirectory); } File directory = chooser.showDialog(browseMinecraft.getScene().getWindow()); if (directory != null) { if (MinecraftFinder.getMinecraftJar(directory, false) != null) { String path = directory.getAbsolutePath(); minecraftDirectory.setText(path); PersistentSettings.setMinecraftDirectory(path); } else { launcherWarning("Not a Minecraft Directory", "Could not find a valid Minecraft installation in the selected directory."); } } }); javaRuntime.setText(getConfiguredJre()); browseJava.setTooltip(new Tooltip("Choose Java Runtime directory.")); browseJava.setOnAction(event -> { DirectoryChooser chooser = new DirectoryChooser(); chooser.setTitle("Select Java Installation"); File jreDir = new File(javaRuntime.getText()); if (jreDir.isDirectory()) { chooser.setInitialDirectory(jreDir); } File directory = chooser.showDialog(browseJava.getScene().getWindow()); if (directory != null) { File jreSubDir = new File(directory, "jre"); if (JreUtil.isJreDir(directory)) { javaRuntime.setText(directory.getAbsolutePath()); } else if (JreUtil.isJreDir(jreSubDir)) { javaRuntime.setText(jreSubDir.getAbsolutePath()); } else { launcherWarning("Not a Java Runtime Installation", "Could not find a valid Java installation in the selected directory."); } } }); javaOptions.setTooltip(new Tooltip("Additional command-line options to pass to Java.")); javaOptions.setText(settings.javaOptions); chunkyOptions.setTooltip(new Tooltip("Additional options to pass to Chunky.")); chunkyOptions.setText(settings.chunkyOptions); enableDebugConsole.setTooltip(new Tooltip("Opens a debug console when Chunky is started.")); enableDebugConsole.setSelected(settings.debugConsole); verboseLogging.setTooltip(new Tooltip("Enables verbose log output.")); verboseLogging.setSelected(settings.verboseLogging); closeConsoleOnExit.setTooltip(new Tooltip("Close the debug console when Chunky exits.")); closeConsoleOnExit.setSelected(settings.closeConsoleOnExit); downloadSnapshots.setSelected(settings.downloadSnapshots); downloadSnapshots.selectedProperty().addListener((observable, oldValue, newValue) -> { // In contrast to the other options, we have to update the // "download snapshots" setting directly so that when the // user clicks "Check for Update" we will respect the check box value. settings.downloadSnapshots = newValue; settings.save(); }); openSettingsDirectory.setOnAction(event -> { // Running Desktop.open() on the JavaFX application thread seems to // lock up the application on Linux, so we create a new thread to run that. // This StackOverflow question seems to ask about the same bug: // http://stackoverflow.com/questions/23176624/javafx-freeze-on-desktop-openfile-desktop-browseuri new Thread(() -> { try { if (Desktop.isDesktopSupported()) { Desktop.getDesktop().open(SettingsDirectory.getSettingsDirectory()); } else { Platform.runLater( () -> launcherWarning("Failed to open Settings Directory", "Can not open system file browser.")); } } catch (IOException e1) { Platform.runLater(() -> launcherWarning("Failed to open Settings Directory", "Failed to open system file browser. Reason: " + e1.getMessage())); } }).start(); }); checkForUpdate.setOnAction(event -> { if (isBusy()) { setBusy(true); UpdateChecker updateThread = new UpdateChecker(settings, this); updateThread.start(); } }); } /** * Updates the launcher settings when opening the main launcher window. * * <p>The launcher UI may be created before the first-time setup * window is shown, at which point the settings directory has * not yet been selected. The launcher settings and installed versions * must thus be updated only when the launcher UI is shown. */ protected void onShowing(WindowEvent event) { File settingsDir = SettingsDirectory.getSettingsDirectory(); if (settingsDir != null) { settingsDirectory.setText(settingsDir.getAbsolutePath()); } updateVersionList(); } private String getConfiguredJre() { String configuredJre = settings.javaDir; if (!configuredJre.isEmpty() && JreUtil.isJreDir(new File(configuredJre))) { return configuredJre; } else { return System.getProperty("java.home"); } } private void launcherWarning(String title, String content) { Dialogs.warning(title, content); } public void launcherError(String title, String message) { Dialogs.error(title, message); } /** Show an error dialog when Chunky failed to launch. */ private void launchFailure(String message) { LaunchErrorDialog dialog = new LaunchErrorDialog(message); dialog.show(); dialog.toFront(); } /** Initialize or update the version list. */ void updateVersionList() { version.getItems().setAll(VersionInfo.LATEST); version.getItems().addAll(ChunkyDeployer.availableVersions()); for (VersionInfo versionInfo : version.getItems()) { if (versionInfo.name.equals(settings.version)) { version.getSelectionModel().select(versionInfo); break; } } } @Override public void updateError(String message) { Platform.runLater(() -> { launcherError("Failed to download update", message); setBusy(false); }); } @Override public void updateAvailable(VersionInfo latest) { Platform.runLater(() -> { try { UpdateDialog updateDialog = new UpdateDialog(this, latest); updateDialog.show(); } catch (IOException e) { e.printStackTrace(System.err); } }); } @Override public void noUpdateAvailable() { Platform.runLater(() -> { setBusy(false); Tooltip tooltip = new Tooltip("No update found. Try again later."); Point2D screen = checkForUpdate.localToScreen(0, 0); tooltip.setAnchorLocation(PopupWindow.AnchorLocation.CONTENT_TOP_RIGHT); tooltip.setAutoHide(true); tooltip.show(checkForUpdate, screen.getX() + checkForUpdate.getWidth(), screen.getY() + checkForUpdate.getHeight()); }); } /** Only call this from the JavaFX application thread! */ public void setBusy(boolean busy) { busyIndicator.setVisible(busy); checkForUpdate.setDisable(busy); } /** * Check if an update or update check is in progress. * Should only be called from the JavaFX application thread. */ public boolean isBusy() { // The busy indicator visibility is used for mutual exclusion. // This works because the visibility is only modified on // the JavaFX application thread. return !busyIndicator.isVisible(); } /** This should only be called from the JavaFX application thread. */ public void selectLatestVersion() { version.getSelectionModel().select(VersionInfo.LATEST); } /** This should only be called from the JavaFX application thread. */ public void launchChunky() { settings.javaDir = javaRuntime.getText(); settings.debugConsole = enableDebugConsole.isSelected(); settings.verboseLogging = verboseLogging.isSelected(); settings.closeConsoleOnExit = closeConsoleOnExit.isSelected(); settings.javaOptions = javaOptions.getText(); settings.chunkyOptions = chunkyOptions.getText(); settings.version = version.getSelectionModel().getSelectedItem().name; settings.showLauncher = alwaysShowLauncher.isSelected(); settings.showAdvancedSettings = advancedSettings.isExpanded(); // Resolve specific version. VersionInfo version = ChunkyDeployer.resolveVersion(settings.version); if (!ChunkyDeployer.canLaunch(version, this, true)) { return; } ChunkyDeployer.LoggerBuilder loggerBuilder = () -> { if (settings.forceGuiConsole || (!settings.headless && settings.debugConsole)) { DebugConsole console = new DebugConsole(settings.closeConsoleOnExit); console.show(); return console; } else { return new ConsoleLogger(); } }; PersistentSettings.setMinecraftDirectory(minecraftDirectory.getText()); if (ChunkyDeployer.launchChunky(settings, version, LaunchMode.GUI, this::launchFailure, loggerBuilder) == 0) { settings.save(); cancelButton.getScene().getWindow().hide(); } } }