From b90d6b7b8c7cc864c8824cc8b97db4621c109312 Mon Sep 17 00:00:00 2001 From: huangyuhui Date: Tue, 16 Jan 2018 19:34:53 +0800 Subject: [PATCH] Update Readme.md for HMCLCore introduction --- .../main/java/org/jackhuang/hmcl/Main.java | 98 +++ .../jackhuang/hmcl/game/AccountHelper.java | 10 +- .../hmcl/game/HMCLModpackInstallTask.java | 16 +- .../jackhuang/hmcl/game/LauncherHelper.java | 44 +- .../org/jackhuang/hmcl/setting/Config.java | 2 +- .../org/jackhuang/hmcl/setting/Settings.java | 2 +- .../org/jackhuang/hmcl/ui/AccountItem.java | 143 ++++ .../org/jackhuang/hmcl/ui/AccountsPage.java | 180 ++++ .../jackhuang/hmcl/ui/AdvancedListBox.java | 57 ++ .../org/jackhuang/hmcl/ui/Controllers.java | 113 +++ .../java/org/jackhuang/hmcl/ui/Decorator.java | 519 ++++++++++++ .../jackhuang/hmcl/ui/DialogController.java | 11 +- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 303 +++++++ .../hmcl/ui/InstallerController.java | 8 +- .../org/jackhuang/hmcl/ui/InstallerItem.java | 2 +- .../jackhuang/hmcl/ui/LaunchingStepsPane.java | 6 +- .../jackhuang/hmcl/ui/LeftPaneController.java | 118 +++ .../java/org/jackhuang/hmcl/ui/LogWindow.java | 8 +- .../java/org/jackhuang/hmcl/ui/MainPage.java | 127 +++ .../jackhuang/hmcl/ui/MessageDialogPane.java | 2 +- .../org/jackhuang/hmcl/ui/ModController.java | 153 ++++ .../org/jackhuang/hmcl/ui/ProfilePage.java | 121 +++ .../main/java/org/jackhuang/hmcl/ui/SVG.java | 96 +++ .../hmcl/ui/SafeIntStringConverter.java | 5 + .../org/jackhuang/hmcl/ui/SettingsPage.java | 135 +++ .../org/jackhuang/hmcl/ui/VersionItem.java | 8 +- .../jackhuang/hmcl/ui/VersionListItem.java | 10 +- .../org/jackhuang/hmcl/ui/VersionPage.java | 189 +++++ .../hmcl/ui/VersionSettingsController.java | 269 ++++++ .../java/org/jackhuang/hmcl/ui/WebStage.java | 2 +- .../hmcl/ui/YggdrasilAccountLoginPane.java | 97 +++ .../hmcl/ui/animation/TransitionHandler.java | 4 +- .../hmcl/ui/construct/ComponentListCell.java | 8 +- .../jackhuang/hmcl/ui/construct/FileItem.java | 2 +- .../hmcl/ui/construct/MessageBox.java | 4 +- .../hmcl/ui/construct/MultiFileItem.java | 157 ++++ .../hmcl/ui/construct/NumberValidator.java | 7 +- .../hmcl/ui/construct/RipplerContainer.java | 193 +++++ .../hmcl/ui/construct/Validator.java | 6 + .../ui/download/AdditionalInstallersPage.java | 4 +- .../ui/download/DownloadWizardProvider.java | 134 +++ .../hmcl/ui/download/InstallTypePage.java | 55 ++ .../ui/download/InstallerWizardProvider.java | 4 +- .../hmcl/ui/download/InstallersPage.java | 137 ++++ .../hmcl/ui/download/ModpackPage.java | 133 +++ .../hmcl/ui/download/VersionsPage.java | 6 +- .../hmcl/ui/download/VersionsPageItem.java | 4 +- .../ui/export/ModpackFileSelectionPage.java | 34 +- .../hmcl/ui/export/ModpackInfoPage.java | 18 +- .../ui/wizard/AbstractWizardDisplayer.java | 150 ++++ .../ui/wizard/DefaultWizardDisplayer.java | 115 +++ .../org/jackhuang/hmcl/ui/wizard/Summary.java | 2 +- .../hmcl/ui/wizard/WizardController.java | 125 +++ .../hmcl/upgrade/AppDataUpgrader.java | 20 +- .../hmcl/upgrade/NewFileUpgrader.java | 6 +- .../jackhuang/hmcl/upgrade/UpdateChecker.java | 4 +- .../main/kotlin/org/jackhuang/hmcl/Main.kt | 102 --- .../org/jackhuang/hmcl/ui/AccountItem.kt | 128 --- .../org/jackhuang/hmcl/ui/AccountsPage.kt | 169 ---- .../org/jackhuang/hmcl/ui/AdvancedListBox.kt | 55 -- .../org/jackhuang/hmcl/ui/Controllers.kt | 81 -- .../kotlin/org/jackhuang/hmcl/ui/Decorator.kt | 457 ----------- .../kotlin/org/jackhuang/hmcl/ui/FXUtils.kt | 270 ------ .../jackhuang/hmcl/ui/LeftPaneController.kt | 116 --- .../kotlin/org/jackhuang/hmcl/ui/MainPage.kt | 107 --- .../org/jackhuang/hmcl/ui/ModController.kt | 114 --- .../org/jackhuang/hmcl/ui/ProfilePage.kt | 88 -- .../main/kotlin/org/jackhuang/hmcl/ui/SVG.kt | 65 -- .../org/jackhuang/hmcl/ui/SettingsPage.kt | 117 --- .../org/jackhuang/hmcl/ui/VersionPage.kt | 139 ---- .../hmcl/ui/VersionSettingsController.kt | 252 ------ .../hmcl/ui/YggdrasilAccountLoginPane.kt | 82 -- .../hmcl/ui/construct/MultiFileItem.kt | 111 --- .../hmcl/ui/construct/RipplerContainer.kt | 179 ---- .../ui/download/DownloadWizardProvider.kt | 117 --- .../hmcl/ui/download/InstallTypePage.kt | 49 -- .../hmcl/ui/download/InstallersPage.kt | 105 --- .../jackhuang/hmcl/ui/download/ModpackPage.kt | 108 --- .../hmcl/ui/wizard/AbstractWizardDisplayer.kt | 137 ---- .../hmcl/ui/wizard/DefaultWizardDisplayer.kt | 93 --- .../hmcl/ui/wizard/WizardController.kt | 95 --- .../kotlin/org/jackhuang/hmcl/util/Lang.kt | 123 --- .../kotlin/org/jackhuang/hmcl/util/Lib.kt | 44 - .../org/jackhuang/hmcl/util/Properties.kt | 767 ------------------ .../java/org/jackhuang/hmcl/task/Task.java | 22 +- .../jackhuang/hmcl/util/AutoTypingMap.java | 5 + .../java/org/jackhuang/hmcl/util/Lang.java | 54 +- .../src/test/java/org/jackhuang/hmcl/Test.kt | 152 ---- README.md | 108 ++- build.gradle | 10 - 90 files changed, 4192 insertions(+), 4615 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/Main.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListBox.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionSettingsController.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/YggdrasilAccountLoginPane.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/RipplerContainer.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallTypePage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/AbstractWizardDisplayer.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/Main.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/AccountItem.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/AccountsPage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/AdvancedListBox.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/Controllers.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/Decorator.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/FXUtils.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/LeftPaneController.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/MainPage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/ModController.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/ProfilePage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/SVG.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/SettingsPage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionPage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/VersionSettingsController.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/YggdrasilAccountLoginPane.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/construct/MultiFileItem.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/construct/RipplerContainer.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/download/InstallTypePage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/download/InstallersPage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/download/ModpackPage.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/wizard/AbstractWizardDisplayer.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/wizard/DefaultWizardDisplayer.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/wizard/WizardController.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/util/Lang.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/util/Lib.kt delete mode 100644 HMCL/src/main/kotlin/org/jackhuang/hmcl/util/Properties.kt delete mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/Test.kt diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java new file mode 100644 index 000000000..439bef307 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -0,0 +1,98 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl; + +import com.jfoenix.concurrency.JFXUtilities; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.stage.Stage; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.util.*; + +import java.io.File; +import java.util.ResourceBundle; +import java.util.logging.Level; + +public final class Main extends Application { + + @Override + public void start(Stage primaryStage) { + // When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false, + // Stage.show() cannot work again because JavaFX Toolkit have already shut down. + Platform.setImplicitExit(false); + Controllers.initialize(primaryStage); + primaryStage.setResizable(false); + primaryStage.setScene(Controllers.getScene()); + primaryStage.show(); + } + + public static void main(String[] args) { + NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher"); + Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER; + + launch(args); + } + + public static void stopApplication() { + JFXUtilities.runInFX(() -> { + stopWithoutPlatform(); + Platform.exit(); + }); + } + + public static void stopWithoutPlatform() { + JFXUtilities.runInFX(() -> { + Controllers.getStage().close(); + Schedulers.shutdown(); + }); + } + + public static String i18n(String key) { + try { + return RESOURCE_BUNDLE.getString(key); + } catch (Exception e) { + Logging.LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e); + return key; + } + } + + public static File getWorkingDirectory(String folder) { + String home = System.getProperty("user.home", "."); + switch (OperatingSystem.CURRENT_OS) { + case LINUX: + return new File(home, "." + folder + "/"); + case WINDOWS: + String appdata = System.getenv("APPDATA"); + return new File(Lang.nonNull(appdata, home), "." + folder + "/"); + case OSX: + return new File(home, "Library/Application Support/" + folder); + default: + return new File(home, folder + "/"); + } + } + + public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft"); + + public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"; + public static final String NAME = "HMCL"; + public static final String TITLE = NAME + " " + VERSION; + public static final File APPDATA = getWorkingDirectory("hmcl"); + public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle(); +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java index e225bd1d7..ac45079d0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java @@ -29,7 +29,7 @@ import org.jackhuang.hmcl.task.Scheduler; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.DialogController; -import org.jackhuang.hmcl.ui.FXUtilsKt; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.NetworkUtils; import java.io.File; @@ -80,9 +80,9 @@ public final class AccountHelper { } public static Image getSkin(YggdrasilAccount account, double scaleRatio) { - if (account.getSelectedProfile() == null) return FXUtilsKt.DEFAULT_ICON; + if (account.getSelectedProfile() == null) return FXUtils.DEFAULT_ICON; String name = account.getSelectedProfile().getName(); - if (name == null) return FXUtilsKt.DEFAULT_ICON; + if (name == null) return FXUtils.DEFAULT_ICON; File file = getSkinFile(name); if (file.exists()) { Image original = new Image("file:" + file.getAbsolutePath()); @@ -91,7 +91,7 @@ public final class AccountHelper { original.getHeight() * scaleRatio, false, false); } - return FXUtilsKt.DEFAULT_ICON; + return FXUtils.DEFAULT_ICON; } public static Rectangle2D getViewport(double scaleRatio) { @@ -128,7 +128,7 @@ public final class AccountHelper { @Override public void execute() throws Exception { if (account.canLogIn() && (account.getSelectedProfile() == null || refresh)) - DialogController.INSTANCE.logIn(account); + DialogController.logIn(account); GameProfile profile = account.getSelectedProfile(); if (profile == null) return; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 76c166b19..9191b9727 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -26,32 +26,28 @@ import org.jackhuang.hmcl.util.CompressingUtils; import org.jackhuang.hmcl.util.Constants; import java.io.File; -import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Objects; public final class HMCLModpackInstallTask extends Task { private final File zipFile; - private final String version; + private final String id; private final HMCLGameRepository repository; private final DefaultDependencyManager dependency; private final List dependencies = new LinkedList<>(); private final List dependents = new LinkedList<>(); - public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String id) throws IOException { + public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String id) { dependency = profile.getDependency(); repository = profile.getRepository(); this.zipFile = zipFile; - this.version = id; + this.id = id; if (repository.hasVersion(id)) throw new IllegalArgumentException("Version " + id + " already exists"); - String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json"); - Version version = Constants.GSON.fromJson(json, Version.class).setJar(null); dependents.add(dependency.gameBuilder().name(id).gameVersion(modpack.getGameVersion()).buildAsync()); - dependencies.add(new VersionJsonSaveTask(repository, version)); onDone().register(event -> { if (event.isFailed()) repository.removeVersionFromDisk(id); @@ -70,7 +66,11 @@ public final class HMCLModpackInstallTask extends Task { @Override public void execute() throws Exception { - CompressingUtils.unzip(zipFile, repository.getRunDirectory(version), + String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json"); + Version version = Constants.GSON.fromJson(json, Version.class).setJar(null); + dependencies.add(new VersionJsonSaveTask(repository, version)); + + CompressingUtils.unzip(zipFile, repository.getRunDirectory(id), "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), false); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index ba643cae2..623ffbb0a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -66,31 +66,31 @@ public final class LauncherHelper { Version version = repository.getVersion(selectedVersion); VersionSetting setting = profile.getVersionSetting(selectedVersion); - Controllers.INSTANCE.dialog(launchingStepsPane); - TaskExecutor executor = Task.of(v -> emitStatus(LoadingState.DEPENDENCIES), Schedulers.javafx()) + Controllers.dialog(launchingStepsPane); + TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES)) .then(dependencyManager.checkGameCompletionAsync(version)) - .then(Task.of(v -> emitStatus(LoadingState.MODS), Schedulers.javafx())) + .then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS))) .then(new CurseCompletionTask(dependencyManager, selectedVersion)) - .then(Task.of(v -> emitStatus(LoadingState.LOGIN), Schedulers.javafx())) - .then(Task.of(v -> { + .then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGIN))) + .then(Task.of(variables -> { try { - v.set("account", account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy())); + variables.set("account", account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy())); } catch (AuthenticationException e) { - v.set("account", DialogController.INSTANCE.logIn(account)); - JFXUtilities.runInFX(() -> Controllers.INSTANCE.dialog(launchingStepsPane)); + variables.set("account", DialogController.logIn(account)); + JFXUtilities.runInFX(() -> Controllers.dialog(launchingStepsPane)); } })) - .then(Task.of(v -> emitStatus(LoadingState.LAUNCHING), Schedulers.javafx())) - .then(Task.of(v -> { - v.set("launcher", new HMCLGameLauncher( - repository, selectedVersion, v.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(v.get("account"), setting) + .then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING))) + .then(Task.of(variables -> { + variables.set("launcher", new HMCLGameLauncher( + repository, selectedVersion, variables.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(variables.get("account"), setting) )); })) - .then(v -> v.get("launcher").launchAsync()) - .then(Task.of(v -> { - PROCESSES.add(v.get(DefaultLauncher.LAUNCH_ASYNC_ID)); + .then(variables -> variables.get("launcher").launchAsync()) + .then(Task.of(variables -> { + PROCESSES.add(variables.get(DefaultLauncher.LAUNCH_ASYNC_ID)); if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE) - Main.Companion.stop(); + Main.stopApplication(); })) .executor(); @@ -106,7 +106,7 @@ public final class LauncherHelper { @Override public void onTerminate() { - Platform.runLater(Controllers.INSTANCE::closeDialog); + Platform.runLater(Controllers::closeDialog); } }); @@ -122,7 +122,7 @@ public final class LauncherHelper { public void emitStatus(LoadingState state) { if (state == LoadingState.DONE) - Controllers.INSTANCE.closeDialog(); + Controllers.closeDialog(); launchingStepsPane.setCurrentState(state.toString()); launchingStepsPane.setSteps((state.ordinal() + 1) + " / " + LoadingState.values().length); @@ -131,7 +131,7 @@ public final class LauncherHelper { private void checkExit(LauncherVisibility v) { switch (v) { case HIDE_AND_REOPEN: - Platform.runLater(Controllers.INSTANCE.getStage()::show); + Platform.runLater(Controllers.getStage()::show); break; case KEEP: // No operations here @@ -143,7 +143,7 @@ public final class LauncherHelper { // Shut down the platform when user closed log window. Platform.setImplicitExit(true); // If we use Main.stop(), log window will be halt immediately. - Main.Companion.stopWithoutJavaFXPlatform(); + Main.stopWithoutPlatform(); }); break; } @@ -215,7 +215,7 @@ public final class LauncherHelper { switch (visibility) { case HIDE_AND_REOPEN: Platform.runLater(() -> { - Controllers.INSTANCE.getStage().hide(); + Controllers.getStage().hide(); emitStatus(LoadingState.DONE); }); break; @@ -226,7 +226,7 @@ public final class LauncherHelper { break; case HIDE: Platform.runLater(() -> { - Controllers.INSTANCE.getStage().close(); + Controllers.getStage().close(); emitStatus(LoadingState.DONE); }); break; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index c8a5039ba..d38cf6623 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -34,7 +34,7 @@ public final class Config { private String backgroundImage = null; @SerializedName("commonpath") - private String commonDirectory = Main.getMinecraftDirectory().getAbsolutePath(); + private String commonDirectory = Main.MINECRAFT_DIRECTORY.getAbsolutePath(); @SerializedName("proxyType") private int proxyType = 0; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index a8357b267..a9c60ece2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -130,7 +130,7 @@ public class Settings { else { Logging.LOG.config("No settings file here, may be first loading."); if (!c.getConfigurations().containsKey(HOME_PROFILE)) - c.getConfigurations().put(HOME_PROFILE, new Profile(HOME_PROFILE, Main.getMinecraftDirectory())); + c.getConfigurations().put(HOME_PROFILE, new Profile(HOME_PROFILE, Main.MINECRAFT_DIRECTORY)); } return c; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java new file mode 100644 index 000000000..a4613f497 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java @@ -0,0 +1,143 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXProgressBar; +import com.jfoenix.controls.JFXRadioButton; +import javafx.beans.binding.Bindings; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleGroup; +import javafx.scene.effect.BlurType; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.OfflineAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.task.Schedulers; + +public final class AccountItem extends StackPane { + + private final Account account; + + @FXML + private Pane icon; + @FXML private VBox content; + @FXML private StackPane header; + @FXML private StackPane body; + @FXML private JFXButton btnDelete; + @FXML private JFXButton btnRefresh; + @FXML private Label lblUser; + @FXML private JFXRadioButton chkSelected; + @FXML private Label lblType; + @FXML private JFXProgressBar pgsSkin; + @FXML private ImageView portraitView; + @FXML private HBox buttonPane; + + public AccountItem(int i, Account account, ToggleGroup toggleGroup) { + this.account = account; + + FXUtils.loadFXML(this, "/assets/fxml/account-item.fxml"); + + FXUtils.limitWidth(this, 160); + FXUtils.limitHeight(this, 156); + + setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -0.5, 1.0)); + + chkSelected.setToggleGroup(toggleGroup); + btnDelete.setGraphic(SVG.delete("black", 15, 15)); + btnRefresh.setGraphic(SVG.refresh("black", 15, 15)); + + // create content + String headerColor = getDefaultColor(i % 12); + header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor); + + // create image view + icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 32.0, header.boundsInParentProperty(), icon.heightProperty())); + + chkSelected.getProperties().put("account", account); + chkSelected.setSelected(Settings.INSTANCE.getSelectedAccount() == account); + lblUser.setText(account.getUsername()); + lblType.setText(AccountsPage.accountType(account)); + + if (account instanceof YggdrasilAccount) { + btnRefresh.setOnMouseClicked(e -> { + pgsSkin.setVisible(true); + AccountHelper.refreshSkinAsync((YggdrasilAccount) account) + .subscribe(Schedulers.javafx(), this::loadSkin); + }); + AccountHelper.loadSkinAsync((YggdrasilAccount) account) + .subscribe(Schedulers.javafx(), this::loadSkin); + } + + if (account instanceof OfflineAccount) { // Offline Account cannot be refreshed, + buttonPane.getChildren().remove(btnRefresh); + } + } + + private void loadSkin() { + if (!(account instanceof YggdrasilAccount)) + return; + + pgsSkin.setVisible(false); + portraitView.setViewport(AccountHelper.getViewport(4)); + portraitView.setImage(AccountHelper.getSkin((YggdrasilAccount) account, 4)); + FXUtils.limitSize(portraitView, 32, 32); + } + + private String getDefaultColor(int i) { + switch (i) { + case 0: return "#8F3F7E"; + case 1: return "#B5305F"; + case 2: return "#CE584A"; + case 3: return "#DB8D5C"; + case 4: return "#DA854E"; + case 5: return "#E9AB44"; + case 6: return "#FEE435"; + case 7: return "#99C286"; + case 8: return "#01A05E"; + case 9: return "#4A8895"; + case 10: return "#16669B"; + case 11: return "#2F65A5"; + case 12: return "#4E6A9C"; + default: return "#FFFFFF"; + } + } + + public Account getAccount() { + return account; + } + + public void setSelected(boolean selected) { + chkSelected.setSelected(selected); + } + + public void setOnDeleteButtonMouseClicked(EventHandler eventHandler) { + btnDelete.setOnMouseClicked(eventHandler); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java new file mode 100644 index 000000000..f51380687 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java @@ -0,0 +1,180 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.*; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.OfflineAccount; +import org.jackhuang.hmcl.auth.OfflineAccountFactory; +import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; +import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.wizard.DecoratorPage; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public final class AccountsPage extends StackPane implements DecoratorPage { + private final StringProperty title = new SimpleStringProperty(this, "title", "Accounts"); + + @FXML + private ScrollPane scrollPane; + @FXML private JFXMasonryPane masonryPane; + @FXML private JFXDialog dialog; + @FXML private JFXTextField txtUsername; + @FXML private JFXPasswordField txtPassword; + @FXML private Label lblCreationWarning; + @FXML private JFXComboBox cboType; + @FXML private JFXProgressBar progressBar; + + { + FXUtils.loadFXML(this, "/assets/fxml/account.fxml"); + + getChildren().remove(dialog); + dialog.setDialogContainer(this); + + FXUtils.smoothScrolling(scrollPane); + FXUtils.setValidateWhileTextChanged(txtUsername); + FXUtils.setValidateWhileTextChanged(txtPassword); + + cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> { + txtPassword.setVisible(newValue.intValue() != 0); + }); + cboType.getSelectionModel().select(0); + + txtPassword.setOnAction(e -> onCreationAccept()); + txtUsername.setOnAction(e -> onCreationAccept()); + + FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), account -> { + for (Node node : masonryPane.getChildren()) + if (node instanceof AccountItem) + ((AccountItem) node).setSelected(account == ((AccountItem) node).getAccount()); + }); + + loadAccounts(); + + if (Settings.INSTANCE.getAccounts().isEmpty()) + addNewAccount(); + } + + public void loadAccounts() { + List children = new LinkedList<>(); + int i = 0; + ToggleGroup group = new ToggleGroup(); + for (Map.Entry entry : Settings.INSTANCE.getAccounts().entrySet()) { + children.add(buildNode(++i, entry.getValue(), group)); + } + group.selectedToggleProperty().addListener((a, b, newValue) -> { + if (newValue != null) + Settings.INSTANCE.setSelectedAccount((Account) newValue.getProperties().get("account")); + }); + FXUtils.resetChildren(masonryPane, children); + Platform.runLater(() -> { + masonryPane.requestLayout(); + scrollPane.requestLayout(); + }); + } + + private Node buildNode(int i, Account account, ToggleGroup group) { + AccountItem item = new AccountItem(i, account, group); + item.setOnDeleteButtonMouseClicked(e -> { + Settings.INSTANCE.deleteAccount(account.getUsername()); + Platform.runLater(this::loadAccounts); + }); + return item; + } + + public void addNewAccount() { + txtUsername.setText(""); + txtPassword.setText(""); + dialog.show(); + } + + public void onCreationAccept() { + int type = cboType.getSelectionModel().getSelectedIndex(); + String username = txtUsername.getText(); + String password = txtPassword.getText(); + progressBar.setVisible(true); + lblCreationWarning.setText(""); + Task.ofResult("create_account", () -> { + try { + Account account; + switch (type) { + case 0: account = OfflineAccountFactory.INSTANCE.fromUsername(username); break; + case 1: account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password); break; + default: throw new Error(); + } + + account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy()); + return account; + } catch (Exception e) { + return e; + } + }).subscribe(Schedulers.javafx(), variables -> { + Object account = variables.get("create_account"); + if (account instanceof Account) { + Settings.INSTANCE.addAccount((Account) account); + dialog.close(); + loadAccounts(); + } else if (account instanceof InvalidCredentialsException) { + lblCreationWarning.setText(Main.i18n("login.wrong_password")); + } else if (account instanceof Exception) { + lblCreationWarning.setText(((Exception) account).getLocalizedMessage()); + } + progressBar.setVisible(false); + }); + } + + public void onCreationCancel() { + dialog.close(); + } + + public String getTitle() { + return title.get(); + } + + @Override + public StringProperty titleProperty() { + return title; + } + + public void setTitle(String title) { + this.title.set(title); + } + + public static String accountType(Account account) { + if (account instanceof OfflineAccount) return Main.i18n("login.methods.offline"); + else if (account instanceof YggdrasilAccount) return Main.i18n("login.methods.yggdrasil"); + else throw new Error(Main.i18n("login.methods.no_method") + ": " + account); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListBox.java new file mode 100644 index 000000000..6a5d36c99 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AdvancedListBox.java @@ -0,0 +1,57 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + +public class AdvancedListBox extends ScrollPane { + private final VBox container = new VBox(); + + { + setContent(container); + + FXUtils.smoothScrolling(this); + + setFitToHeight(true); + setFitToWidth(true); + setHbarPolicy(ScrollBarPolicy.NEVER); + + container.setSpacing(5); + container.getStyleClass().add("advanced-list-box-content"); + } + + public AdvancedListBox add(Node child) { + if (child instanceof Pane) + container.getChildren().add(child); + else { + StackPane pane = new StackPane(); + pane.getStyleClass().add("advanced-list-box-item"); + pane.getChildren().setAll(child); + container.getChildren().add(pane); + } + return this; + } + + public AdvancedListBox startCategory(String category) { + return add(new ClassTitle(category)); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java new file mode 100644 index 000000000..18461ea4a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -0,0 +1,113 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXDialog; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.layout.Region; +import javafx.stage.Stage; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.JavaVersion; + +public final class Controllers { + + private static Scene scene; + private static Stage stage; + private static MainPage mainPage = new MainPage(); + private static SettingsPage settingsPage = null; + private static VersionPage versionPage = null; + private static LeftPaneController leftPaneController; + private static Decorator decorator; + + public static Scene getScene() { + return scene; + } + + public static Stage getStage() { + return stage; + } + + // FXThread + public static SettingsPage getSettingsPage() { + if (settingsPage == null) + settingsPage = new SettingsPage(); + return settingsPage; + } + + // FXThread + public static VersionPage getVersionPage() { + if (versionPage == null) + versionPage = new VersionPage(); + return versionPage; + } + + public static Decorator getDecorator() { + return decorator; + } + + public static MainPage getMainPage() { + return mainPage; + } + + public static LeftPaneController getLeftPaneController() { + return leftPaneController; + } + + public static void initialize(Stage stage) { + Controllers.stage = stage; + + decorator = new Decorator(stage, mainPage, Main.TITLE, false, true); + decorator.showPage(null); + leftPaneController = new LeftPaneController(decorator.getLeftPane()); + + Settings.INSTANCE.onProfileLoading(); + Task.of(JavaVersion::initialize).start(); + + decorator.setCustomMaximize(false); + + scene = new Scene(decorator, 804, 521); + scene.getStylesheets().addAll(FXUtils.STYLESHEETS); + stage.setMinWidth(800); + stage.setMaxWidth(800); + stage.setMinHeight(480); + stage.setMaxHeight(480); + + stage.getIcons().add(new Image("/assets/img/icon.png")); + stage.setTitle(Main.TITLE); + } + + public static JFXDialog dialog(Region content) { + return decorator.showDialog(content); + } + + public static void dialog(String text) { + dialog(new MessageDialogPane(text, decorator.getDialog())); + } + + public static void closeDialog() { + decorator.getDialog().close(); + } + + public static void navigate(Node node) { + decorator.showPage(node); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java new file mode 100644 index 000000000..88cdfa33b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java @@ -0,0 +1,519 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialog; +import com.jfoenix.controls.JFXDrawer; +import com.jfoenix.controls.JFXHamburger; +import com.jfoenix.effects.JFXDepthManager; +import com.jfoenix.svg.SVGGlyph; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Rectangle2D; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.ui.animation.AnimationProducer; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionHandler; +import org.jackhuang.hmcl.ui.wizard.*; +import org.jackhuang.hmcl.util.Lang; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public final class Decorator extends StackPane implements AbstractWizardDisplayer { + private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE), + glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); }); + private static final SVGGlyph resizeMax = Lang.apply(new SVGGlyph(0, "RESIZE_MAX", "M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34 25-60t59-26z", Color.WHITE), + glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); }); + private static final SVGGlyph resizeMin = Lang.apply(new SVGGlyph(0, "RESIZE_MIN", "M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z", Color.WHITE), + glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); }); + private static final SVGGlyph close = Lang.apply(new SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE), + glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); }); + + private final ObjectProperty onCloseButtonAction = new SimpleObjectProperty<>(Main::stopApplication); + private final BooleanProperty customMaximize = new SimpleBooleanProperty(false); + + private final Stage primaryStage; + private final Node mainPage; + private final boolean max, min; + private final WizardController wizardController = new WizardController(this); + private final Queue cancelQueue = new ConcurrentLinkedQueue<>(); + + private double xOffset, yOffset, newX, newY, initX, initY; + private boolean allowMove, isDragging, dialogShown, maximized; + private BoundingBox originalBox, maximizedBox; + private TransitionHandler animationHandler; + + @FXML + private StackPane contentPlaceHolder; + @FXML + private StackPane drawerWrapper; + @FXML + private BorderPane titleContainer; + @FXML + private BorderPane leftRootPane; + @FXML + private HBox buttonsContainer; + @FXML + private JFXButton backNavButton; + @FXML + private JFXButton refreshNavButton; + @FXML + private JFXButton closeNavButton; + @FXML + private JFXButton refreshMenuButton; + @FXML + private JFXButton addMenuButton; + @FXML + private Label titleLabel; + @FXML + private Label lblTitle; + @FXML + private AdvancedListBox leftPane; + @FXML + private JFXDrawer drawer; + @FXML + private StackPane titleBurgerContainer; + @FXML + private JFXHamburger titleBurger; + @FXML + private JFXDialog dialog; + @FXML + private JFXButton btnMin; + @FXML + private JFXButton btnMax; + @FXML + private JFXButton btnClose; + + public Decorator(Stage primaryStage, Node mainPage, String title) { + this(primaryStage, mainPage, title, true, true); + } + + public Decorator(Stage primaryStage, Node mainPage, String title, boolean max, boolean min) { + this.primaryStage = primaryStage; + this.mainPage = mainPage; + this.max = max; + this.min = min; + + FXUtils.loadFXML(this, "/assets/fxml/decorator.fxml"); + + primaryStage.initStyle(StageStyle.UNDECORATED); + btnClose.setGraphic(close); + btnMin.setGraphic(minus); + btnMax.setGraphic(resizeMax); + + lblTitle.setText(title); + + buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY))); + titleContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { + if (event.getClickCount() == 2) + btnMax.fire(); + }); + + drawerWrapper.getChildren().remove(dialog); + dialog.setDialogContainer(drawerWrapper); + dialog.setOnDialogClosed(e -> dialogShown = false); + dialog.setOnDialogOpened(e -> dialogShown = true); + + if (!min) buttonsContainer.getChildren().remove(btnMin); + if (!max) buttonsContainer.getChildren().remove(btnMax); + + JFXDepthManager.setDepth(titleContainer, 1); + titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true); + titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { + if (!isDragging) allowMove = false; + }); + + animationHandler = new TransitionHandler(contentPlaceHolder); + + FXUtils.setOverflowHidden((Pane) lookup("#contentPlaceHolderRoot")); + FXUtils.setOverflowHidden(drawerWrapper); + } + + public void onMouseMoved(MouseEvent mouseEvent) { + if (!primaryStage.isMaximized() && !primaryStage.isFullScreen() && !maximized) { + if (!primaryStage.isResizable()) + updateInitMouseValues(mouseEvent); + else { + double x = mouseEvent.getX(), y = mouseEvent.getY(); + Bounds boundsInParent = getBoundsInParent(); + if (getBorder() != null && getBorder().getStrokes().size() > 0) { + double borderWidth = this.contentPlaceHolder.snappedLeftInset(); + if (this.isRightEdge(x, y, boundsInParent)) { + if (y < borderWidth) { + setCursor(Cursor.NE_RESIZE); + } else if (y > this.getHeight() - borderWidth) { + setCursor(Cursor.SE_RESIZE); + } else { + setCursor(Cursor.E_RESIZE); + } + } else if (this.isLeftEdge(x, y, boundsInParent)) { + if (y < borderWidth) { + setCursor(Cursor.NW_RESIZE); + } else if (y > this.getHeight() - borderWidth) { + setCursor(Cursor.SW_RESIZE); + } else { + setCursor(Cursor.W_RESIZE); + } + } else if (this.isTopEdge(x, y, boundsInParent)) { + setCursor(Cursor.N_RESIZE); + } else if (this.isBottomEdge(x, y, boundsInParent)) { + setCursor(Cursor.S_RESIZE); + } else { + setCursor(Cursor.DEFAULT); + } + + this.updateInitMouseValues(mouseEvent); + } + } + } else { + setCursor(Cursor.DEFAULT); + } + } + + public void onMouseReleased() { + isDragging = false; + } + + public void onMouseDragged(MouseEvent mouseEvent) { + this.isDragging = true; + if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) { + if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) { + this.newX = mouseEvent.getScreenX(); + this.newY = mouseEvent.getScreenY(); + double deltax = this.newX - this.initX; + double deltay = this.newY - this.initY; + Cursor cursor = this.getCursor(); + if (Cursor.E_RESIZE == cursor) { + this.setStageWidth(this.primaryStage.getWidth() + deltax); + mouseEvent.consume(); + } else if (Cursor.NE_RESIZE == cursor) { + if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) { + this.primaryStage.setY(this.primaryStage.getY() + deltay); + } + + this.setStageWidth(this.primaryStage.getWidth() + deltax); + mouseEvent.consume(); + } else if (Cursor.SE_RESIZE == cursor) { + this.setStageWidth(this.primaryStage.getWidth() + deltax); + this.setStageHeight(this.primaryStage.getHeight() + deltay); + mouseEvent.consume(); + } else if (Cursor.S_RESIZE == cursor) { + this.setStageHeight(this.primaryStage.getHeight() + deltay); + mouseEvent.consume(); + } else if (Cursor.W_RESIZE == cursor) { + if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) { + this.primaryStage.setX(this.primaryStage.getX() + deltax); + } + + mouseEvent.consume(); + } else if (Cursor.SW_RESIZE == cursor) { + if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) { + this.primaryStage.setX(this.primaryStage.getX() + deltax); + } + + this.setStageHeight(this.primaryStage.getHeight() + deltay); + mouseEvent.consume(); + } else if (Cursor.NW_RESIZE == cursor) { + if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) { + this.primaryStage.setX(this.primaryStage.getX() + deltax); + } + + if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) { + this.primaryStage.setY(this.primaryStage.getY() + deltay); + } + + mouseEvent.consume(); + } else if (Cursor.N_RESIZE == cursor) { + if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) { + this.primaryStage.setY(this.primaryStage.getY() + deltay); + } + + mouseEvent.consume(); + } else if (this.allowMove) { + this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset); + this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset); + mouseEvent.consume(); + } + } + } + } + + public void onMin() { + primaryStage.setIconified(true); + } + + public void onMax() { + if (!max) return; + if (!this.isCustomMaximize()) { + this.primaryStage.setMaximized(!this.primaryStage.isMaximized()); + this.maximized = this.primaryStage.isMaximized(); + if (this.primaryStage.isMaximized()) { + this.btnMax.setGraphic(resizeMin); + this.btnMax.setTooltip(new Tooltip("Restore Down")); + } else { + this.btnMax.setGraphic(resizeMax); + this.btnMax.setTooltip(new Tooltip("Maximize")); + } + } else { + if (!this.maximized) { + this.originalBox = new BoundingBox(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight()); + Screen screen = Screen.getScreensForRectangle(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight()).get(0); + Rectangle2D bounds = screen.getVisualBounds(); + this.maximizedBox = new BoundingBox(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()); + primaryStage.setX(this.maximizedBox.getMinX()); + primaryStage.setY(this.maximizedBox.getMinY()); + primaryStage.setWidth(this.maximizedBox.getWidth()); + primaryStage.setHeight(this.maximizedBox.getHeight()); + this.btnMax.setGraphic(resizeMin); + this.btnMax.setTooltip(new Tooltip("Restore Down")); + } else { + primaryStage.setX(this.originalBox.getMinX()); + primaryStage.setY(this.originalBox.getMinY()); + primaryStage.setWidth(this.originalBox.getWidth()); + primaryStage.setHeight(this.originalBox.getHeight()); + this.originalBox = null; + this.btnMax.setGraphic(resizeMax); + this.btnMax.setTooltip(new Tooltip("Maximize")); + } + + this.maximized = !this.maximized; + } + } + + public void onClose() { + onCloseButtonAction.get().run(); + } + + private void updateInitMouseValues(MouseEvent mouseEvent) { + initX = mouseEvent.getScreenX(); + initY = mouseEvent.getScreenY(); + xOffset = mouseEvent.getSceneX(); + yOffset = mouseEvent.getSceneY(); + } + + private boolean isRightEdge(double x, double y, Bounds boundsInParent) { + return x < getWidth() && x > getWidth() - contentPlaceHolder.snappedLeftInset(); + } + + private boolean isTopEdge(double x, double y, Bounds boundsInParent) { + return y >= 0 && y < contentPlaceHolder.snappedLeftInset(); + } + + private boolean isBottomEdge(double x, double y, Bounds boundsInParent) { + return y < getHeight() && y > getHeight() - contentPlaceHolder.snappedLeftInset(); + } + + private boolean isLeftEdge(double x, double y, Bounds boundsInParent) { + return x >= 0 && x < contentPlaceHolder.snappedLeftInset(); + } + + private boolean setStageWidth(double width) { + if (width >= primaryStage.getMinWidth() && width >= titleContainer.getMinWidth()) { + primaryStage.setWidth(width); + initX = newX; + return true; + } else { + if (width >= primaryStage.getMinWidth() && width <= titleContainer.getMinWidth()) + primaryStage.setWidth(titleContainer.getMinWidth()); + + return false; + } + } + + private boolean setStageHeight(double height) { + if (height >= primaryStage.getMinHeight() && height >= titleContainer.getHeight()) { + primaryStage.setHeight(height); + initY = newY; + return true; + } else { + if (height >= primaryStage.getMinHeight() && height <= titleContainer.getHeight()) + primaryStage.setHeight(titleContainer.getHeight()); + + return false; + } + } + + public void setMaximized(boolean maximized) { + if (this.maximized != maximized) { + Platform.runLater(btnMax::fire); + } + } + + private void setContent(Node content, AnimationProducer animation) { + animationHandler.setContent(content, animation); + + if (content instanceof Region) { + ((Region) content).setMinSize(0, 0); + FXUtils.setOverflowHidden((Region) content); + } + + backNavButton.setDisable(!wizardController.canPrev()); + + if (content instanceof Refreshable) + refreshNavButton.setVisible(true); + + if (content != mainPage) + closeNavButton.setVisible(true); + + String prefix = category == null ? "" : category + " - "; + + titleLabel.textProperty().unbind(); + + if (content instanceof WizardPage) + titleLabel.setText(prefix + ((WizardPage) content).getTitle()); + + if (content instanceof DecoratorPage) + titleLabel.textProperty().bind(((DecoratorPage) content).titleProperty()); + } + + private String category; + private Node nowPage; + + public void showPage(Node content) { + Node c = content == null ? mainPage : content; + onEnd(); + if (nowPage instanceof DecoratorPage) + ((DecoratorPage) nowPage).onClose(); + nowPage = content; + + setContent(c, ContainerAnimations.FADE.getAnimationProducer()); + + if (c instanceof Region) { + // Let root pane fix window size. + StackPane parent = (StackPane) c.getParent(); + ((Region) c).prefWidthProperty().bind(parent.widthProperty()); + ((Region) c).prefHeightProperty().bind(parent.heightProperty()); + } + } + + public JFXDialog showDialog(Region content) { + dialog.setContent(content); + if (!dialogShown) + dialog.show(); + return dialog; + } + + public void startWizard(WizardProvider wizardProvider) { + startWizard(wizardProvider, null); + } + + public void startWizard(WizardProvider wizardProvider, String category) { + this.category = category; + wizardController.setProvider(wizardProvider); + wizardController.onStart(); + } + + @Override + public void onStart() { + backNavButton.setVisible(true); + backNavButton.setDisable(false); + closeNavButton.setVisible(true); + refreshNavButton.setVisible(false); + } + + @Override + public void onEnd() { + backNavButton.setVisible(false); + closeNavButton.setVisible(false); + refreshNavButton.setVisible(false); + } + + @Override + public void navigateTo(Node page, Navigation.NavigationDirection nav) { + setContent(page, nav.getAnimation().getAnimationProducer()); + } + + public void onRefresh() { + ((Refreshable) contentPlaceHolder.getChildren().get(0)).refresh(); + } + + public void onCloseNav() { + wizardController.onCancel(); + showPage(null); + } + + public void onBack() { + wizardController.onPrev(true); + } + + @Override + public Queue getCancelQueue() { + return cancelQueue; + } + + public Runnable getOnCloseButtonAction() { + return onCloseButtonAction.get(); + } + + public ObjectProperty onCloseButtonActionProperty() { + return onCloseButtonAction; + } + + public void setOnCloseButtonAction(Runnable onCloseButtonAction) { + this.onCloseButtonAction.set(onCloseButtonAction); + } + + public boolean isCustomMaximize() { + return customMaximize.get(); + } + + public BooleanProperty customMaximizeProperty() { + return customMaximize; + } + + public void setCustomMaximize(boolean customMaximize) { + this.customMaximize.set(customMaximize); + } + + @Override + public WizardController getWizardController() { + return wizardController; + } + + public JFXDialog getDialog() { + return dialog; + } + + public JFXButton getAddMenuButton() { + return addMenuButton; + } + + public AdvancedListBox getLeftPane() { + return leftPane; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java index 44edde63a..6527dcbe9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java @@ -29,9 +29,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; public final class DialogController { - public static final DialogController INSTANCE = new DialogController(); - - private DialogController() {} public static AuthInfo logIn(Account account) throws Exception { if (account instanceof YggdrasilAccount) { @@ -41,14 +38,12 @@ public final class DialogController { YggdrasilAccountLoginPane pane = new YggdrasilAccountLoginPane((YggdrasilAccount) account, it -> { res.set(it); latch.countDown(); - Controllers.INSTANCE.closeDialog(); - return Unit.INSTANCE; + Controllers.closeDialog(); }, () -> { latch.countDown(); - Controllers.INSTANCE.closeDialog(); - return Unit.INSTANCE; + Controllers.closeDialog(); }); - pane.dialog = Controllers.INSTANCE.dialog(pane); + pane.setDialog(Controllers.dialog(pane)); }); latch.await(); return Optional.ofNullable(res.get()).orElseThrow(SilentException::new); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java new file mode 100644 index 000000000..e3f2a2f19 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -0,0 +1,303 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.adapters.ReflectionHelper; +import com.jfoenix.controls.*; +import javafx.animation.Animation; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.beans.property.Property; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.beans.value.WeakChangeListener; +import javafx.event.EventHandler; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.WritableImage; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; +import javafx.scene.layout.Region; +import javafx.scene.shape.Rectangle; +import javafx.util.Duration; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Logging; +import org.jackhuang.hmcl.util.OperatingSystem; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.logging.Level; + +import static org.jackhuang.hmcl.util.ReflectionHelper.call; +import static org.jackhuang.hmcl.util.ReflectionHelper.construct; + +public final class FXUtils { + private FXUtils() { + } + + public static void onChange(ObservableValue value, Consumer consumer) { + value.addListener((a, b, c) -> consumer.accept(c)); + } + + public static void onWeakChange(ObservableValue value, Consumer consumer) { + value.addListener(new WeakChangeListener<>((a, b, c) -> consumer.accept(c))); + } + + public static void onChangeAndOperate(ObservableValue value, Consumer consumer) { + onChange(value, consumer); + consumer.accept(value.getValue()); + } + + public static void onWeakChangeAndOperate(ObservableValue value, Consumer consumer) { + onWeakChange(value, consumer); + consumer.accept(value.getValue()); + } + + public static void limitSize(ImageView imageView, double maxWidth, double maxHeight) { + imageView.setPreserveRatio(true); + onChangeAndOperate(imageView.imageProperty(), image -> { + if (image != null && (image.getWidth() > maxWidth || image.getHeight() > maxHeight)) { + imageView.setFitHeight(maxHeight); + imageView.setFitWidth(maxWidth); + } else { + imageView.setFitHeight(-1); + imageView.setFitWidth(-1); + } + }); + } + + public static void setValidateWhileTextChanged(JFXTextField field) { + field.textProperty().addListener(o -> field.validate()); + field.validate(); + } + + public static void setValidateWhileTextChanged(JFXPasswordField field) { + field.textProperty().addListener(o -> field.validate()); + field.validate(); + } + + public static void setOverflowHidden(Region region) { + Rectangle rectangle = new Rectangle(); + rectangle.widthProperty().bind(region.widthProperty()); + rectangle.heightProperty().bind(region.heightProperty()); + region.setClip(rectangle); + } + + public static void limitWidth(Region region, double width) { + region.setMaxWidth(width); + region.setMinWidth(width); + region.setPrefWidth(width); + } + + public static void limitHeight(Region region, double height) { + region.setMaxHeight(height); + region.setMinHeight(height); + region.setPrefHeight(height); + } + + public static void smoothScrolling(ScrollPane scrollPane) { + JFXScrollPane.smoothScrolling(scrollPane); + } + + public static void loadFXML(Node node, String absolutePath) { + FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Main.RESOURCE_BUNDLE); + loader.setRoot(node); + loader.setController(node); + Lang.invoke(() -> loader.load()); + } + + public static WritableImage takeSnapshot(Parent node, double width, double height) { + Scene scene = new Scene(node, width, height); + scene.getStylesheets().addAll(STYLESHEETS); + return scene.snapshot(null); + } + + public static void resetChildren(JFXMasonryPane pane, List children) { + // Fixes mis-repositioning. + ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null); + pane.getChildren().setAll(children); + } + + public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) { + try { + call(construct(Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"), new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false), + "install", node, tooltip); + } catch (Throwable e) { + Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e); + Tooltip.install(node, tooltip); + } + } + + public static boolean alert(Alert.AlertType type, String title, String contentText) { + return alert(type, title, contentText, null); + } + + public static boolean alert(Alert.AlertType type, String title, String contentText, String headerText) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(headerText); + alert.setContentText(contentText); + Optional result = alert.showAndWait(); + return result.isPresent() && result.get() == ButtonType.OK; + } + + public static Optional inputDialog(String title, String contentText) { + return inputDialog(title, contentText, null); + } + + public static Optional inputDialog(String title, String contentText, String headerText) { + return inputDialog(title, contentText, headerText, ""); + } + + public static Optional inputDialog(String title, String contentText, String headerText, String defaultValue) { + TextInputDialog dialog = new TextInputDialog(defaultValue); + dialog.setTitle(title); + dialog.setHeaderText(headerText); + dialog.setContentText(contentText); + return dialog.showAndWait(); + } + + public static void openFolder(File file) { + file.mkdirs(); + String path = file.getAbsolutePath(); + + switch (OperatingSystem.CURRENT_OS) { + case OSX: + try { + Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path}); + } catch (IOException e) { + Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e); + } + break; + default: + try { + java.awt.Desktop.getDesktop().open(file); + } catch (Throwable e) { + Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e); + } + } + } + + public static void bindInt(JFXTextField textField, Property property) { + textField.textProperty().unbind(); + textField.textProperty().bindBidirectional((Property) property, SafeIntStringConverter.INSTANCE); + } + + public static void bindString(JFXTextField textField, Property property) { + textField.textProperty().unbind(); + textField.textProperty().bindBidirectional(property); + } + + public static void bindBoolean(JFXToggleButton toggleButton, Property property) { + toggleButton.selectedProperty().unbind(); + toggleButton.selectedProperty().bindBidirectional(property); + } + + public static void bindBoolean(JFXCheckBox checkBox, Property property) { + checkBox.selectedProperty().unbind(); + checkBox.selectedProperty().bindBidirectional(property); + } + + public static void bindEnum(JFXComboBox comboBox, Property property) { + unbindEnum(comboBox); + ChangeListener listener = (a, b, newValue) -> { + ((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]); + }; + comboBox.getSelectionModel().select(property.getValue().ordinal()); + comboBox.getProperties().put("listener", listener); + comboBox.getSelectionModel().selectedIndexProperty().addListener(listener); + } + + public static void unbindEnum(JFXComboBox comboBox) { + ChangeListener listener = Lang.get(comboBox.getProperties(), "listener", ChangeListener.class, null); + if (listener == null) return; + comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener); + } + + public static void smoothScrolling(ListView listView) { + listView.skinProperty().addListener(o -> { + ScrollBar bar = (ScrollBar) listView.lookup(".scroll-bar"); + Node virtualFlow = listView.lookup(".virtual-flow"); + double[] frictions = new double[]{0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001}; + double[] pushes = new double[]{1}; + double[] derivatives = new double[frictions.length]; + + Timeline timeline = new Timeline(); + bar.addEventHandler(MouseEvent.DRAG_DETECTED, e -> timeline.stop()); + + EventHandler scrollEventHandler = event -> { + if (event.getEventType() == ScrollEvent.SCROLL) { + int direction = event.getDeltaY() > 0 ? -1 : 1; + for (int i = 0; i < pushes.length; ++i) + derivatives[i] += direction * pushes[i]; + if (timeline.getStatus() == Animation.Status.STOPPED) + timeline.play(); + event.consume(); + } + }; + + bar.addEventHandler(ScrollEvent.ANY, scrollEventHandler); + virtualFlow.setOnScroll(scrollEventHandler); + + timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> { + for (int i = 0; i < derivatives.length; ++i) + derivatives[i] *= frictions[i]; + for (int i = 1; i < derivatives.length; ++i) + derivatives[i] += derivatives[i - 1]; + double dy = derivatives[derivatives.length - 1]; + double height = listView.getLayoutBounds().getHeight(); + bar.setValue(Math.min(Math.max(bar.getValue() + dy / height, 0), 1)); + if (Math.abs(dy) < 0.001) + timeline.stop(); + listView.requestLayout(); + })); + timeline.setCycleCount(Animation.INDEFINITE); + }); + } + + public static final Image DEFAULT_ICON = new Image("/assets/img/icon.png"); + + public static final String[] STYLESHEETS = new String[]{ + FXUtils.class.getResource("/css/jfoenix-fonts.css").toExternalForm(), + FXUtils.class.getResource("/css/jfoenix-design.css").toExternalForm(), + FXUtils.class.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm() + }; + + public static final Interpolator SINE = new Interpolator() { + @Override + protected double curve(double t) { + return Math.sin(t * Math.PI / 2); + } + + @Override + public String toString() { + return "Interpolator.SINE"; + } + }; +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerController.java index df0f719bd..33fe78fff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerController.java @@ -43,7 +43,7 @@ public class InstallerController { private String optiFine; public void initialize() { - FXUtilsKt.smoothScrolling(scrollPane); + FXUtils.smoothScrolling(scrollPane); } public void loadVersion(Profile profile, String versionId) { @@ -59,8 +59,8 @@ public class InstallerController { LinkedList newList = new LinkedList<>(version.getLibraries()); newList.remove(library); new VersionJsonSaveTask(profile.getRepository(), version.setLibraries(newList)) - .with(Task.of(e -> profile.getRepository().refreshVersions())) - .with(Task.of(e -> loadVersion(this.profile, this.versionId))) + .with(Task.of(profile.getRepository()::refreshVersions)) + .with(Task.of(() -> loadVersion(this.profile, this.versionId))) .start(); }; @@ -85,6 +85,6 @@ public class InstallerController { // TODO: if minecraftVersion returns null. if (gameVersion == null) return; - Controllers.INSTANCE.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion, version, forge, liteLoader, optiFine)); + Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion, version, forge, liteLoader, optiFine)); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java index b616acae4..0c61891c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java @@ -38,7 +38,7 @@ public class InstallerItem extends BorderPane { public InstallerItem(String artifact, String version, Consumer deleteCallback) { this.deleteCallback = deleteCallback; - FXUtilsKt.loadFXML(this, "/assets/fxml/version/installer-item.fxml"); + FXUtils.loadFXML(this, "/assets/fxml/version/installer-item.fxml"); setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;"); JFXDepthManager.setDepth(this, 1); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LaunchingStepsPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LaunchingStepsPane.java index c0f008cdd..996a8511f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LaunchingStepsPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LaunchingStepsPane.java @@ -31,10 +31,10 @@ public class LaunchingStepsPane extends StackPane { private Label lblSteps; public LaunchingStepsPane() { - FXUtilsKt.loadFXML(this, "/assets/fxml/launching-steps.fxml"); + FXUtils.loadFXML(this, "/assets/fxml/launching-steps.fxml"); - FXUtilsKt.limitHeight(this, 200); - FXUtilsKt.limitWidth(this, 400); + FXUtils.limitHeight(this, 200); + FXUtils.limitWidth(this, 400); } public void setCurrentState(String currentState) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java new file mode 100644 index 000000000..a66ae0f69 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java @@ -0,0 +1,118 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Paint; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.ProfileChangedEvent; +import org.jackhuang.hmcl.event.ProfileLoadingEvent; +import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.ui.construct.IconedItem; +import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Pair; + +import java.util.LinkedList; +import java.util.Objects; + +public final class LeftPaneController { + private final AdvancedListBox leftPane; + private final VBox profilePane = new VBox(); + private final VersionListItem accountItem = new VersionListItem("No Account", "unknown"); + + public LeftPaneController(AdvancedListBox leftPane) { + this.leftPane = leftPane; + + leftPane.startCategory("ACCOUNTS") + .add(Lang.apply(new RipplerContainer(accountItem), rippler -> { + rippler.setOnMouseClicked(e -> Controllers.navigate(new AccountsPage())); + accountItem.setOnSettingsButtonClicked(() -> Controllers.navigate(new AccountsPage())); + })) + .startCategory("LAUNCHER") + .add(Lang.apply(new IconedItem(SVG.gear("black", 20, 20), Main.i18n("launcher.title.launcher")), iconedItem -> { + iconedItem.prefWidthProperty().bind(leftPane.widthProperty()); + iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage())); + })) + .startCategory(Main.i18n("ui.label.profile")) + .add(profilePane); + + EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading); + EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged); + + Controllers.getDecorator().getAddMenuButton().setOnMouseClicked(e -> + Controllers.getDecorator().showPage(new ProfilePage(null)) + ); + + FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), it -> { + if (it == null) { + accountItem.setVersionName("mojang@mojang.com"); + accountItem.setGameVersion("Yggdrasil"); + } else { + accountItem.setVersionName(it.getUsername()); + accountItem.setGameVersion(AccountsPage.accountType(it)); + } + + if (it instanceof YggdrasilAccount) + accountItem.setImage(AccountHelper.getSkin((YggdrasilAccount) it, 4), AccountHelper.getViewport(4)); + else + accountItem.setImage(FXUtils.DEFAULT_ICON, null); + }); + + if (Settings.INSTANCE.getAccounts().isEmpty()) + Controllers.navigate(new AccountsPage()); + } + + public void onProfileChanged(ProfileChangedEvent event) { + Profile profile = event.getProfile(); + + for (Node node : profilePane.getChildren()) { + if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof Pair) { + ((RipplerContainer) node).setSelected(Objects.equals(((Pair) node.getProperties().get("profile")).getKey(), profile.getName())); + } + } + } + + public void onProfilesLoading() { + LinkedList list = new LinkedList<>(); + for (Profile profile : Settings.INSTANCE.getProfiles()) { + VersionListItem item = new VersionListItem(profile.getName()); + RipplerContainer ripplerContainer = new RipplerContainer(item); + item.setOnSettingsButtonClicked(() -> Controllers.getDecorator().showPage(new ProfilePage(profile))); + ripplerContainer.setRipplerFill(Paint.valueOf("#89E1F9")); + ripplerContainer.setOnMouseClicked(e -> { + // clean selected property + for (Node node : profilePane.getChildren()) + if (node instanceof RipplerContainer) + ((RipplerContainer) node).setSelected(false); + ripplerContainer.setSelected(true); + Settings.INSTANCE.setSelectedProfile(profile); + }); + ripplerContainer.getProperties().put("profile", new Pair<>(profile.getName(), item)); + ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty()); + list.add(ripplerContainer); + } + Platform.runLater(() -> profilePane.getChildren().setAll(list)); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index 58ba20198..cf5b4bba4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -31,7 +31,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; -import org.jackhuang.hmcl.MainKt; +import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.event.Event; import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.game.LauncherHelper; @@ -61,8 +61,8 @@ public final class LogWindow extends Stage { public LogWindow() { setScene(new Scene(impl, 800, 480)); - getScene().getStylesheets().addAll(FXUtilsKt.getStylesheets()); - setTitle(MainKt.i18n("logwindow.title")); + getScene().getStylesheets().addAll(FXUtils.STYLESHEETS); + setTitle(Main.i18n("logwindow.title")); getIcons().add(new Image("/assets/img/icon.png")); } @@ -164,7 +164,7 @@ public final class LogWindow extends Stage { Document document; LogWindowImpl() { - FXUtilsKt.loadFXML(this, "/assets/fxml/log.fxml"); + FXUtils.loadFXML(this, "/assets/fxml/log.fxml"); engine = webView.getEngine(); engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html"))) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java new file mode 100644 index 000000000..185d326d2 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java @@ -0,0 +1,127 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXMasonryPane; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.ProfileChangedEvent; +import org.jackhuang.hmcl.event.ProfileLoadingEvent; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; +import org.jackhuang.hmcl.game.GameVersion; +import org.jackhuang.hmcl.game.LauncherHelper; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.ui.download.DownloadWizardProvider; +import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.util.Lang; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; + +public final class MainPage extends StackPane implements DecoratorPage { + + private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("launcher.title.main")); + + @FXML + private JFXButton btnRefresh; + + @FXML + private JFXButton btnAdd; + + @FXML + private JFXMasonryPane masonryPane; + + { + FXUtils.loadFXML(this, "/assets/fxml/main.fxml"); + + EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(() -> Platform.runLater(this::loadVersions)); + EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading); + EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged); + + btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), "Install New Game")); + btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersions()); + } + + private Node buildNode(Profile profile, String version, String game) { + VersionItem item = new VersionItem(); + item.setGameVersion(game); + item.setVersionName(version); + item.setOnLaunchButtonClicked(e -> { + if (Settings.INSTANCE.getSelectedAccount() == null) + Controllers.dialog(Main.i18n("login.no_Player007")); + else + LauncherHelper.INSTANCE.launch(version); + }); + item.setOnDeleteButtonClicked(e -> { + profile.getRepository().removeVersionFromDisk(version); + Platform.runLater(this::loadVersions); + }); + item.setOnSettingsButtonClicked(e -> { + Controllers.getDecorator().showPage(Controllers.getVersionPage()); + Controllers.getVersionPage().load(version, profile); + }); + File iconFile = profile.getRepository().getVersionIcon(version); + if (iconFile.exists()) + item.setImage(new Image("file:" + iconFile.getAbsolutePath())); + return item; + } + + public void onProfilesLoading() { + // TODO: Profiles + } + + public void onProfileChanged(ProfileChangedEvent event) { + Platform.runLater(() -> loadVersions(event.getProfile())); + } + + private void loadVersions() { + loadVersions(Settings.INSTANCE.getSelectedProfile()); + } + + private void loadVersions(Profile profile) { + List children = new LinkedList<>(); + for (Version version : profile.getRepository().getVersions()) { + children.add(buildNode(profile, version.getId(), Lang.nonNull(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version.getId())), "Unknown"))); + } + FXUtils.resetChildren(masonryPane, children); + } + + public String getTitle() { + return title.get(); + } + + @Override + public StringProperty titleProperty() { + return title; + } + + public void setTitle(String title) { + this.title.set(title); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MessageDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MessageDialogPane.java index cf211c6d0..45ddfdb2d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MessageDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MessageDialogPane.java @@ -36,7 +36,7 @@ public final class MessageDialogPane extends StackPane { this.text = text; this.dialog = dialog; - FXUtilsKt.loadFXML(this, "/assets/fxml/message-dialog.fxml"); + FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml"); content.setText(text); acceptButton.setOnMouseClicked(e -> dialog.close()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.java new file mode 100644 index 000000000..777a11e5e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.java @@ -0,0 +1,153 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXSpinner; +import com.jfoenix.controls.JFXTabPane; +import javafx.application.Platform; +import javafx.beans.value.WeakChangeListener; +import javafx.fxml.FXML; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.TransferMode; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.mod.ModInfo; +import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.FileUtils; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Logging; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import java.util.stream.Stream; + +public final class ModController { + @FXML + private ScrollPane scrollPane; + + @FXML private StackPane rootPane; + + @FXML private VBox modPane; + + @FXML private StackPane contentPane; + @FXML private JFXSpinner spinner; + + private JFXTabPane parentTab; + private ModManager modManager; + private String versionId; + + public void initialize() { + FXUtils.smoothScrolling(scrollPane); + + rootPane.setOnDragOver(event -> { + if (event.getGestureSource() != rootPane && event.getDragboard().hasFiles()) + event.acceptTransferModes(TransferMode.COPY_OR_MOVE); + event.consume(); + }); + + rootPane.setOnDragDropped(event -> { + List mods = event.getDragboard().getFiles(); + Stream stream = null; + if (mods != null) + stream = mods.stream() + .filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it))); + if (stream != null && stream.findAny().isPresent()) { + stream.forEach(it -> { + try { + modManager.addMod(versionId, it); + } catch (IOException | IllegalArgumentException e) { + Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e); + } + }); + loadMods(modManager, versionId); + event.setDropCompleted(true); + } + event.consume(); + }); + } + + public void loadMods(ModManager modManager, String versionId) { + this.modManager = modManager; + this.versionId = versionId; + Task.of(variables -> { + synchronized (ModController.this) { + Platform.runLater(() -> { + rootPane.getChildren().remove(contentPane); + spinner.setVisible(true); + }); + + modManager.refreshMods(versionId); + + // Surprisingly, if there are a great number of mods, this processing will cause a long UI pause, + // constructing UI elements. + // We must do this asynchronously. + LinkedList list = new LinkedList<>(); + for (ModInfo modInfo : modManager.getMods(versionId)) { + ModItem item = new ModItem(modInfo, i -> { + modManager.removeMods(versionId, modInfo); + loadMods(modManager, versionId); + }); + modInfo.activeProperty().addListener((a, b, newValue) -> { + if (newValue) + item.getStyleClass().remove("disabled"); + else + item.getStyleClass().add("disabled"); + }); + if (!modInfo.isActive()) + item.getStyleClass().add("disabled"); + + list.add(item); + } + + Platform.runLater(() -> { + rootPane.getChildren().add(contentPane); + spinner.setVisible(false); + }); + variables.set("list", list); + } + }).subscribe(Schedulers.javafx(), variables -> { + FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> { + if (newValue != null && newValue.getUserData() == ModController.this) + modPane.getChildren().setAll(variables.>get("list")); + }); + }); + + } + + public void onAdd() { + FileChooser chooser = new FileChooser(); + chooser.setTitle(Main.i18n("mods.choose_mod")); + chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter("Mod", "*.jar", "*.zip", "*.litemod")); + File res = chooser.showOpenDialog(Controllers.getStage()); + if (res == null) return; + Task.of(() -> modManager.addMod(versionId, res)) + .subscribe(Task.of(Schedulers.javafx(), () -> loadMods(modManager, versionId))); + } + + public void setParentTab(JFXTabPane parentTab) { + this.parentTab = parentTab; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java new file mode 100644 index 000000000..80a06c8a2 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java @@ -0,0 +1,121 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTextField; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.fxml.FXML; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.ui.construct.FileItem; +import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.util.StringUtils; + +import java.io.File; +import java.util.Optional; + +public final class ProfilePage extends StackPane implements DecoratorPage { + private final StringProperty title; + private final StringProperty location; + private final Profile profile; + + @FXML + private JFXTextField txtProfileName; + @FXML + private FileItem gameDir; + @FXML private JFXButton btnSave; + @FXML private JFXButton btnDelete; + + /** + * @param profile null if creating a new profile. + */ + public ProfilePage(Profile profile) { + this.profile = profile; + + title = new SimpleStringProperty(this, "title", + profile == null ? Main.i18n("ui.newProfileWindow.title") : Main.i18n("ui.label.profile") + " - " + profile.getName()); + location = new SimpleStringProperty(this, "location", + Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse("")); + + FXUtils.loadFXML(this, "/assets/fxml/profile.fxml"); + + txtProfileName.setText(Optional.ofNullable(profile).map(Profile::getName).orElse("")); + FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> { + btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation())); + }); + gameDir.setProperty(location); + FXUtils.onChangeAndOperate(location, it -> { + btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation())); + }); + + if (profile == null) + btnDelete.setVisible(false); + } + + public void onDelete() { + if (profile != null) { + Settings.INSTANCE.deleteProfile(profile); + Controllers.navigate(null); + } + } + + public void onSave() { + if (profile != null) { + profile.setName(txtProfileName.getText()); + if (StringUtils.isNotBlank(getLocation())) + profile.setGameDir(new File(getLocation())); + } else { + if (StringUtils.isBlank(getLocation())) { + gameDir.onExplore(); + } + Settings.INSTANCE.putProfile(new Profile(txtProfileName.getText(), new File(getLocation()))); + } + + Settings.INSTANCE.onProfileLoading(); + Controllers.navigate(null); + } + + public String getTitle() { + return title.get(); + } + + @Override + public StringProperty titleProperty() { + return title; + } + + public void setTitle(String title) { + this.title.set(title); + } + + public String getLocation() { + return location.get(); + } + + public StringProperty locationProperty() { + return location; + } + + public void setLocation(String location) { + this.location.set(location); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java new file mode 100644 index 000000000..22e5a7d7b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -0,0 +1,96 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.shape.SVGPath; + +public final class SVG { + private SVG() { + } + + private static Node createSVGPath(String d, String fill, double width, double height) { + SVGPath path = new SVGPath(); + path.getStyleClass().add("svg"); + path.setContent(d); + path.setStyle("-fx-fill: " + fill + ";"); + + Group svg = new Group(path); + double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight()); + svg.setScaleX(scale); + svg.setScaleY(scale); + + return svg; + } + + // default fill: white, width: 20, height 20 + + public static Node gear(String fill, double width, double height) { + return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height); + } + + public static Node back(String fill, double width, double height) { + return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height); + } + + public static Node close(String fill, double width, double height) { + return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height); + } + + public static Node dotsVertical(String fill, double width, double height) { + return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height); + } + + public static Node delete(String fill, double width, double height) { + return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height); + } + + public static Node accountEdit(String fill, double width, double height) { + return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height); + } + + public static Node expand(String fill, double width, double height) { + return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height); + } + + public static Node collapse(String fill, double width, double height) { + return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height); + } + + public static Node navigate(String fill, double width, double height) { + return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height); + } + + public static Node launch(String fill, double width, double height) { + return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height); + } + + public static Node pencil(String fill, double width, double height) { + return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height); + } + + public static Node refresh(String fill, double width, double height) { + return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height); + } + + public static Node folderOpen(String fill, double width, double height) { + return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height); + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SafeIntStringConverter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SafeIntStringConverter.java index 9124e9fd5..c1c30fe7f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SafeIntStringConverter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SafeIntStringConverter.java @@ -26,6 +26,11 @@ import java.util.Optional; * @author huangyuhui */ public final class SafeIntStringConverter extends StringConverter { + public static final SafeIntStringConverter INSTANCE = new SafeIntStringConverter(); + + private SafeIntStringConverter() { + } + @Override public Integer fromString(String string) { return Optional.ofNullable(string).map(Lang::toIntOrNull).orElse(null); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java new file mode 100644 index 000000000..c0099c0e5 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java @@ -0,0 +1,135 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program 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. + * + * This program 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 this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.controls.JFXComboBox; +import com.jfoenix.controls.JFXTextField; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Font; +import org.jackhuang.hmcl.Main; +import org.jackhuang.hmcl.setting.DownloadProviders; +import org.jackhuang.hmcl.setting.Locales; +import org.jackhuang.hmcl.setting.Proxies; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.ui.construct.FileItem; +import org.jackhuang.hmcl.ui.construct.FontComboBox; +import org.jackhuang.hmcl.ui.construct.Validator; +import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.util.Lang; + +public final class SettingsPage extends StackPane implements DecoratorPage { + private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("launcher.title.launcher")); + + @FXML + private JFXTextField txtProxyHost; + @FXML + private JFXTextField txtProxyPort; + @FXML + private JFXTextField txtProxyUsername; + @FXML + private JFXTextField txtProxyPassword; + @FXML + private JFXTextField txtFontSize; + @FXML + private JFXComboBox cboProxyType; + @FXML + private JFXComboBox