diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index 3f8969aa4..3927f2be9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui and contributors * * 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 @@ -20,14 +20,13 @@ package org.jackhuang.hmcl.game; import javafx.beans.binding.Bindings; import javafx.beans.binding.ObjectBinding; import javafx.scene.image.Image; -import javafx.scene.image.PixelWriter; -import javafx.scene.image.WritableImage; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; import org.jackhuang.hmcl.auth.yggdrasil.*; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.ResourceNotFoundError; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.javafx.BindingMapping; @@ -231,34 +230,18 @@ public final class TexturesLoader { public static ObjectBinding fxAvatarBinding(YggdrasilService service, UUID uuid, int size) { return BindingMapping.of(skinBinding(service, uuid)) .map(it -> toAvatar(it.image, size)) - .map(TexturesLoader::toFXImage); + .map(FXUtils::toFXImage); } public static ObjectBinding fxAvatarBinding(Account account, int size) { if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) { return BindingMapping.of(skinBinding(account)) .map(it -> toAvatar(it.image, size)) - .map(TexturesLoader::toFXImage); + .map(FXUtils::toFXImage); } else { return Bindings.createObjectBinding( - () -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size))); + () -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size))); } } // ==== - - // Based on https://stackoverflow.com/a/57552025 - // Fix #874: Use it instead of SwingFXUtils.toFXImage - private static WritableImage toFXImage(BufferedImage image) { - WritableImage wr = new WritableImage(image.getWidth(), image.getHeight()); - PixelWriter pw = wr.getPixelWriter(); - - final int iw = image.getWidth(); - final int ih = image.getHeight(); - for (int x = 0; x < iw; x++) { - for (int y = 0; y < ih; y++) { - pw.setArgb(x, y, image.getRGB(x, y)); - } - } - return wr; - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 691cd52dc..8ddc24616 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -38,6 +38,8 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.TransferMode; @@ -55,6 +57,7 @@ import org.jackhuang.hmcl.util.javafx.ExtendedProperties; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import java.awt.image.BufferedImage; import java.io.File; import java.io.FileFilter; import java.io.IOException; @@ -552,4 +555,20 @@ public final class FXUtils { } }); } + + // Based on https://stackoverflow.com/a/57552025 + // Fix #874: Use it instead of SwingFXUtils.toFXImage + public static WritableImage toFXImage(BufferedImage image) { + WritableImage wr = new WritableImage(image.getWidth(), image.getHeight()); + PixelWriter pw = wr.getPixelWriter(); + + final int iw = image.getWidth(); + final int ih = image.getHeight(); + for (int x = 0; x < iw; x++) { + for (int y = 0; y < ih; y++) { + pw.setArgb(x, y, image.getRGB(x, y)); + } + } + return wr; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java index b295f6d94..0e0252e18 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui and contributors * * 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 @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.account; import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.control.Tooltip; @@ -27,14 +28,19 @@ import javafx.scene.image.ImageView; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; +import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.javafx.BindingMapping; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static javafx.beans.binding.Bindings.createStringBinding; +import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory; +import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName; +import static org.jackhuang.hmcl.ui.FXUtils.toFXImage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class AccountAdvancedListItem extends AdvancedListItem { @@ -48,16 +54,18 @@ public class AccountAdvancedListItem extends AdvancedListItem { Account account = get(); if (account == null) { titleProperty().unbind(); + subtitleProperty().unbind(); + imageView.imageProperty().unbind(); + tooltip.textProperty().unbind(); setTitle(i18n("account.missing")); setSubtitle(i18n("account.missing.add")); - imageView.imageProperty().unbind(); - imageView.setImage(newImage("/assets/img/steve.png")); - tooltip.setText(""); + imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32))); + tooltip.setText(i18n("account.create")); } else { - titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account)); - setSubtitle(accountSubtitle(account)); + titleProperty().bind(BindingMapping.of(account, Account::getCharacter)); + subtitleProperty().bind(accountSubtitle(account)); imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32)); - tooltip.setText(account.getCharacter() + " " + accountTooltip(account)); + tooltip.textProperty().bind(accountTooltip(account)); } } }; @@ -88,23 +96,27 @@ public class AccountAdvancedListItem extends AdvancedListItem { return account; } - private static String accountSubtitle(Account account) { - String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)); + private static ObservableValue accountSubtitle(Account account) { if (account instanceof AuthlibInjectorAccount) { - return ((AuthlibInjectorAccount) account).getServer().getName(); + return BindingMapping.of(((AuthlibInjectorAccount) account).getServer(), AuthlibInjectorServer::getName); } else { - return loginTypeName; + return createStringBinding(() -> getLocalizedLoginTypeName(getAccountFactory(account))); } } - private static String accountTooltip(Account account) { + private static ObservableValue accountTooltip(Account account) { if (account instanceof AuthlibInjectorAccount) { AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer(); - return account.getUsername() + ", " + i18n("account.injector.server") + ": " + server.getName(); + return Bindings.format("%s (%s) (%s)", + BindingMapping.of(account, Account::getCharacter), + account.getUsername(), + BindingMapping.of(server, AuthlibInjectorServer::getName)); } else if (account instanceof YggdrasilAccount) { - return account.getUsername(); + return Bindings.format("%s (%s)", + BindingMapping.of(account, Account::getCharacter), + account.getUsername()); } else { - return ""; + return BindingMapping.of(account, Account::getCharacter); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index 2b463cb4b..d241563e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -57,6 +57,7 @@ import static java.util.logging.Level.WARNING; import static java.util.stream.Collectors.toList; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.io.FileUtils.getExtension; @@ -86,33 +87,39 @@ public class DecoratorController { setupAuthlibInjectorDnD(); - // pass key events to current dialog + // pass key events to current dialog / current page decorator.addEventFilter(KeyEvent.ANY, e -> { - if (dialogPane == null || !dialogPane.peek().isPresent()) { - return; - } - Node currentDialog = dialogPane.peek().get(); - if (!(e.getTarget() instanceof Node)) { - return; + return; // event source can't be determined } - boolean targetInDialog = false; + Node newTarget; + if (dialogPane != null && dialogPane.peek().isPresent()) { + newTarget = dialogPane.peek().get(); // current dialog + } else { + newTarget = navigator.getCurrentPage(); // current page + } + + boolean needsRedirect = true; Node t = (Node) e.getTarget(); while (t != null) { - if (t == currentDialog) { - targetInDialog = true; + if (t == newTarget) { + // current event target is in newTarget + needsRedirect = false; break; } t = t.getParent(); } - if (targetInDialog) { + if (!needsRedirect) { return; } e.consume(); - currentDialog.fireEvent(e.copyFor(e.getSource(), currentDialog)); + newTarget.fireEvent(e.copyFor(e.getSource(), newTarget)); }); + + // press ESC to go back + onEscPressed(navigator, this::back); } public Decorator getDecorator() { @@ -229,10 +236,15 @@ public class DecoratorController { if (navigator.getCurrentPage() instanceof DecoratorPage) { DecoratorPage page = (DecoratorPage) navigator.getCurrentPage(); - if (page.back()) - navigator.close(); + if (page.back()) { + if (navigator.canGoBack()) { + navigator.close(); + } + } } else { - navigator.close(); + if (navigator.canGoBack()) { + navigator.close(); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 245fce628..1670695e1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui and contributors * * 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 @@ -66,7 +66,8 @@ public class RootPage extends DecoratorTabPage { public RootPage() { setLeftPaneWidth(200); - EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource())); + EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class) + .register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource())); Profile profile = Profiles.getSelectedProfile(); if (profile != null && profile.getRepository().isLoaded()) @@ -78,7 +79,8 @@ public class RootPage extends DecoratorTabPage { @Override public boolean back() { - if (mainTab.isSelected()) return true; + if (mainTab.isSelected()) + return true; else { getSelectionModel().select(mainTab); return false; @@ -102,20 +104,23 @@ public class RootPage extends DecoratorTabPage { MainPage mainPage = new MainPage(); FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { File modpack = modpacks.get(0); - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); + Controllers.getDecorator().startWizard( + new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), + i18n("install.modpack")); }); FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), mainPage::setCurrentGame); mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty()); - mainPage.latestVersionProperty().bind( - BindingMapping.of(UpdateChecker.latestVersionProperty()) - .map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion()))); + mainPage.latestVersionProperty().bind(BindingMapping.of(UpdateChecker.latestVersionProperty()) + .map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion()))); Profiles.registerVersionsListener(profile -> { HMCLGameRepository repository = profile.getRepository(); List children = repository.getVersions().parallelStream() .filter(version -> !version.isHidden()) - .sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime()) + .sorted(Comparator + .comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) + : version.getReleaseTime()) .thenComparing(a -> VersionNumber.asVersion(a.getId()))) .collect(Collectors.toList()); runInFX(() -> { @@ -135,7 +140,13 @@ public class RootPage extends DecoratorTabPage { // first item in left sidebar AccountAdvancedListItem accountListItem = new AccountAdvancedListItem(); - accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage())); + accountListItem.setOnAction(e -> { + Controllers.navigate(Controllers.getAccountListPage()); + + if (Accounts.getAccounts().isEmpty()) { + Controllers.dialog(new AddAccountPane()); + } + }); accountListItem.accountProperty().bind(Accounts.selectedAccountProperty()); // second item in left sidebar @@ -158,20 +169,16 @@ public class RootPage extends DecoratorTabPage { // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); - launcherSettingsItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey()); + launcherSettingsItem + .setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey()); launcherSettingsItem.setActionButtonVisible(false); launcherSettingsItem.setTitle(i18n("settings.launcher")); launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage())); // the left sidebar - AdvancedListBox sideBar = new AdvancedListBox() - .startCategory(i18n("account").toUpperCase()) - .add(accountListItem) - .startCategory(i18n("version").toUpperCase()) - .add(gameListItem) - .add(gameItem) - .startCategory(i18n("launcher").toUpperCase()) - .add(launcherSettingsItem); + AdvancedListBox sideBar = new AdvancedListBox().startCategory(i18n("account").toUpperCase()) + .add(accountListItem).startCategory(i18n("version").toUpperCase()).add(gameListItem).add(gameItem) + .startCategory(i18n("launcher").toUpperCase()).add(launcherSettingsItem); // the root page, with the sidebar in left, navigator in center. BorderPane root = new BorderPane(); @@ -195,7 +202,8 @@ public class RootPage extends DecoratorTabPage { private boolean checkedAccont = false; public void checkAccount() { - if (checkedAccont) return; + if (checkedAccont) + return; checkedAccont = true; if (Accounts.getAccounts().isEmpty()) Platform.runLater(this::addNewAccount); @@ -217,8 +225,11 @@ public class RootPage extends DecoratorTabPage { File modpackFile = new File("modpack.zip").getAbsoluteFile(); if (modpackFile.exists()) { Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath())) - .thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding)) - .thenApplyAsync(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack) + .thenApplyAsync( + encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding)) + .thenApplyAsync(modpack -> ModpackHelper + .getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), + modpack) .withRunAsync(Schedulers.javafx(), this::checkAccount).executor()) .thenAcceptAsync(Schedulers.javafx(), executor -> { Controllers.taskDialog(executor, i18n("modpack.installing")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java index 2f9784e6b..b1ab83e3a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java @@ -88,7 +88,7 @@ public final class SelfDependencyPatcher { static class DependencyDescriptor { - private static final String REPOSITORY_URL = "https://maven.aliyun.com/repository/central/"; + private static final String REPOSITORY_URL = System.getProperty("hmcl.openjfx.repo", "https://maven.aliyun.com/repository/central/"); private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies"); private static String currentArchClassifier() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java index 179a35510..ab39645d6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java @@ -55,7 +55,7 @@ public class LaunchOptions implements Serializable { private String preLaunchCommand; private NativesDirectoryType nativesDirType; private String nativesDir; - private ProcessPriority processPriority; + private ProcessPriority processPriority = ProcessPriority.NORMAL; /** * The game directory @@ -497,7 +497,7 @@ public class LaunchOptions implements Serializable { return this; } - public Builder setProcessPriority(ProcessPriority processPriority) { + public Builder setProcessPriority(@NotNull ProcessPriority processPriority) { options.processPriority = processPriority; return this; } diff --git a/README.md b/README.md index d93f25f23..cad2e02df 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,4 @@ Make sure you have Java installed with JavaFX 8 at least. Liberica full JDK 8~16 |`-Dhmcl.version.override=`|Override the version number.| |`-Dhmcl.update_source.override=`|Override the update source.| |`-Dhmcl.authlibinjector.location=`|Use specified authlib-injector (instead of downloading one).| +|`-Dhmcl.openjfx.repo=`|Download OpenJFX from specified Maven repository. Default value is `https://maven.aliyun.com/repository/central/`.|