Compare commits

...
36 Commits
Author SHA1 Message Date
huanghongxun 3d008dcab5 Make DatapackListPage multiple-selectable 2019-03-19 10:31:02 +08:00
huanghongxun 5953cc6411 Add filter to hide world that version mismatch 2019-03-19 00:04:52 +08:00
huanghongxun 2dce675fe2 Ignore all color formatting in world name. 2019-03-18 23:24:50 +08:00
huanghongxun aa013a86dd Add upgrade button on Forge, LiteLoader, OptiFine 2019-03-18 23:17:53 +08:00
huanghongxun f866f692e2 Hide ForgeNew/OldInstallTask 2019-03-18 20:17:03 +08:00
huanghongxun 68a2514cef Disable mod pane when Forge/Liteloader is not installed. 2019-03-18 20:14:31 +08:00
huanghongxun 32562d7435 Revert JFoenix 2019-03-18 13:44:38 +08:00
huanghongxun 78be1aaa7c Fix blank space in bottom of settings page 2019-03-18 13:43:17 +08:00
huanghongxun 1eb4b1ca9e Fix unable to delete the version which failed to download 2019-03-05 19:37:37 +08:00
huanghongxun bf37db2cbe Fix #538 2019-03-05 19:32:24 +08:00
huanghongxun 5b30617a6b Forbid user from installing both Forge 1.13 and OptiFine simultaneously 2019-03-04 12:27:09 +08:00
huanghongxun 266c686a3a Fix forge 1.13 installation 2019-03-04 11:39:30 +08:00
huanghongxun dc1df98277 Fix compilation error 2019-02-24 23:13:31 +08:00
huanghongxun def45cc3e4 Fix NumberFormatException when parsing forge version list json 2019-02-24 23:11:09 +08:00
huanghongxun 8007f1958e Rename isDependentsSucceeded to isDependentSucceeded 2019-02-24 17:00:17 +08:00
huanghongxun b22e724a3f Remove SilentException 2019-02-24 12:39:40 +08:00
huanghongxun ed66b016db Rename then-series functions 2019-02-24 12:39:39 +08:00
yushijinhun 34890f21b3 Update issue template 2019-02-24 12:29:58 +08:00
yushijinhun 9f85e4a63b Do not run GameVersion.minecraftVersion on UI thread 2019-02-24 12:25:19 +08:00
yushijinhun 7fcd4d94a6 Merge branch 'javafx' of github.com:huanghongxun/HMCL into javafx 2019-02-23 23:52:26 +08:00
yushijinhun 84fbca7905 Remove unused code 2019-02-23 23:48:23 +08:00
huanghongxun 9add2387b9 Add comments to FileUtils and IOUtils 2019-02-23 23:42:05 +08:00
huanghongxun a388450f94 Remove AutoTypingMap 2019-02-23 22:50:01 +08:00
huanghongxun fff7abc2ac finalized no longer passes AutoTypingMap as callback parameter 2019-02-23 22:50:00 +08:00
yushijinhun e5587418c7 Remove SafeIntStringConverter 2019-02-23 22:36:38 +08:00
yushijinhun 7c89fac1e9 Rename MultiStepBinding->BindingMapping 2019-02-23 22:14:29 +08:00
yushijinhun bc9a9ffb72 rewrite MultiStepBinding.asyncMap 2019-02-23 21:58:44 +08:00
yushijinhun 46d27a04a2 Fix auth server list in add account pane 2019-02-23 21:49:38 +08:00
huanghongxun 0f33981c96 Fix #534 2019-02-23 19:13:24 +08:00
yushijinhun b396e0b208 Fix compilation error 2019-02-23 18:41:39 +08:00
yushijinhun 1bf2f17538 Fix #533 2019-02-23 18:38:43 +08:00
huanghongxun 608a531655 Remove AtomicReference 2019-02-20 20:18:34 +08:00
huanghongxun e57180eb15 remove lastException in task.variables 2019-02-20 20:07:43 +08:00
huanghongxun 3ae826d9ca Fix task.getLastException does not receive subtasks' value 2019-02-20 20:07:11 +08:00
huanghongxun e940845c60 Fix not correct mod installation 2019-02-20 16:40:26 +08:00
huanghongxun d8aa989cc4 Fix #532 2019-02-19 23:15:45 +08:00
115 changed files with 1803 additions and 1778 deletions

No files matched your search

+3 -3
View File
@@ -6,7 +6,7 @@
我们发现,对于不经常检查邮箱的人来说,GitHub issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
@@ -24,5 +24,5 @@
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(在启动器设置中打开日志文件夹并将文件夹内所有文件打包发在这里):
* 问题描述(如何触发问题):
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
* 问题描述(如何触发问题):
+1 -1
View File
@@ -112,7 +112,7 @@ def createExecutable(String suffix, String header) {
processResources {
ext.convertToBSS = { String resource ->
exclude resource
// exclude resource
doFirst {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
@@ -25,7 +24,9 @@ import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -39,6 +40,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -97,7 +99,7 @@ public final class Launcher extends Application {
public static void stopApplication() {
LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
@@ -111,7 +113,7 @@ public final class Launcher extends Application {
public static void stopWithoutPlatform() {
LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
@@ -20,9 +20,6 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -38,7 +38,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
@@ -122,14 +122,14 @@ public final class LauncherHelper {
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(variables -> {
.then(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version);
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(var -> {
.then(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
@@ -141,44 +141,42 @@ public final class LauncherHelper {
}
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(i18n("account.methods"), variables -> {
.thenCompose(() -> Task.ofResult(i18n("account.methods"), () -> {
try {
variables.set("account", account.logIn());
return account.logIn();
} catch (CredentialExpiredException e) {
LOG.info("Credential has expired: " + e);
variables.set("account", DialogController.logIn(account));
return DialogController.logIn(account);
} catch (AuthenticationException e) {
LOG.warning("Authentication failed, try playing offline: " + e);
variables.set("account",
account.playOffline().orElseThrow(() -> e));
return account.playOffline().orElseThrow(() -> e);
}
}))
.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()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
}))
.then(variables -> {
DefaultLauncher launcher = variables.get("launcher");
.thenApply(Schedulers.javafx(), authInfo -> {
emitStatus(LoadingState.LAUNCHING);
return authInfo;
})
.thenApply(authInfo -> new HMCLGameLauncher(
repository,
selectedVersion,
authInfo,
setting.toLaunchOptions(profile.getGameDir()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, setting, gameVersion.isPresent())
))
.thenCompose(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
} else {
return new LaunchTask<>(() -> {
return new LaunchTask<ManagedProcess>(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(i18n("version.launch_script"));
}
})
.then(Task.of(variables -> {
.thenAccept(process -> { // process is LaunchTask's result
if (scriptFile == null) {
ManagedProcess process = variables.get(LaunchTask.LAUNCH_ID);
PROCESSES.add(process);
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
@@ -187,13 +185,13 @@ public final class LauncherHelper {
process.stop();
it.fireEvent(new DialogCloseEvent());
});
} else
} else {
Platform.runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
}
})
.executor();
launchingStepsPane.setExecutor(executor, false);
@@ -236,7 +234,7 @@ public final class LauncherHelper {
}
Controllers.dialog(message,
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
MessageType.ERROR);
}
}
});
@@ -263,7 +261,7 @@ public final class LauncherHelper {
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
@@ -282,10 +280,10 @@ public final class LauncherHelper {
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, onAccept);
}
flag = true;
}
@@ -301,10 +299,10 @@ public final class LauncherHelper {
if (java8.isPresent()) {
java8required = true;
setting.setJavaVersion(java8.get());
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageBox.INFORMATION_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageType.INFORMATION, onAccept);
flag = true;
} else {
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, null);
flag = true;
}
}
@@ -319,7 +317,7 @@ public final class LauncherHelper {
newJavaRequired = true;
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, onAccept);
flag = true;
}
}
@@ -351,7 +349,7 @@ public final class LauncherHelper {
if (java64.isPresent()) {
setting.setJavaVersion(java64.get());
} else {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
}
@@ -361,13 +359,13 @@ public final class LauncherHelper {
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
// Cannot allocate too much memory exceeding free space.
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
@@ -379,7 +377,7 @@ public final class LauncherHelper {
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
}
@@ -429,13 +427,6 @@ public final class LauncherHelper {
public void execute() throws Exception {
setResult(supplier.get());
}
@Override
public String getId() {
return LAUNCH_ID;
}
static final String LAUNCH_ID = "launch";
}
/**
@@ -110,13 +110,13 @@ public final class ModpackHelper {
if (modpack.getManifest() instanceof CurseManifest)
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
.finalized(Schedulers.defaultScheduler(), ExceptionalConsumer.fromRunnable(success), failure);
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof HMCLModpackManifest)
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.finalized(Schedulers.defaultScheduler(), ExceptionalConsumer.fromRunnable(success), failure);
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.finalized(Schedulers.defaultScheduler(), ExceptionalConsumer.fromRunnable(success), failure)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
}
@@ -17,7 +17,9 @@
*/
package org.jackhuang.hmcl.game;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
@@ -33,6 +35,7 @@ import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@@ -48,7 +51,8 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
@@ -69,8 +73,8 @@ public final class TexturesLoader {
private final Map<String, String> metadata;
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
this.image = image;
this.metadata = metadata;
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public BufferedImage getImage() {
@@ -98,6 +102,10 @@ public final class TexturesLoader {
}
public static LoadedTexture loadTexture(Texture texture) throws IOException {
if (StringUtils.isBlank(texture.getUrl())) {
throw new IOException("Texture url is empty");
}
Path file = getTexturePath(texture);
if (!Files.isRegularFile(file)) {
// download it
@@ -118,7 +126,11 @@ public final class TexturesLoader {
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
}
return new LoadedTexture(img, texture.getMetadata());
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
}
return new LoadedTexture(img, metadata);
}
// ====
@@ -142,7 +154,7 @@ public final class TexturesLoader {
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
return MultiStepBinding.of(service.getProfileRepository().binding(uuid))
return BindingMapping.of(service.getProfileRepository().binding(uuid))
.map(profile -> profile
.flatMap(it -> {
try {
@@ -152,20 +164,23 @@ public final class TexturesLoader {
return Optional.empty();
}
})
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN))))
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return getDefaultSkin(TextureModel.detectModelName(texture.getMetadata()));
}
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return uuidFallback;
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback, POOL);
}, uuidFallback);
}
// ====
@@ -189,7 +204,7 @@ public final class TexturesLoader {
}
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return MultiStepBinding.of(skinBinding(service, uuid))
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(img -> SwingFXUtils.toFXImage(img, null));
}
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -32,9 +31,7 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateStringProperty;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
@@ -42,6 +39,7 @@ import java.lang.reflect.Type;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
/**
*
@@ -90,9 +88,9 @@ public final class Profile implements Observable {
return global.get();
}
private final ImmediateStringProperty name;
private final SimpleStringProperty name;
public ImmediateStringProperty nameProperty() {
public StringProperty nameProperty() {
return name;
}
@@ -131,8 +129,8 @@ public final class Profile implements Observable {
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
this.name = new ImmediateStringProperty(this, "name", name);
gameDir = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
this.name = new SimpleStringProperty(this, "name", name);
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
this.global.set(global == null ? new VersionSetting() : global);
this.selectedVersion.set(selectedVersion);
@@ -146,7 +144,7 @@ public final class Profile implements Observable {
}
private void checkSelectedVersion() {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (!repository.isLoaded()) return;
String newValue = selectedVersion.get();
if (!repository.hasVersion(newValue)) {
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.setting;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.*;
@@ -36,6 +35,7 @@ import java.util.stream.Collectors;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Profiles {
@@ -162,7 +162,7 @@ public final class Profiles {
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
Profile profile = selectedProfile.get();
if (profile != null && profile.getRepository() == event.getSource()) {
selectedVersion.bind(profile.selectedVersionProperty());
@@ -27,6 +27,7 @@ import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.io.IOException;
@@ -132,7 +133,8 @@ public class Theme {
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> config().getTheme().getForegroundColor(), config().themeProperty());
return BindingMapping.of(config().themeProperty())
.map(Theme::getForegroundColor);
}
public static ObjectBinding<Color> blackFillBinding() {
@@ -21,13 +21,18 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ImmediateBooleanProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateIntegerProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateStringProperty;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -60,9 +65,9 @@ public final class VersionSetting {
this.global = global;
}
private final ImmediateBooleanProperty usesGlobalProperty = new ImmediateBooleanProperty(this, "usesGlobal", false);
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
public ImmediateBooleanProperty usesGlobalProperty() {
public BooleanProperty usesGlobalProperty() {
return usesGlobalProperty;
}
@@ -84,9 +89,9 @@ public final class VersionSetting {
// java
private final ImmediateStringProperty javaProperty = new ImmediateStringProperty(this, "java", "");
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
public ImmediateStringProperty javaProperty() {
public StringProperty javaProperty() {
return javaProperty;
}
@@ -110,7 +115,7 @@ public final class VersionSetting {
setDefaultJavaPath(null);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
/**
* Path to Java executable, or null if user customizes java directory.
@@ -124,9 +129,9 @@ public final class VersionSetting {
defaultJavaPathProperty.set(defaultJavaPath);
}
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public ImmediateStringProperty javaDirProperty() {
public StringProperty javaDirProperty() {
return javaDirProperty;
}
@@ -141,9 +146,9 @@ public final class VersionSetting {
javaDirProperty.set(javaDir);
}
private final ImmediateStringProperty wrapperProperty = new ImmediateStringProperty(this, "wrapper", "");
private final StringProperty wrapperProperty = new SimpleStringProperty(this, "wrapper", "");
public ImmediateStringProperty wrapperProperty() {
public StringProperty wrapperProperty() {
return wrapperProperty;
}
@@ -158,9 +163,9 @@ public final class VersionSetting {
wrapperProperty.set(wrapper);
}
private final ImmediateStringProperty permSizeProperty = new ImmediateStringProperty(this, "permSize", "");
private final StringProperty permSizeProperty = new SimpleStringProperty(this, "permSize", "");
public ImmediateStringProperty permSizeProperty() {
public StringProperty permSizeProperty() {
return permSizeProperty;
}
@@ -175,9 +180,9 @@ public final class VersionSetting {
permSizeProperty.set(permSize);
}
private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
public ImmediateIntegerProperty maxMemoryProperty() {
public IntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
}
@@ -195,9 +200,9 @@ public final class VersionSetting {
/**
* The minimum memory that JVM can allocate for heap.
*/
private final ImmediateObjectProperty<Integer> minMemoryProperty = new ImmediateObjectProperty<>(this, "minMemory", null);
private final ObjectProperty<Integer> minMemoryProperty = new SimpleObjectProperty<>(this, "minMemory", null);
public ImmediateObjectProperty<Integer> minMemoryProperty() {
public ObjectProperty<Integer> minMemoryProperty() {
return minMemoryProperty;
}
@@ -209,9 +214,9 @@ public final class VersionSetting {
minMemoryProperty.set(minMemory);
}
private final ImmediateStringProperty preLaunchCommandProperty = new ImmediateStringProperty(this, "precalledCommand", "");
private final StringProperty preLaunchCommandProperty = new SimpleStringProperty(this, "precalledCommand", "");
public ImmediateStringProperty preLaunchCommandProperty() {
public StringProperty preLaunchCommandProperty() {
return preLaunchCommandProperty;
}
@@ -229,9 +234,9 @@ public final class VersionSetting {
// options
private final ImmediateStringProperty javaArgsProperty = new ImmediateStringProperty(this, "javaArgs", "");
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
public ImmediateStringProperty javaArgsProperty() {
public StringProperty javaArgsProperty() {
return javaArgsProperty;
}
@@ -246,9 +251,9 @@ public final class VersionSetting {
javaArgsProperty.set(javaArgs);
}
private final ImmediateStringProperty minecraftArgsProperty = new ImmediateStringProperty(this, "minecraftArgs", "");
private final StringProperty minecraftArgsProperty = new SimpleStringProperty(this, "minecraftArgs", "");
public ImmediateStringProperty minecraftArgsProperty() {
public StringProperty minecraftArgsProperty() {
return minecraftArgsProperty;
}
@@ -263,9 +268,9 @@ public final class VersionSetting {
minecraftArgsProperty.set(minecraftArgs);
}
private final ImmediateBooleanProperty noJVMArgsProperty = new ImmediateBooleanProperty(this, "noJVMArgs", false);
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
public ImmediateBooleanProperty noJVMArgsProperty() {
public BooleanProperty noJVMArgsProperty() {
return noJVMArgsProperty;
}
@@ -280,9 +285,9 @@ public final class VersionSetting {
noJVMArgsProperty.set(noJVMArgs);
}
private final ImmediateBooleanProperty notCheckJVMProperty = new ImmediateBooleanProperty(this, "notCheckJVM", false);
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
public ImmediateBooleanProperty notCheckJVMProperty() {
public BooleanProperty notCheckJVMProperty() {
return notCheckJVMProperty;
}
@@ -297,9 +302,9 @@ public final class VersionSetting {
notCheckJVMProperty.set(notCheckJVM);
}
private final ImmediateBooleanProperty notCheckGameProperty = new ImmediateBooleanProperty(this, "notCheckGame", false);
private final BooleanProperty notCheckGameProperty = new SimpleBooleanProperty(this, "notCheckGame", false);
public ImmediateBooleanProperty notCheckGameProperty() {
public BooleanProperty notCheckGameProperty() {
return notCheckGameProperty;
}
@@ -314,9 +319,9 @@ public final class VersionSetting {
notCheckGameProperty.set(notCheckGame);
}
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
private final BooleanProperty showLogsProperty = new SimpleBooleanProperty(this, "showLogs", false);
public ImmediateBooleanProperty showLogsProperty() {
public BooleanProperty showLogsProperty() {
return showLogsProperty;
}
@@ -333,14 +338,14 @@ public final class VersionSetting {
// Minecraft settings.
private final ImmediateStringProperty serverIpProperty = new ImmediateStringProperty(this, "serverIp", "");
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
public ImmediateStringProperty serverIpProperty() {
public StringProperty serverIpProperty() {
return serverIpProperty;
}
/**
* The server ip that will be entered after Minecraft successfully loaded immediately.
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* Format: ip:port or without port.
*/
@@ -353,9 +358,9 @@ public final class VersionSetting {
}
private final ImmediateBooleanProperty fullscreenProperty = new ImmediateBooleanProperty(this, "fullscreen", false);
private final BooleanProperty fullscreenProperty = new SimpleBooleanProperty(this, "fullscreen", false);
public ImmediateBooleanProperty fullscreenProperty() {
public BooleanProperty fullscreenProperty() {
return fullscreenProperty;
}
@@ -370,9 +375,9 @@ public final class VersionSetting {
fullscreenProperty.set(fullscreen);
}
private final ImmediateIntegerProperty widthProperty = new ImmediateIntegerProperty(this, "width", 854);
private final IntegerProperty widthProperty = new SimpleIntegerProperty(this, "width", 854);
public ImmediateIntegerProperty widthProperty() {
public IntegerProperty widthProperty() {
return widthProperty;
}
@@ -392,9 +397,9 @@ public final class VersionSetting {
}
private final ImmediateIntegerProperty heightProperty = new ImmediateIntegerProperty(this, "height", 480);
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
public ImmediateIntegerProperty heightProperty() {
public IntegerProperty heightProperty() {
return heightProperty;
}
@@ -417,9 +422,9 @@ public final class VersionSetting {
* 0 - .minecraft<br/>
* 1 - .minecraft/versions/&lt;version&gt;/<br/>
*/
private final ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty = new ImmediateObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
private final ObjectProperty<EnumGameDirectory> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
public ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
public ObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
return gameDirTypeProperty;
}
@@ -434,9 +439,9 @@ public final class VersionSetting {
/**
* Your custom gameDir
*/
private final ImmediateStringProperty gameDirProperty = new ImmediateStringProperty(this, "gameDir", "");
private final StringProperty gameDirProperty = new SimpleStringProperty(this, "gameDir", "");
public ImmediateStringProperty gameDirProperty() {
public StringProperty gameDirProperty() {
return gameDirProperty;
}
@@ -455,9 +460,9 @@ public final class VersionSetting {
* 1 - Hide the launcher when the game starts.<br/>
* 2 - Keep the launcher open.<br/>
*/
private final ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty = new ImmediateObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
private final ObjectProperty<LauncherVisibility> launcherVisibilityProperty = new SimpleObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
public ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
public ObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
return launcherVisibilityProperty;
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.JFXUtilities;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
@@ -34,7 +33,11 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountList;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileList;
@@ -45,7 +48,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -57,6 +60,7 @@ import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
@@ -158,7 +162,7 @@ public final class Controllers {
});
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(
MultiStepBinding.of(UpdateChecker.latestVersionProperty())
BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
Profiles.registerVersionsListener(profile -> {
@@ -173,7 +177,7 @@ public final class Controllers {
return node;
})
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.getVersions().setAll(children);
});
@@ -223,14 +227,14 @@ public final class Controllers {
}
public static void dialog(String text, String title) {
dialog(text, title, MessageBox.INFORMATION_MESSAGE);
dialog(text, title, MessageType.INFORMATION);
}
public static void dialog(String text, String title, int type) {
public static void dialog(String text, String title, MessageType type) {
dialog(text, title, type, null);
}
public static void dialog(String text, String title, int type, Runnable onAccept) {
public static void dialog(String text, String title, MessageType type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, type, onAccept));
}
@@ -17,24 +17,26 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.JFXUtilities;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.task.SilentException;
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public final class DialogController {
public static AuthInfo logIn(Account account) throws Exception {
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
JFXUtilities.runInFX(() -> {
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
@@ -42,7 +44,7 @@ public final class DialogController {
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
}
return account.logIn();
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
@@ -31,6 +30,7 @@ import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
@@ -44,10 +44,11 @@ import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -74,6 +75,14 @@ public final class FXUtils {
private FXUtils() {
}
public static void runInFX(Runnable runnable) {
if (Platform.isFxApplicationThread()) {
runnable.run();
} else {
Platform.runLater(runnable);
}
}
public static void checkFxUserThread() {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not on FX application thread; currentThread = "
@@ -153,6 +162,20 @@ public final class FXUtils {
});
}
public static <K, T> void setupCellValueFactory(JFXTreeTableColumn<K, T> column, Function<K, ObservableValue<T>> mapper) {
column.setCellValueFactory(param -> {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
});
}
public static Node wrapMargin(Node node, Insets insets) {
StackPane.setMargin(node, insets);
return new StackPane(node);
}
public static void setValidateWhileTextChanged(Node field, boolean validate) {
if (field instanceof JFXTextField) {
if (validate) {
@@ -251,7 +274,7 @@ public final class FXUtils {
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
try {
// Java 8
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
@@ -331,7 +354,7 @@ public final class FXUtils {
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
textField.textProperty().bindBidirectional(property, SafeIntStringConverter.INSTANCE);
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
}
public static void unbindInt(JFXTextField textField, Property<Number> property) {
@@ -510,6 +533,7 @@ public final class FXUtils {
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(graphicBuilder.apply(item));
}
}
@@ -17,10 +17,13 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Consumer;
@@ -30,26 +33,37 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
* @author huangyuhui
*/
public class InstallerItem extends BorderPane {
private final Consumer<InstallerItem> deleteCallback;
@FXML
private Label lblInstallerArtifact;
@FXML
private Label lblInstallerVersion;
public InstallerItem(String artifact, String version, Consumer<InstallerItem> deleteCallback) {
this.deleteCallback = deleteCallback;
FXUtils.loadFXML(this, "/assets/fxml/version/installer-item.fxml");
public InstallerItem(String artifact, String version, Runnable upgrade, Consumer<InstallerItem> deleteCallback) {
getStyleClass().add("two-line-list-item");
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblInstallerArtifact.setText(artifact);
lblInstallerVersion.setText(i18n("archive.version") + ": " + version);
{
TwoLineListItem item = new TwoLineListItem();
item.setTitle(artifact);
item.setSubtitle(i18n("archive.version") + ": " + version);
setCenter(item);
}
{
HBox hBox = new HBox();
JFXButton upgradeButton = new JFXButton();
upgradeButton.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
upgradeButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(upgradeButton, i18n("install.change_version"));
upgradeButton.setOnMouseClicked(e -> upgrade.run());
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
deleteButton.getStyleClass().add("toggle-icon4");
deleteButton.setOnMouseClicked(e -> deleteCallback.accept(this));
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.getChildren().setAll(upgradeButton, deleteButton);
setRight(hBox);
}
}
@FXML
private void onDelete() {
deleteCallback.accept(this);
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
@@ -25,7 +24,6 @@ import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
@@ -44,6 +42,7 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LeftPaneController extends AdvancedListBox {
@@ -112,7 +111,7 @@ public final class LeftPaneController extends AdvancedListBox {
private boolean checkedModpack = false;
private void onRefreshedVersions(HMCLGameRepository repository) {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (!checkedModpack) {
checkedModpack = true;
@@ -120,14 +119,13 @@ public final class LeftPaneController extends AdvancedListBox {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
.thenResult(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenResult(modpack -> {
.thenApply(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenAccept(modpack -> {
AtomicReference<Region> region = new AtomicReference<>();
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), this::checkAccount)).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing")));
executor.start();
return null;
}).start();
}
}
@@ -18,18 +18,11 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
public abstract class ListPage<T extends Node> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public abstract class ListPage<T extends Node> extends ListPageBase<T> {
private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false);
public abstract void add();
@@ -42,30 +35,6 @@ public abstract class ListPage<T extends Node> extends Control {
return new ListPageSkin(this);
}
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
public boolean isRefreshable() {
return refreshable.get();
}
@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Control;
public class ListPageBase<T> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
}
@@ -201,11 +201,11 @@ public final class LogWindow extends Stage {
if (!flag)
cboLines.getSelectionModel().select(0);
btnFatals.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(fatal.get()) + " fatals", fatal));
btnErrors.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(error.get()) + " errors", error));
btnWarns.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(warn.get()) + " warns", warn));
btnInfos.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(info.get()) + " infos", info));
btnDebugs.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(debug.get()) + " debugs", debug));
btnFatals.textProperty().bind(Bindings.concat(fatal, " fatals"));
btnErrors.textProperty().bind(Bindings.concat(error, " errors"));
btnWarns.textProperty().bind(Bindings.concat(warn, " warns"));
btnInfos.textProperty().bind(Bindings.concat(info, " infos"));
btnDebugs.textProperty().bind(Bindings.concat(debug, " debugs"));
btnFatals.selectedProperty().addListener(o -> specificChanged());
btnErrors.selectedProperty().addListener(o -> specificChanged());
@@ -29,6 +29,10 @@ public final class SVG {
private SVG() {
}
public interface SVGIcon {
Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
}
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
@@ -1,43 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.Lang;
import java.util.Optional;
/**
* @author huangyuhui
*/
public final class SafeIntStringConverter extends StringConverter<Number> {
public static final SafeIntStringConverter INSTANCE = new SafeIntStringConverter();
private SafeIntStringConverter() {
}
@Override
public Number fromString(String string) {
return Optional.ofNullable(string).map(Lang::toIntOrNull).orElse(null);
}
@Override
public String toString(Number object) {
return Optional.ofNullable(object).map(Object::toString).orElse("");
}
}
@@ -31,7 +31,7 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
@@ -228,7 +228,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
try {
Files.write(logFile, Logging.getRawLogs());
} catch (IOException e) {
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + e, null, MessageBox.ERROR_MESSAGE));
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + e, null, MessageType.ERROR));
LOG.log(Level.WARNING, "Failed to export logs", e);
return;
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
@@ -72,7 +73,7 @@ public abstract class SettingsView extends StackPane {
{
VBox rootPane = new VBox();
rootPane.setStyle("-fx-padding: 18;");
rootPane.setPadding(new Insets(36, 18, 36, 18));
{
ComponentList settingsPane = new ComponentList();
{
@@ -80,12 +81,31 @@ public abstract class SettingsView extends StackPane {
StackPane sponsorPane = new StackPane();
sponsorPane.setCursor(Cursor.HAND);
sponsorPane.setOnMouseClicked(e -> onSponsor());
GridPane gridPane = new GridPane();
ColumnConstraints col = new ColumnConstraints();
col.setHgrow(Priority.SOMETIMES);
col.setMaxWidth(Double.POSITIVE_INFINITY);
gridPane.getColumnConstraints().setAll(col);
RowConstraints row = new RowConstraints();
row.setMinHeight(Double.NEGATIVE_INFINITY);
row.setValignment(VPos.TOP);
row.setVgrow(Priority.SOMETIMES);
gridPane.getRowConstraints().setAll(row);
{
Label label = new Label(i18n("sponsor.hmcl"));
label.setWrapText(true);
label.prefWidthProperty().bind(sponsorPane.widthProperty());
sponsorPane.getChildren().add(label);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
sponsorPane.getChildren().setAll(gridPane);
settingsPane.getContent().add(sponsorPane);
}
@@ -0,0 +1,96 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import java.util.List;
public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>> extends SkinBase<T> {
public ToolbarListPageSkin(T skinnable) {
super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
BorderPane root = new BorderPane();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(initializeToolbar(skinnable));
root.setTop(toolbar);
}
{
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
VBox content = new VBox();
content.setSpacing(10);
content.setPadding(new Insets(10));
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
root.setCenter(scrollPane);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
}
public static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
}
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
return ret;
}
protected abstract List<Node> initializeToolbar(T skinnable);
}
@@ -37,6 +37,7 @@ import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
public class AccountListItem extends RadioButton {
@@ -84,6 +85,8 @@ public class AccountListItem extends RadioButton {
} catch (CredentialExpiredException e) {
try {
DialogController.logIn(account);
} catch (CancellationException e1) {
// ignore cancellation
} catch (Exception e1) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
}
@@ -17,12 +17,9 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
@@ -31,16 +28,16 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
public class AccountListItemSkin extends SkinBase<AccountListItem> {
public AccountListItemSkin(AccountListItem skinnable) {
@@ -75,7 +72,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
if (skinnable.getAccount() instanceof AuthlibInjectorAccount) {
Tooltip tooltip = new Tooltip();
AuthlibInjectorServer server = ((AuthlibInjectorAccount) skinnable.getAccount()).getServer();
tooltip.textProperty().bind(Bindings.createStringBinding(server::toString, server));
tooltip.textProperty().bind(BindingMapping.of(server, AuthlibInjectorServer::toString));
FXUtils.installSlowTooltip(subtitle, tooltip);
}
VBox item = new VBox(title, subtitle);
@@ -91,7 +88,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
right.getChildren().add(btnRefresh);
JFXButton btnRemove = new JFXButton();
@@ -99,7 +96,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -62,7 +62,7 @@ public class AccountLoginPane extends StackPane {
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult(() -> oldAccount.logInWithPassword(password))
.finalizedResult(Schedulers.javafx(), authInfo -> {
.whenComplete(Schedulers.javafx(), authInfo -> {
success.accept(authInfo);
fireEvent(new DialogCloseEvent());
progressBar.setVisible(false);
@@ -17,9 +17,10 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
@@ -47,12 +48,13 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
@@ -126,7 +128,7 @@ public class AddAccountPane extends StackPane {
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
// authlib-injector links
links.bind(MultiStepBinding.of(cboServers.getSelectionModel().selectedItemProperty())
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
.map(AddAccountPane::createHyperlinks)
.map(FXCollections::observableList));
Bindings.bindContent(linksContainer.getChildren(), links);
@@ -196,7 +198,7 @@ public class AddAccountPane extends StackPane {
Object additionalData = getAuthAdditionalData();
Task.ofResult(() -> factory.create(new Selector(), username, password, additionalData))
.finalizedResult(Schedulers.javafx(), account -> {
.whenComplete(Schedulers.javafx(), account -> {
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
@@ -293,7 +295,7 @@ public class AddAccountPane extends StackPane {
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException();
} finally {
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
}
}
}
@@ -106,11 +106,11 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.locateServer(url);
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
}).whenComplete(Schedulers.javafx(), (isDependentSucceeded, exception) -> {
addServerPane.setDisable(false);
nextPane.hideSpinner();
if (isDependentsSucceeded) {
if (isDependentSucceeded) {
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
@@ -118,7 +118,6 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
Exception exception = variables.get("lastException");
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
}
@@ -21,7 +21,7 @@ import static javafx.collections.FXCollections.emptyObservableList;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListCell;
@@ -49,7 +49,7 @@ public class FontComboBox extends JFXComboBox<String> {
}
});
itemsProperty().bind(MultiStepBinding.of(valueProperty())
itemsProperty().bind(BindingMapping.of(valueProperty())
.map(value -> value == null ? emptyObservableList() : singletonObservableList(value)));
setOnMouseClicked(e -> {
@@ -26,11 +26,12 @@ import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
private final StackPane pane;
private final CheckBox checkBox;
private boolean showLabel;
private ObservableValue<Boolean> booleanProperty;
@@ -49,6 +50,8 @@ public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
final StringConverter<T> converter) {
this.getStyleClass().add("check-box-tree-table-cell");
this.checkBox = new JFXCheckBox();
this.pane = new StackPane(checkBox);
this.pane.setAlignment(Pos.CENTER);
setGraphic(null);
setSelectedStateCallback(getSelectedProperty);
setConverter(converter);
@@ -101,7 +104,8 @@ public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
if (showLabel) {
setText(c.toString(item));
}
setGraphic(checkBox);
setGraphic(pane);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
@@ -1,172 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
import org.jackhuang.hmcl.ui.FXUtils;
import javax.swing.*;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageBox {
private MessageBox() {
}
private static final String TITLE = i18n("message.info");
/**
* User Operation: Yes
*/
public static final int YES_OPTION = 0;
/**
* User Operation: No
*/
public static final int NO_OPTION = 1;
/**
* User Operation: Cancel
*/
public static final int CANCEL_OPTION = 2;
/**
* User Operation: OK
*/
public static final int OK_OPTION = 0;
/**
* User Operation: Closed Message Box
*/
public static final int CLOSED_OPTION = -1;
/**
* Buttons: Yes No
*/
public static final int YES_NO_OPTION = 10;
/**
* Buttons: Yes No Cancel
*/
public static final int YES_NO_CANCEL_OPTION = 11;
/**
* Buttons: OK Cancel
*/
public static final int OK_CANCEL_OPTION = 12;
/**
* Message Box Type: Error
*/
public static final int ERROR_MESSAGE = 0;
/**
* Message Box Type: Info
*/
public static final int INFORMATION_MESSAGE = 1;
/**
* Message Box Type: Warning
*/
public static final int WARNING_MESSAGE = 2;
/**
* Message Box Type: Question
*/
public static final int QUESTION_MESSAGE = 3;
/**
* Message Box Type: Fine
*/
public static final int FINE_MESSAGE = 4;
/**
* Message Box Type: Plain
*/
public static final int PLAIN_MESSAGE = -1;
public static void show(String message) {
show(message, TITLE);
}
/**
* Show MsgBox with title and options
*
* @param message The Message
* @param title The title of MsgBox.
* @return user operation.
*/
public static Optional<ButtonType> show(String message, String title) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
return alert.showAndWait();
}
public static int confirm(String message, String title) {
return confirm(message, title, -1);
}
public static int confirm(String message, int option) {
return confirm(message, TITLE, option);
}
public static int confirm(String message, String title, int option) {
return FXUtils.runInUIThread(() -> {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
switch (option) {
case YES_NO_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
break;
case YES_NO_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
break;
case OK_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
break;
default:
throw new IllegalArgumentException("Unrecognized message box option " + option);
}
Optional<ButtonType> buttonType = alert.showAndWait();
if (!buttonType.isPresent()) return CLOSED_OPTION;
else if (buttonType.get() == ButtonType.OK) return OK_OPTION;
else if (buttonType.get() == ButtonType.YES) return YES_OPTION;
else if (buttonType.get() == ButtonType.NO) return NO_OPTION;
else if (buttonType.get() == ButtonType.CANCEL) return CANCEL_OPTION;
else throw new IllegalStateException("Unexpected button type:" + buttonType.get());
});
}
public static Optional<String> input(String message) {
return input(message, UIManager.getString("OptionPane.inputDialogTitle"));
}
public static Optional<String> input(String message, String title) {
return input(message, title, "");
}
public static Optional<String> input(String message, String title, String initialValue) {
TextInputDialog dialog = new TextInputDialog(initialValue);
dialog.setTitle(title);
dialog.setHeaderText(message);
dialog.setContentText(message);
return dialog.showAndWait();
}
}
@@ -32,6 +32,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
public static enum MessageType {
ERROR,
INFORMATION,
WARNING,
QUESTION,
FINE,
PLAIN,
}
@FXML
private JFXButton acceptButton;
@FXML
@@ -45,7 +54,7 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, int type, Runnable onAccept) {
public MessageDialogPane(String text, String title, MessageType type, Runnable onAccept) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
if (title != null)
@@ -60,19 +69,19 @@ public final class MessageDialogPane extends StackPane {
actions.getChildren().remove(cancelButton);
switch (type) {
case MessageBox.INFORMATION_MESSAGE:
case INFORMATION:
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.ERROR_MESSAGE:
case ERROR:
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.FINE_MESSAGE:
case FINE:
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.WARNING_MESSAGE:
case WARNING:
graphic.setGraphic(SVG.alert(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.QUESTION_MESSAGE:
case QUESTION:
graphic.setGraphic(SVG.helpCircle(Theme.blackFillBinding(), 40, 40));
break;
default:
@@ -81,7 +90,7 @@ public final class MessageDialogPane extends StackPane {
}
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageBox.QUESTION_MESSAGE, onAccept);
this(text, title, MessageType.QUESTION, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
@@ -75,12 +75,12 @@ public class RipplerContainer extends StackPane {
focusedProperty().addListener((a, b, newValue) -> {
if (newValue) {
if (!isPressed())
buttonRippler.setOverlayVisible(true);
buttonRippler.showOverlay();
} else {
buttonRippler.setOverlayVisible(false);
buttonRippler.hideOverlay();
}
});
pressedProperty().addListener(o -> buttonRippler.setOverlayVisible(false));
pressedProperty().addListener(o -> buttonRippler.hideOverlay());
setPickOnBounds(false);
buttonContainer.setPickOnBounds(false);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
@@ -34,6 +33,8 @@ import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Consumer<Region> onCancel;
@@ -121,6 +122,6 @@ public class TaskExecutorDialogPane extends StackPane {
public void setCancel(Consumer<Region> onCancel) {
this.onCancel = onCancel;
JFXUtilities.runInFX(() -> btnCancel.setDisable(onCancel == null));
runInFX(() -> btnCancel.setDisable(onCancel == null));
}
}
@@ -22,13 +22,15 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
@@ -89,16 +91,16 @@ public final class InstallerWizardProvider implements WizardProvider {
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
Task ret = Task.ofResult("version", () -> version);
TaskResult<Version> ret = Task.ofResult(() -> version);
if (settings.containsKey("forge"))
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
if (settings.containsKey("liteloader"))
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
return ret.then(profile.getRepository().refreshVersionsAsync());
}
@@ -121,11 +123,13 @@ public final class InstallerWizardProvider implements WizardProvider {
public static void alertFailureMessage(Exception exception, Runnable next) {
if (exception instanceof LibraryDownloadException) {
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
} else if (exception instanceof DownloadException) {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException) {
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageType.ERROR, next);
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageType.ERROR, next);
}
}
}
@@ -27,7 +27,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
@@ -84,11 +84,11 @@ public class ModpackInstallWizardProvider implements WizardProvider {
try {
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
} catch (UnsupportedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
} catch (IOException e) {
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageType.ERROR);
}
return null;
} else {
@@ -105,9 +105,9 @@ public class ModpackInstallWizardProvider implements WizardProvider {
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof CurseCompletionException) {
if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageType.INFORMATION, next);
}
} else {
InstallerWizardProvider.alertFailureMessage(exception, next);
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebStage;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -110,8 +110,8 @@ public final class ModpackPage extends StackPane implements WizardPage {
spinnerPane.showSpinner();
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
.thenResult(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.finalizedResult(Schedulers.javafx(), manifest -> {
.thenApply(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.whenComplete(Schedulers.javafx(), manifest -> {
spinnerPane.hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
@@ -129,7 +129,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
}
}, e -> {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
Platform.runLater(controller::onEnd);
}).start();
}
@@ -0,0 +1,88 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import java.util.LinkedList;
import java.util.Map;
import static org.jackhuang.hmcl.ui.download.InstallerWizardProvider.alertFailureMessage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class UpdateInstallerWizardProvider implements WizardProvider {
private final Profile profile;
private final String gameVersion;
private final Version version;
private final String libraryId;
private final Library oldLibrary;
public UpdateInstallerWizardProvider(Profile profile, String gameVersion, Version version, String libraryId, Library oldLibrary) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
this.libraryId = libraryId;
this.oldLibrary = oldLibrary;
}
@Override
public void start(Map<String, Object> settings) {
}
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
// We remove library but not save it,
// so if installation failed will not break down current version.
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(oldLibrary);
return new MaintainTask(version.setLibraries(newList))
.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get(libraryId)))
.then(profile.getRepository().refreshVersionsAsync());
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
DownloadProvider provider = profile.getDependency().getDownloadProvider();
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, provider, libraryId, () -> {
Controllers.confirmDialog(i18n("install.change_version.confirm", i18n("install.installer." + libraryId), oldLibrary.getVersion(), ((RemoteVersion) settings.get(libraryId)).getSelfVersion()),
i18n("install.change_version"), controller::onFinish, controller::onCancel);
});
default:
throw new IllegalStateException();
}
}
@Override
public boolean cancel() {
return true;
}
}
@@ -59,7 +59,7 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
if (settings.containsKey("optifine"))
builder.version((RemoteVersion) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions())
return builder.buildAsync().whenComplete((a, b) -> profile.getRepository().refreshVersions())
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
}
@@ -128,8 +128,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override
public void refresh() {
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
executor = versionList.refreshAsync(gameVersion, downloadProvider).finalized((variables, isDependentsSucceeded) -> {
if (isDependentsSucceeded) {
executor = versionList.refreshAsync(gameVersion, downloadProvider).whenComplete((isDependentSucceeded, exception) -> {
if (isDependentSucceeded) {
List<VersionsPageItem> items = loadVersions();
Platform.runLater(() -> {
@@ -147,7 +147,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
}
});
} else {
LOG.log(Level.WARNING, "Failed to fetch versions list", (Throwable) variables.get("lastException"));
LOG.log(Level.WARNING, "Failed to fetch versions list", exception);
Platform.runLater(() -> {
transitionHandler.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
});
@@ -72,7 +72,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
@@ -1,60 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListItem extends BorderPane {
public DatapackListItem(Datapack.Pack info, Consumer<DatapackListItem> deleteCallback) {
JFXCheckBox chkEnabled = new JFXCheckBox();
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
setLeft(chkEnabled);
TwoLineListItem modItem = new TwoLineListItem();
BorderPane.setAlignment(modItem, Pos.CENTER);
setCenter(modItem);
JFXButton btnRemove = new JFXButton();
FXUtils.installFastTooltip(btnRemove, i18n("datapack.remove"));
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(btnRemove);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
modItem.setTitle(info.getId());
modItem.setSubtitle(info.getDescription());
chkEnabled.selectedProperty().bindBidirectional(info.activeProperty());
}
}
@@ -19,11 +19,15 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -32,16 +36,19 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPage<DatapackListItem> implements DecoratorPage {
public class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty();
private final Path worldDir;
private final Datapack datapack;
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
public DatapackListPage(String worldName, Path worldDir) {
this.worldDir = worldDir;
@@ -50,14 +57,7 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir();
setItems(MappedObservableList.create(datapack.getInfo(),
pack -> new DatapackListItem(pack, item -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
Logging.LOG.warning("Failed to delete datapack");
}
})));
setItems(items = MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)), mods -> {
mods.forEach(it -> {
@@ -69,7 +69,19 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
}
});
}, datapack::loadFromDir);
}, this::refresh);
}
@Override
protected Skin<?> createDefaultSkin() {
return new DatapackListPageSkin(this);
}
public void refresh() {
setLoading(true);
Task.of(datapack::loadFromDir)
.with(Task.of(() -> setLoading(false)))
.start();
}
@Override
@@ -77,7 +89,6 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
return title;
}
@Override
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));
@@ -97,4 +108,32 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
datapack.loadFromDir();
}
void removeSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(pack -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
// Fail to remove mods if the game is running or the datapack is absent.
Logging.LOG.warning("Failed to delete datapack " + pack);
}
});
}
void enableSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(info -> info.setActive(true));
}
void disableSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(info -> info.setActive(false));
}
}
@@ -0,0 +1,126 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXTreeTableColumn;
import com.jfoenix.controls.JFXTreeTableView;
import com.jfoenix.controls.RecursiveTreeItem;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.setupCellValueFactory;
import static org.jackhuang.hmcl.ui.FXUtils.wrapMargin;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class DatapackListPageSkin extends SkinBase<DatapackListPage> {
DatapackListPageSkin(DatapackListPage skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
JFXTreeTableView<DatapackInfoObject> tableView = new JFXTreeTableView<>();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton(i18n("datapack.add"), SVG::plus, skinnable::add));
toolbar.getChildren().add(createToolbarButton(i18n("mods.remove"), SVG::delete, () ->
skinnable.removeSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
skinnable.enableSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
skinnable.disableSelected(tableView.getSelectionModel().getSelectedItems())));
root.setTop(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
tableView.getStyleClass().addAll("no-header");
tableView.setShowRoot(false);
tableView.setEditable(true);
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.setRoot(new RecursiveTreeItem<>(skinnable.getItems(), RecursiveTreeObject::getChildren));
JFXTreeTableColumn<DatapackInfoObject, Boolean> activeColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(activeColumn, DatapackInfoObject::activeProperty);
activeColumn.setCellFactory(c -> new JFXCheckBoxTreeTableCell<>());
activeColumn.setEditable(true);
activeColumn.setMaxWidth(40);
activeColumn.setMinWidth(40);
JFXTreeTableColumn<DatapackInfoObject, Node> detailColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(detailColumn, DatapackInfoObject::nodeProperty);
tableView.getColumns().setAll(activeColumn, detailColumn);
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
center.setContent(tableView);
root.setCenter(center);
}
getChildren().setAll(root);
}
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
private final BooleanProperty active;
private final Datapack.Pack packInfo;
private final ObjectProperty<Node> node;
DatapackInfoObject(Datapack.Pack packInfo) {
this.packInfo = packInfo;
this.active = packInfo.activeProperty();
this.node = new SimpleObjectProperty<>(wrapMargin(new TwoLineListItem(packInfo.getId(), StringUtils.parseColorEscapes(packInfo.getDescription())),
new Insets(8, 0, 8, 0)));
}
BooleanProperty activeProperty() {
return active;
}
ObjectProperty<Node> nodeProperty() {
return node;
}
Datapack.Pack getPackInfo() {
return packInfo;
}
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
@@ -25,11 +26,20 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.setting.Profile;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class GameItem extends Control {
private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 1, TimeUnit.SECONDS);
private final Profile profile;
private final String version;
private final StringProperty title = new SimpleStringProperty();
@@ -40,16 +50,19 @@ public class GameItem extends Control {
this.profile = profile;
this.version = id;
String game = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse("Unknown");
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getVersion(id));
analyzer.getForge().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))));
analyzer.getOptiFine().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))));
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
CompletableFuture.supplyAsync(() -> GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse("Unknown"), POOL_VERSION_RESOLVE)
.thenAcceptAsync(game -> {
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getVersion(id));
analyzer.getForge().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))));
analyzer.getOptiFine().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))));
subtitle.set(libraries.toString());
}, Platform::runLater)
.exceptionally(handleUncaught);
title.set(id);
subtitle.set(libraries.toString());
image.set(profile.getRepository().getVersionIconImage(version));
}
@@ -17,12 +17,10 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.Node;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
@@ -30,32 +28,31 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameList extends Control implements DecoratorPage {
public class GameList extends ListPageBase<GameListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(I18n.i18n("version.manage"));
private final BooleanProperty loading = new SimpleBooleanProperty(true);
private final ListProperty<GameListItem> items = new SimpleListProperty<>(FXCollections.observableArrayList());
private ToggleGroup toggleGroup;
public GameList() {
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == Profiles.getSelectedProfile().getRepository())
JFXUtilities.runInFX(() -> loading.set(true));
runInFX(() -> setLoading(true));
});
Profiles.registerVersionsListener(this::loadVersions);
@@ -72,10 +69,10 @@ public class GameList extends Control implements DecoratorPage {
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> new GameListItem(toggleGroup, profile, version.getId()))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
loading.set(false);
items.setAll(children);
setLoading(false);
itemsProperty().setAll(children);
children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
@@ -97,8 +94,8 @@ public class GameList extends Control implements DecoratorPage {
}
@Override
protected Skin<?> createDefaultSkin() {
return new GameListSkin(this);
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
}
void addNewGame() {
@@ -128,11 +125,20 @@ public class GameList extends Control implements DecoratorPage {
return title.getReadOnlyProperty();
}
public BooleanProperty loadingProperty() {
return loading;
}
private class GameListSkin extends ToolbarListPageSkin<GameList> {
public ListProperty<GameListItem> itemsProperty() {
return items;
public GameListSkin() {
super(GameList.this);
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Arrays.asList(
createToolbarButton(i18n("install.new_game"), SVG::plus, skinnable::addNewGame),
createToolbarButton(i18n("install.modpack"), SVG::importIcon, skinnable::importModpack),
createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, skinnable::modifyGlobalGameSettings)
);
}
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXRadioButton;
@@ -33,6 +32,7 @@ import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameListItemSkin extends SkinBase<GameListItem> {
@@ -73,7 +73,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
right.getChildren().add(btnUpgrade);
}
@@ -1,116 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.i18n.I18n;
public class GameListSkin extends SkinBase<GameList> {
private static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
}
public GameListSkin(GameList skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
JFXButton btnAddNewGame = new JFXButton();
btnAddNewGame.getStyleClass().add("jfx-tool-bar-button");
btnAddNewGame.textFillProperty().bind(Theme.foregroundFillBinding());
btnAddNewGame.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
btnAddNewGame.setText(I18n.i18n("install.new_game"));
btnAddNewGame.setOnMouseClicked(e -> skinnable.addNewGame());
toolbar.getChildren().add(btnAddNewGame);
JFXButton btnImportModpack = new JFXButton();
btnImportModpack.getStyleClass().add("jfx-tool-bar-button");
btnImportModpack.textFillProperty().bind(Theme.foregroundFillBinding());
btnImportModpack.setGraphic(wrap(SVG.importIcon(Theme.foregroundFillBinding(), -1, -1)));
btnImportModpack.setText(I18n.i18n("install.modpack"));
btnImportModpack.setOnMouseClicked(e -> skinnable.importModpack());
toolbar.getChildren().add(btnImportModpack);
JFXButton btnRefresh = new JFXButton();
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding());
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)));
btnRefresh.setText(I18n.i18n("button.refresh"));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
toolbar.getChildren().add(btnRefresh);
JFXButton btnModify = new JFXButton();
btnModify.getStyleClass().add("jfx-tool-bar-button");
btnModify.textFillProperty().bind(Theme.foregroundFillBinding());
btnModify.setGraphic(wrap(SVG.gear(Theme.foregroundFillBinding(), -1, -1)));
btnModify.setText(I18n.i18n("settings.type.global.manage"));
btnModify.setOnMouseClicked(e -> skinnable.modifyGlobalGameSettings());
toolbar.getChildren().add(btnModify);
root.setTop(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
center.loadingProperty().bind(skinnable.loadingProperty());
center.getStyleClass().add("large-spinner-pane");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
VBox gameList = new VBox();
gameList.maxWidthProperty().bind(scrollPane.widthProperty());
gameList.setSpacing(10);
gameList.setStyle("-fx-padding: 10 10 10 10;");
Bindings.bindContent(gameList.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(gameList);
JFXScrollPane.smoothScrolling(scrollPane);
center.setContent(scrollPane);
root.setCenter(center);
}
getChildren().setAll(root);
}
}
@@ -30,9 +30,9 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.download.InstallerWizardProvider;
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
import java.util.LinkedList;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -42,37 +42,50 @@ public class InstallerListPage extends ListPage<InstallerItem> {
private Profile profile;
private String versionId;
private Version version;
private String gameVersion;
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
this.version = profile.getRepository().getResolvedVersion(versionId);
this.gameVersion = null;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
Task.ofResult(() -> {
gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse(null);
Function<Library, Consumer<InstallerItem>> removeAction = library -> x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
return LibraryAnalyzer.analyze(version);
}).thenAccept(Schedulers.javafx(), analyzer -> {
Function<Library, Consumer<InstallerItem>> removeAction = library -> x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(maintainedVersion -> new VersionJsonSaveTask(profile.getRepository(), maintainedVersion))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
itemsProperty().clear();
analyzer.getForge().ifPresent(library -> itemsProperty().add(new InstallerItem("Forge", library.getVersion(), removeAction.apply(library))));
analyzer.getLiteLoader().ifPresent(library -> itemsProperty().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction.apply(library))));
analyzer.getOptiFine().ifPresent(library -> itemsProperty().add(new InstallerItem("OptiFine", library.getVersion(), removeAction.apply(library))));
itemsProperty().clear();
analyzer.getForge().ifPresent(library -> itemsProperty().add(
new InstallerItem("Forge", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "forge", library));
}, removeAction.apply(library))));
analyzer.getLiteLoader().ifPresent(library -> itemsProperty().add(
new InstallerItem("LiteLoader", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "liteloader", library));
}, removeAction.apply(library))));
analyzer.getOptiFine().ifPresent(library -> itemsProperty().add(
new InstallerItem("OptiFine", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "optifine", library));
}, removeAction.apply(library))));
}).start();
}
@Override
public void add() {
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
if (!gameVersion.isPresent())
if (gameVersion == null)
Controllers.dialog(i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version));
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion, version));
}
}
@@ -17,18 +17,14 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXTabPane;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Profile;
@@ -36,7 +32,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -48,14 +44,15 @@ import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModListPage extends Control {
private final ListProperty<ModListPageSkin.ModInfoObject> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private JFXTabPane parentTab;
private ModManager modManager;
private LibraryAnalyzer libraryAnalyzer;
public ModListPage() {
@@ -81,20 +78,22 @@ public final class ModListPage extends Control {
}
public void loadVersion(Profile profile, String id) {
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedVersion(id));
modded.set(libraryAnalyzer.hasForge() || libraryAnalyzer.hasLiteLoader());
loadMods(profile.getRepository().getModManager(id));
}
public void loadMods(ModManager modManager) {
private void loadMods(ModManager modManager) {
this.modManager = modManager;
Task.ofResult("list", variables -> {
Task.ofResult(() -> {
synchronized (ModListPage.this) {
JFXUtilities.runInFX(() -> loadingProperty().set(true));
runInFX(() -> loadingProperty().set(true));
modManager.refreshMods();
return new LinkedList<>(modManager.getMods());
}
}).finalizedResult(Schedulers.javafx(), (list, isDependentsSucceeded) -> {
}).whenComplete(Schedulers.javafx(), (list, isDependentSucceeded, exception) -> {
loadingProperty().set(false);
if (isDependentsSucceeded)
if (isDependentSucceeded)
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModListPage.this)
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).collect(Collectors.toList()));
@@ -112,7 +111,7 @@ public final class ModListPage extends Control {
List<String> succeeded = new LinkedList<>();
List<String> failed = new LinkedList<>();
if (res == null) return;
Task.of(variables -> {
Task.of(() -> {
for (File file : res) {
try {
modManager.addMod(file);
@@ -124,7 +123,7 @@ public final class ModListPage extends Control {
// Actually addMod will not throw exceptions because FileChooser has already filtered files.
}
}
}).with(Task.of(Schedulers.javafx(), variables -> {
}).with(Task.of(Schedulers.javafx(), () -> {
List<String> prompt = new LinkedList<>();
if (!succeeded.isEmpty())
prompt.add(i18n("mods.add.success", String.join(", ", succeeded)));
@@ -139,7 +138,7 @@ public final class ModListPage extends Control {
this.parentTab = parentTab;
}
public void removeSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
public void removeSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
try {
modManager.removeMods(selectedItems.stream()
.map(TreeItem::getValue)
@@ -151,41 +150,29 @@ public final class ModListPage extends Control {
}
}
public void enableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
public void enableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(true));
}
public void disableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
public void disableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(false));
}
public ObservableList<ModListPageSkin.ModInfoObject> getItems() {
return items.get();
public boolean isModded() {
return modded.get();
}
public void setItems(ObservableList<ModListPageSkin.ModInfoObject> items) {
this.items.set(items);
public BooleanProperty moddedProperty() {
return modded;
}
public ListProperty<ModListPageSkin.ModInfoObject> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
public void setModded(boolean modded) {
this.modded.set(modded);
}
}
@@ -17,42 +17,43 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXTreeTableColumn;
import com.jfoenix.controls.JFXTreeTableView;
import com.jfoenix.controls.RecursiveTreeItem;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.setupCellValueFactory;
import static org.jackhuang.hmcl.ui.FXUtils.wrapMargin;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModListPageSkin extends SkinBase<ModListPage> {
class ModListPageSkin extends SkinBase<ModListPage> {
private static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
}
public ModListPageSkin(ModListPage skinnable) {
ModListPageSkin(ModListPage skinnable) {
super(skinnable);
StackPane pane = new StackPane();
pane.getStyleClass().addAll("notice-pane", "white-background");
BorderPane root = new BorderPane();
JFXTreeTableView<ModInfoObject> tableView = new JFXTreeTableView<>();
@@ -62,46 +63,14 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
JFXButton btnRefresh = new JFXButton();
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding());
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)));
btnRefresh.setText(i18n("button.refresh"));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
toolbar.getChildren().add(btnRefresh);
JFXButton btnAddMod = new JFXButton();
btnAddMod.getStyleClass().add("jfx-tool-bar-button");
btnAddMod.textFillProperty().bind(Theme.foregroundFillBinding());
btnAddMod.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
btnAddMod.setText(i18n("mods.add"));
btnAddMod.setOnMouseClicked(e -> skinnable.add());
toolbar.getChildren().add(btnAddMod);
JFXButton btnRemove = new JFXButton();
btnRemove.getStyleClass().add("jfx-tool-bar-button");
btnRemove.textFillProperty().bind(Theme.foregroundFillBinding());
btnRemove.setGraphic(wrap(SVG.delete(Theme.foregroundFillBinding(), -1, -1)));
btnRemove.setText(i18n("mods.remove"));
btnRemove.setOnMouseClicked(e -> skinnable.removeSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnRemove);
JFXButton btnEnable = new JFXButton();
btnEnable.getStyleClass().add("jfx-tool-bar-button");
btnEnable.textFillProperty().bind(Theme.foregroundFillBinding());
btnEnable.setGraphic(wrap(SVG.check(Theme.foregroundFillBinding(), -1, -1)));
btnEnable.setText(i18n("mods.enable"));
btnEnable.setOnMouseClicked(e -> skinnable.enableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnEnable);
JFXButton btnDisable = new JFXButton();
btnDisable.getStyleClass().add("jfx-tool-bar-button");
btnDisable.textFillProperty().bind(Theme.foregroundFillBinding());
btnDisable.setGraphic(wrap(SVG.close(Theme.foregroundFillBinding(), -1, -1)));
btnDisable.setText(i18n("mods.disable"));
btnDisable.setOnMouseClicked(e -> skinnable.disableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnDisable);
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton(i18n("mods.add"), SVG::plus, skinnable::add));
toolbar.getChildren().add(createToolbarButton(i18n("mods.remove"), SVG::delete, () ->
skinnable.removeSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
skinnable.enableSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
skinnable.disableSelected(tableView.getSelectionModel().getSelectedItems())));
root.setTop(toolbar);
}
@@ -110,6 +79,7 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
tableView.getStyleClass().add("no-header");
tableView.setShowRoot(false);
tableView.setEditable(true);
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
@@ -122,65 +92,54 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
activeColumn.setMaxWidth(40);
activeColumn.setMinWidth(40);
JFXTreeTableColumn<ModInfoObject, String> fileNameColumn = new JFXTreeTableColumn<>();
fileNameColumn.setText(i18n("archive.name"));
setupCellValueFactory(fileNameColumn, ModInfoObject::fileNameProperty);
fileNameColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.8));
JFXTreeTableColumn<ModInfoObject, Node> detailColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(detailColumn, ModInfoObject::nodeProperty);
JFXTreeTableColumn<ModInfoObject, String> versionColumn = new JFXTreeTableColumn<>();
versionColumn.setText(i18n("archive.version"));
versionColumn.setPrefWidth(100);
setupCellValueFactory(versionColumn, ModInfoObject::versionProperty);
versionColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.2));
tableView.getColumns().setAll(activeColumn, fileNameColumn, versionColumn);
tableView.getColumns().setAll(activeColumn, detailColumn);
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
center.setContent(tableView);
root.setCenter(center);
}
getChildren().setAll(root);
}
Label label = new Label(i18n("mods.not_modded"));
label.prefWidthProperty().bind(pane.widthProperty().add(-100));
private <T> void setupCellValueFactory(JFXTreeTableColumn<ModInfoObject, T> column, Function<ModInfoObject, ObservableValue<T>> mapper) {
column.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<ModInfoObject, T>, ObservableValue<T>>() {
@Override
public ObservableValue<T> call(TreeTableColumn.CellDataFeatures<ModInfoObject, T> param) {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
}
FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> {
if (modded) pane.getChildren().setAll(root);
else pane.getChildren().setAll(label);
});
getChildren().setAll(pane);
}
public static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
private final BooleanProperty active;
private final StringProperty fileName;
private final StringProperty version;
private final ModInfo modInfo;
private final ObjectProperty<Node> node;
public ModInfoObject(ModInfo modInfo) {
ModInfoObject(ModInfo modInfo) {
this.modInfo = modInfo;
this.active = modInfo.activeProperty();
this.fileName = new SimpleStringProperty(modInfo.getFileName());
this.version = new SimpleStringProperty(modInfo.getVersion());
StringBuilder message = new StringBuilder(modInfo.getName());
if (isNotBlank(modInfo.getVersion()))
message.append(", ").append(i18n("archive.version")).append(": ").append(modInfo.getVersion());
if (isNotBlank(modInfo.getGameVersion()))
message.append(", ").append(i18n("archive.game_version")).append(": ").append(modInfo.getGameVersion());
if (isNotBlank(modInfo.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(modInfo.getAuthors());
this.node = new SimpleObjectProperty<>(wrapMargin(new TwoLineListItem(modInfo.getFileName(), message.toString()), new Insets(8, 0, 8, 0)));
}
public BooleanProperty activeProperty() {
BooleanProperty activeProperty() {
return active;
}
public StringProperty fileNameProperty() {
return fileName;
ObjectProperty<Node> nodeProperty() {
return node;
}
public StringProperty versionProperty() {
return version;
}
public ModInfo getModInfo() {
ModInfo getModInfo() {
return modInfo;
}
}
@@ -57,7 +57,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -71,7 +71,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private Profile profile;
private String versionId;
private boolean javaItemsLoaded;
private InvalidationListener specificSettingsListener;
@FXML private VBox rootPane;
@FXML private ScrollPane scroll;
@@ -100,6 +99,12 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
@FXML private JFXCheckBox chkEnableSpecificSettings;
@FXML private BorderPane settingsTypePane;
private InvalidationListener specificSettingsListener = any -> {
chkEnableSpecificSettings.setSelected(!lastVersionSetting.isUsesGlobal());
};
private InvalidationListener javaListener = any -> initJavaSubtitle();
public VersionSettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml");
@@ -113,14 +118,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.smoothScrolling(scroll);
Task.ofResult(JavaVersion::getJavas).thenResult(Schedulers.javafx(), list -> {
Task.ofResult(JavaVersion::getJavas).thenAccept(Schedulers.javafx(), list -> {
javaItem.loadChildren(list.stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion() + i18n("settings.game.java_directory.bit",
javaVersion.getPlatform().getBit()), javaVersion.getBinary().toString(), javaVersion))
.collect(Collectors.toList()));
javaItemsLoaded = true;
initializeSelectedJava();
return null;
}).start();
javaItem.setSelectedData(null);
@@ -148,10 +152,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
Platform.runLater(() -> loadVersion(profile, versionId));
});
specificSettingsListener = o -> {
chkEnableSpecificSettings.setSelected(!lastVersionSetting.isUsesGlobal());
};
componentList.disableProperty().bind(chkEnableSpecificSettings.selectedProperty().not());
advancedSettingsPane.disableProperty().bind(chkEnableSpecificSettings.selectedProperty().not());
}
@@ -193,6 +193,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.unbindEnum(cboLauncherVisibility);
lastVersionSetting.usesGlobalProperty().removeListener(specificSettingsListener);
lastVersionSetting.javaDirProperty().removeListener(javaListener);
lastVersionSetting.javaProperty().removeListener(javaListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind();
@@ -232,9 +234,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
});
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
versionSetting.javaDirProperty().addListener(javaListener);
versionSetting.javaProperty().addListener(javaListener);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
@@ -243,6 +244,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lastVersionSetting = versionSetting;
initializeSelectedJava();
initJavaSubtitle();
loadIcon();
}
@@ -267,11 +269,14 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
}
private void initJavaSubtitle(VersionSetting versionSetting) {
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
.subscribe(Task.of(Schedulers.javafx(),
variables -> javaItem.setSubtitle(variables.<JavaVersion>getOptional("java")
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Directory"))));
private void initJavaSubtitle() {
VersionSetting versionSetting = lastVersionSetting;
if (versionSetting == null)
return;
Task.ofResult(versionSetting::getJavaVersion)
.thenAccept(Schedulers.javafx(), javaVersion -> javaItem.setSubtitle(Optional.ofNullable(javaVersion)
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
.start();
}
@FXML
@@ -35,6 +35,7 @@ import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListItem extends Control {
@@ -48,7 +49,7 @@ public class WorldListItem extends Control {
this.world = world;
this.simpleDateFormat = new SimpleDateFormat(i18n("world.time"));
title.set(world.getWorldName());
title.set(parseColorEscapes(world.getWorldName()));
subtitle.set(i18n("world.description", world.getFileName(), simpleDateFormat.format(new Date(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
}
@@ -17,49 +17,83 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListPage extends ListPage<WorldListItem> {
public class WorldListPage extends ListPageBase<WorldListItem> {
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
private Path savesDir;
private List<World> worlds;
private Profile profile;
private String id;
private String gameVersion;
public WorldListPage() {
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
installWorld(modpacks.get(0));
});
}
public void loadVersion(Profile profile, String id) {
this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves");
setLoading(true);
Task.ofResult(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
.finalizedResult(Schedulers.javafx(), (result, isDependentsSucceeded) -> {
setLoading(false);
if (isDependentsSucceeded)
itemsProperty().setAll(result.stream().map(WorldListItem::new).collect(Collectors.toList()));
}).start();
showAll.addListener(e -> {
if (worlds != null)
itemsProperty().setAll(worlds.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(WorldListItem::new).collect(Collectors.toList()));
});
}
@Override
protected ToolbarListPageSkin createDefaultSkin() {
return new WorldListPageSkin();
}
public void loadVersion(Profile profile, String id) {
this.profile = profile;
this.id = id;
this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves");
refresh();
}
public void refresh() {
if (profile == null || id == null)
return;
setLoading(true);
Task
.of(() -> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse(null))
.thenSupply(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
.whenComplete(Schedulers.javafx(), (result, isDependentSucceeded, exception) -> {
worlds = result;
setLoading(false);
if (isDependentSucceeded)
itemsProperty().setAll(result.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(WorldListItem::new).collect(Collectors.toList()));
}).start();
}
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.import.choose"));
@@ -74,10 +108,10 @@ public class WorldListPage extends ListPage<WorldListItem> {
// Only accept one world file because user is required to confirm the new world name
// Or too many input dialogs are popped.
Task.ofResult(() -> new World(zipFile.toPath()))
.finalizedResult(Schedulers.javafx(), world -> {
.whenComplete(Schedulers.javafx(), world -> {
Controllers.inputDialog(i18n("world.name.enter"), (name, resolve, reject) -> {
Task.of(() -> world.install(savesDir, name))
.finalized(Schedulers.javafx(), var -> {
.whenComplete(Schedulers.javafx(), () -> {
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(name))));
resolve.run();
}, e -> {
@@ -92,4 +126,36 @@ public class WorldListPage extends ListPage<WorldListItem> {
Controllers.dialog(i18n("world.import.invalid"));
}).start();
}
public boolean isShowAll() {
return showAll.get();
}
public BooleanProperty showAllProperty() {
return showAll;
}
public void setShowAll(boolean showAll) {
this.showAll.set(showAll);
}
private class WorldListPageSkin extends ToolbarListPageSkin<WorldListPage> {
WorldListPageSkin() {
super(WorldListPage.this);
}
@Override
protected List<Node> initializeToolbar(WorldListPage skinnable) {
JFXCheckBox chkShowAll = new JFXCheckBox();
chkShowAll.getStyleClass().add("jfx-tool-bar-checkbox");
chkShowAll.textFillProperty().bind(Theme.foregroundFillBinding());
chkShowAll.setText(i18n("world.show_all"));
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
return Arrays.asList(chkShowAll,
createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
createToolbarButton(i18n("world.add"), SVG::plus, skinnable::add));
}
}
}
@@ -17,19 +17,19 @@
*/
package org.jackhuang.hmcl.ui.wizard;
import com.jfoenix.utils.JFXUtilities;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Map;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplayer {
@@ -59,17 +59,17 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
pane.setSubtitle((String) subtitle);
}
JFXUtilities.runInFX(() -> {
runInFX(() -> {
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
pane.fireEvent(new DialogCloseEvent());
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
Controllers.dialog((String) settings.get("success_message"), null, MessageBox.FINE_MESSAGE, () -> onEnd());
Controllers.dialog((String) settings.get("success_message"), null, MessageType.FINE, () -> onEnd());
else if (!settings.containsKey("forbid_success_message"))
Controllers.dialog(i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> onEnd());
Controllers.dialog(i18n("message.success"), null, MessageType.FINE, () -> onEnd());
} else {
if (executor.getLastException() == null)
return;
@@ -77,9 +77,9 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
if (settings.get("failure_callback") instanceof WizardProvider.FailureCallback)
((WizardProvider.FailureCallback)settings.get("failure_callback")).onFail(settings, executor.getLastException(), () -> onEnd());
else if (settings.get("failure_message") instanceof String)
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> onEnd());
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageType.ERROR, () -> onEnd());
else if (!settings.containsKey("forbid_failure_message"))
Controllers.dialog(appendix, i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> onEnd());
Controllers.dialog(appendix, i18n("wizard.failed"), MessageType.ERROR, () -> onEnd());
}
});
@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
@@ -102,7 +102,7 @@ public final class UpdateHandler {
Task task = new HMCLDownloadTask(version, downloaded);
TaskExecutor executor = task.executor();
Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"));
Controllers.taskDialog(executor, i18n("message.downloading"));
thread(() -> {
boolean success = executor.test();
@@ -116,14 +116,14 @@ public final class UpdateHandler {
System.exit(0);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR));
return;
}
} else {
Throwable e = task.getLastException();
Throwable e = executor.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR));
}
});
}
@@ -18,9 +18,11 @@
package org.jackhuang.hmcl.util;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -69,7 +71,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
String info = entry.getValue();
LOG.severe(info);
try {
MessageBox.show(info);
Alert alert = new Alert(AlertType.INFORMATION, info);
alert.setTitle(i18n("message.info"));
alert.setHeaderText(i18n("message.info"));
alert.showAndWait();
} catch (Throwable t) {
LOG.log(Level.SEVERE, "Unable to show message", t);
}
@@ -58,10 +58,6 @@ public final class Locales {
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH);
public static SupportedLocale getLocale(int index) {
return Lang.get(LOCALES, index).orElse(DEFAULT);
}
public static SupportedLocale getLocaleByName(String name) {
if (name == null) return DEFAULT;
switch (name.toLowerCase()) {
+18 -6
View File
@@ -57,8 +57,11 @@
-fx-text-fill: -fx-base-text-fill;
}
.rippler-container= .label {
-fx-text-fill: white;
.notice-pane > .label {
-fx-text-fill: #0079FF;
-fx-font-size: 20;
-fx-wrap-text: true;
-fx-text-alignment: CENTER;
}
.class-title {
@@ -379,6 +382,8 @@
-fx-pref-height: 42;
-fx-padding: 2 2 2 2;
-fx-background-color: -fx-base-check-color;
-fx-alignment: CENTER-LEFT;
-fx-spacing: 8;
}
.jfx-tool-bar-second .jfx-rippler {
@@ -992,7 +997,8 @@
}
.jfx-decorator-title {
-fx-text-fill: -fx-base-text-fill; -fx-font-size: 15;
-fx-text-fill: -fx-base-text-fill;
-fx-font-size: 15;
}
.resize-border {
@@ -1010,7 +1016,7 @@
}
.white-background {
-fx-background-color: rgba(255, 255, 255);
-fx-background-color: rgb(255, 255, 255);
}
/*******************************************************************************
@@ -1019,6 +1025,12 @@
* *
*******************************************************************************/
.no-header .column-header-background {
-fx-max-height: 0;
-fx-pref-height: 0;
-fx-min-height: 0;
}
.tree-table-view {
-fx-tree-table-color: rgba(82, 100, 174, 0.4);
-fx-tree-table-rippler-color: rgba(82, 100, 174, 0.6);
@@ -1069,7 +1081,7 @@
.tree-table-view .tree-table-cell {
-fx-border-width: 0 0 0 0;
-fx-padding: 8 0 8 0;
-fx-padding: 0 0 0 0;
/*-fx-alignment: top-center;*/
}
@@ -1115,7 +1127,7 @@
}
.tree-table-row-group {
-fx-background-color: rgba(230, 230, 230);
-fx-background-color: rgb(230, 230, 230);
}
.tree-table-view .menu-item:focused {
@@ -24,11 +24,11 @@
<JFXListView fx:id="list" styleClass="jfx-list-view" VBox.vgrow="ALWAYS">
</JFXListView>
</VBox>
<StackPane fx:id="failedPane">
<Label onMouseClicked="#onRefresh" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.refresh" />
<StackPane fx:id="failedPane" styleClass="notice-pane">
<Label onMouseClicked="#onRefresh" text="%download.failed.refresh" />
</StackPane>
<StackPane fx:id="emptyPane">
<Label onMouseClicked="#onBack" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.empty" />
<StackPane fx:id="emptyPane" styleClass="notice-pane">
<Label onMouseClicked="#onBack" text="%download.failed.empty" />
</StackPane>
</StackPane>
</center>
@@ -95,7 +95,7 @@ download.failed=Failed to download
download.failed.empty=No candidates. Click here to return.
download.failed.refresh=Unable to load version list. Click here to retry.
download.provider.bmclapi=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.provider.mojang=Mojang
download.provider.mojang=Mojang (Forge and OptiFine installation being downloaded from BMCLAPI)
extension.bat=Windows Bat file
extension.mod=Mod file
@@ -124,9 +124,12 @@ input.not_empty=Required field
input.url=Must be a valid URL.
install=Install New Game
install.change_version=Change version
install.change_version.confirm=Sure to update %s from verison %s to %s?
install.failed=Failed to install
install.failed.downloading=Failed to install due to some files not downloaded successfully
install.failed.downloading.detail=Failed to download file: %s
install.failed.optifine_conflict=OptiFine and Forge are both installed simultaneously on Minecraft 1.13
install.installer.choose=Choose a %s version
install.installer.forge=Forge
install.installer.game=Game
@@ -258,6 +261,7 @@ mods.enable=Enable
mods.disable=Disable
mods.name=Name
mods.remove=Remove
mods.not_modded=You should install a mod loader first (Forge, or LiteLoader)
datapack=Data packs
datapack.add=Add data pack
@@ -267,6 +271,7 @@ datapack.title=World %s - Datapacks
datapack.remove=Remove
world=Worlds/Datapacks
world.add=Add world(.zip)
world.datapack=Manage data packs
world.datapack.1_13=Only Minecraft 1.13 and later versions support data packs.
world.description=%s. Last played time: %s. Game version: %s.
@@ -283,6 +288,7 @@ world.import.invalid=Not a valid world zip
world.name=World Name
world.name.enter=Enter the world name
world.reveal=Reveal in Explorer
world.show_all=Show all
world.time=EEE, MMM d, yyyy HH:mm:ss
profile=Game Directories
@@ -94,7 +94,7 @@ download.failed=下載失敗
download.failed.empty=沒有可供安裝的版本,點擊此處返回。
download.failed.refresh=載入版本列表失敗,點擊此處重試。
download.provider.bmclapi=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方伺服器(Mojang)
download.provider.mojang=官方伺服器(Forge 和 OptiFine 自動安裝的下載來源是 BMCLAPI)
extension.bat=Windows 腳本
extension.mod=模組檔案
@@ -123,9 +123,12 @@ input.not_empty=必填
input.url=必須為有效連結
install=新增遊戲
install.change_version=更換版本
install.change_version.confirm=你確定要將 %s 從 %s 更新到 %s 嗎?
install.failed=安裝失敗
install.failed.downloading=安裝失敗,部分文件未能完成下載
install.failed.downloading.detail=未能下載檔案:%s
install.failed.optifine_conflict=暫不支持 OptiFine 與 Forge 同時安裝於 Minecraft 1.13
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
@@ -256,6 +259,7 @@ mods.choose_mod=選擇模組
mods.enable=啟用
mods.disable=禁用
mods.remove=刪除
mods.not_modded=你需要先在自動安裝頁面安裝 Forge 或 LiteLoader 才能進行模組管理。
datapack=資料包
datapack.add=添加資料包
@@ -265,6 +269,7 @@ datapack.title=世界 %s - 資料包
datapack.remove=刪除
world=世界/資料包
world.add=添加世界
world.datapack=管理資料包
world.datapack.1_13=僅 Minecraft 1.13 及之後的版本支持資料包
world.description=%s. 上一次遊戲時間: %s. 遊戲版本: %s
@@ -281,6 +286,7 @@ world.game_version=遊戲版本
world.name=世界名稱
world.name.enter=輸入世界名稱
world.reveal=打開資料夾
world.show_all=顯示全部
world.time=yyyy 年 MM 月 dd 日 HH:mm:ss
profile=遊戲目錄
@@ -94,7 +94,7 @@ download.failed=下载失败
download.failed.empty=没有可供安装的版本,点击此处返回。
download.failed.refresh=加载版本列表失败,点击此处重试。
download.provider.bmclapi=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方(Mojang)
download.provider.mojang=官方(Forge 和 OptiFine 自动安装使用 BMCLAPI 下载源)
extension.bat=Windows 脚本
extension.mod=模组文件
@@ -123,9 +123,12 @@ input.not_empty=必填项
input.url=必须是合法的链接
install=添加游戏
install.change_version=更换版本
install.change_version.confirm=你确定要将 %s 从 %s 更新到 %s 吗?
install.failed=安装失败
install.failed.downloading=安装失败,部分文件未能完成下载
install.failed.downloading.detail=未能下载文件:%s
install.failed.optifine_conflict=暂不支持 OptiFine 与 Forge 同时安装于 Minecraft 1.13
install.installer.choose=选择 %s 版本
install.installer.forge=Forge
install.installer.game=游戏
@@ -256,6 +259,7 @@ mods.choose_mod=选择模组
mods.enable=启用
mods.disable=禁用
mods.remove=删除
mods.not_modded=你需要先在自动安装页面安装 Forge 或 LiteLoader 才能进行模组管理。
datapack=数据包
datapack.add=添加数据包
@@ -265,6 +269,7 @@ datapack.title=世界 %s - 数据包
datapack.remove=删除
world=世界/数据包
world.add=添加世界
world.datapack=管理数据包
world.datapack.1_13=仅 Minecraft 1.13 及之后的版本支持数据包
world.description=%s. 上一次游戏时间: %s. 游戏版本: %s
@@ -281,6 +286,7 @@ world.import.invalid=无法识别的存档压缩包
world.name=世界名称
world.name.enter=输入世界名称
world.reveal=打开文件夹
world.show_all=显示全部
world.time=yyyy 年 MM 月 dd 日 HH:mm:ss
profile=游戏目录
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.util.Immutable;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
@@ -36,10 +37,12 @@ public final class Texture {
this.metadata = metadata;
}
@Nullable
public String getUrl() {
return url;
}
@Nullable
public Map<String, String> getMetadata() {
return metadata;
}
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
import java.util.Map;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
@@ -36,13 +37,14 @@ public final class User implements Validation {
private final String id;
@Nullable
@JsonAdapter(PropertyMapSerializer.class)
private final Map<String, String> properties;
public User(String id) {
this(id, null);
}
public User(String id, Map<String, String> properties) {
public User(String id, @Nullable Map<String, String> properties) {
this.id = id;
this.properties = properties;
}
@@ -51,6 +53,7 @@ public final class User implements Validation {
return id;
}
@Nullable
public Map<String, String> getProperties() {
return properties;
}
@@ -28,7 +28,7 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.ParallelTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
/**
@@ -71,7 +71,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
@Override
public Task checkGameCompletionAsync(Version version) {
return new ParallelTask(
Task.ofThen(var -> {
Task.ofThen(() -> {
if (!repository.getVersionJar(version).exists())
return new GameDownloadTask(this, null, version);
else
@@ -88,36 +88,32 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
}
@Override
public Task installLibraryAsync(String gameVersion, Version version, String libraryId, String libraryVersion) {
public TaskResult<Version> installLibraryAsync(String gameVersion, Version version, String libraryId, String libraryVersion) {
VersionList<?> versionList = getVersionList(libraryId);
return versionList.loadAsync(gameVersion, getDownloadProvider())
.then(variables -> installLibraryAsync(version, versionList.getVersion(gameVersion, libraryVersion)
.thenCompose(() -> installLibraryAsync(version, versionList.getVersion(gameVersion, libraryVersion)
.orElseThrow(() -> new IllegalStateException("Remote library " + libraryId + " has no version " + libraryVersion))));
}
@Override
public Task installLibraryAsync(Version version, RemoteVersion libraryVersion) {
public TaskResult<Version> installLibraryAsync(Version oldVersion, RemoteVersion libraryVersion) {
TaskResult<Version> task;
if (libraryVersion instanceof ForgeRemoteVersion)
return new ForgeInstallTask(this, version, (ForgeRemoteVersion) libraryVersion)
.then(variables -> new LibrariesUniqueTask(variables.get("version")))
.then(variables -> new MaintainTask(variables.get("version")))
.then(variables -> new VersionJsonSaveTask(repository, variables.get("version")));
task = new ForgeInstallTask(this, oldVersion, (ForgeRemoteVersion) libraryVersion);
else if (libraryVersion instanceof LiteLoaderRemoteVersion)
return new LiteLoaderInstallTask(this, version, (LiteLoaderRemoteVersion) libraryVersion)
.then(variables -> new LibrariesUniqueTask(variables.get("version")))
.then(variables -> new MaintainTask(variables.get("version")))
.then(variables -> new VersionJsonSaveTask(repository, variables.get("version")));
task = new LiteLoaderInstallTask(this, oldVersion, (LiteLoaderRemoteVersion) libraryVersion);
else if (libraryVersion instanceof OptiFineRemoteVersion)
return new OptiFineInstallTask(this, version, (OptiFineRemoteVersion) libraryVersion)
.then(variables -> new LibrariesUniqueTask(variables.get("version")))
.then(variables -> new MaintainTask(variables.get("version")))
.then(variables -> new VersionJsonSaveTask(repository, variables.get("version")));
task = new OptiFineInstallTask(this, oldVersion, (OptiFineRemoteVersion) libraryVersion);
else
throw new IllegalArgumentException("Remote library " + libraryVersion + " is unrecognized.");
return task
.thenCompose(LibrariesUniqueTask::new)
.thenCompose(MaintainTask::new)
.thenCompose(newVersion -> new VersionJsonSaveTask(repository, newVersion));
}
public ExceptionalFunction<AutoTypingMap<String>, Task, ?> installLibraryAsync(RemoteVersion libraryVersion) {
return var -> installLibraryAsync(var.get("version"), libraryVersion);
public ExceptionalFunction<Version, TaskResult<Version>, ?> installLibraryAsync(RemoteVersion libraryVersion) {
return version -> installLibraryAsync(version, libraryVersion);
}
}
@@ -21,7 +21,7 @@ import org.jackhuang.hmcl.download.game.*;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.ParallelTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -49,34 +49,35 @@ public class DefaultGameBuilder extends GameBuilder {
@Override
public Task buildAsync() {
return new VersionJsonDownloadTask(gameVersion, dependencyManager).then(variables -> {
Version version = JsonUtils.GSON.fromJson(variables.<String>get(VersionJsonDownloadTask.ID), Version.class);
version = version.setId(name).setJar(null);
variables.set("version", version);
Task result = downloadGameAsync(gameVersion, version).then(new ParallelTask(
return new VersionJsonDownloadTask(gameVersion, dependencyManager).thenCompose(rawJson -> {
Version original = JsonUtils.GSON.fromJson(rawJson, Version.class);
Version version = original.setId(name).setJar(null);
Task vanillaTask = downloadGameAsync(gameVersion, version).then(new ParallelTask(
new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY),
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version))); // using [with] because download failure here are tolerant.
TaskResult<Version> libraryTask = vanillaTask.thenSupply(() -> version);
if (toolVersions.containsKey("forge"))
result = result.then(libraryTaskHelper(gameVersion, "forge"));
libraryTask = libraryTask.thenCompose(libraryTaskHelper(gameVersion, "forge"));
if (toolVersions.containsKey("liteloader"))
result = result.then(libraryTaskHelper(gameVersion, "liteloader"));
libraryTask = libraryTask.thenCompose(libraryTaskHelper(gameVersion, "liteloader"));
if (toolVersions.containsKey("optifine"))
result = result.then(libraryTaskHelper(gameVersion, "optifine"));
libraryTask = libraryTask.thenCompose(libraryTaskHelper(gameVersion, "optifine"));
for (RemoteVersion remoteVersion : remoteVersions)
result = result.then(dependencyManager.installLibraryAsync(remoteVersion));
libraryTask = libraryTask.thenCompose(dependencyManager.installLibraryAsync(remoteVersion));
return result;
}).finalized((variables, isDependentsSucceeded) -> {
if (!isDependentsSucceeded)
dependencyManager.getGameRepository().getVersionRoot(name).delete();
return libraryTask;
}).whenComplete((isDependentSucceeded, exception) -> {
if (!isDependentSucceeded)
dependencyManager.getGameRepository().removeVersionFromDisk(name);
});
}
private ExceptionalFunction<AutoTypingMap<String>, Task, ?> libraryTaskHelper(String gameVersion, String libraryId) {
return variables -> dependencyManager.installLibraryAsync(gameVersion, variables.get("version"), libraryId, toolVersions.get(libraryId));
private ExceptionalFunction<Version, TaskResult<Version>, ?> libraryTaskHelper(String gameVersion, String libraryId) {
return version -> dependencyManager.installLibraryAsync(gameVersion, version, libraryId, toolVersions.get(libraryId));
}
protected Task downloadGameAsync(String gameVersion, Version version) {
@@ -23,15 +23,9 @@ import org.jackhuang.hmcl.task.TaskResult;
public class MaintainTask extends TaskResult<Version> {
private final Version version;
private final String id;
public MaintainTask(Version version) {
this(version, ID);
}
public MaintainTask(Version version, String id) {
this.version = version;
this.id = id;
}
@Override
@@ -76,11 +70,4 @@ public class MaintainTask extends TaskResult<Version> {
return builder.build();
}
@Override
public String getId() {
return id;
}
public static final String ID = "version";
}
@@ -74,7 +74,7 @@ public abstract class VersionList<T extends RemoteVersion> {
}
public Task loadAsync(DownloadProvider downloadProvider) {
return Task.ofThen(variables -> {
return Task.ofThen(() -> {
lock.readLock().lock();
boolean loaded;
@@ -88,7 +88,7 @@ public abstract class VersionList<T extends RemoteVersion> {
}
public Task loadAsync(String gameVersion, DownloadProvider downloadProvider) {
return Task.ofThen(variables -> {
return Task.ofThen(() -> {
lock.readLock().lock();
boolean loaded;
@@ -60,7 +60,7 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
final GetTask task = new GetTask(NetworkUtils.toURL("https://bmclapi2.bangbang93.com/forge/minecraft/" + gameVersion));
return new Task() {
@Override
public Collection<? extends Task> getDependents() {
public Collection<Task> getDependents() {
return Collections.singleton(task);
}
@@ -78,7 +78,7 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
continue;
String jar = null;
for (ForgeVersion.File file : version.getFiles())
if ("installer".equals(file.getCategory())) {
if ("installer".equals(file.getCategory()) && "jar".equals(file.getFormat())) {
String classifier = gameVersion + "-" + version.getVersion()
+ (StringUtils.isNotBlank(version.getBranch()) ? "-" + version.getBranch() : "");
String fileName = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat();
@@ -104,7 +104,6 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
private final String branch;
private final String mcversion;
private final String version;
private final int build;
private final List<File> files;
/**
@@ -112,14 +111,13 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
*/
@SuppressWarnings("unused")
public ForgeVersion() {
this(null, null, null, 0, null);
this(null, null, null, null);
}
public ForgeVersion(String branch, String mcversion, String version, int build, List<File> files) {
public ForgeVersion(String branch, String mcversion, String version, List<File> files) {
this.branch = branch;
this.mcversion = mcversion;
this.version = version;
this.build = build;
this.files = files;
}
@@ -135,10 +133,6 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
return version;
}
public int getBuild() {
return build;
}
public List<File> getFiles() {
return files;
}
@@ -58,7 +58,8 @@ public final class ForgeInstallTask extends TaskResult<Version> {
public void preExecute() throws Exception {
installer = Files.createTempFile("forge-installer", ".jar");
dependent = new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer.toFile());
dependent = new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer.toFile())
.setCaching(true);
}
@Override
@@ -82,16 +83,6 @@ public final class ForgeInstallTask extends TaskResult<Version> {
return Collections.singleton(dependency);
}
@Override
public String getId() {
return "version";
}
@Override
public boolean isRelyingOnDependencies() {
return false;
}
@Override
public void execute() {
if (VersionNumber.VERSION_COMPARATOR.compare("1.13", remote.getGameVersion()) <= 0)
@@ -34,14 +34,16 @@ public class ForgeNewInstallProfile {
private final int spec;
private final String minecraft;
private final String json;
private final Artifact path;
private final List<Library> libraries;
private final List<Processor> processors;
private final Map<String, Datum> data;
public ForgeNewInstallProfile(int spec, String minecraft, String json, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
public ForgeNewInstallProfile(int spec, String minecraft, String json, Artifact path, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
this.spec = spec;
this.minecraft = minecraft;
this.json = json;
this.path = path;
this.libraries = libraries;
this.processors = processors;
this.data = data;
@@ -69,6 +71,14 @@ public class ForgeNewInstallProfile {
return json;
}
/**
* Maven artifact path for the main jar to install.
* @return artifact path of the main jar.
*/
public Artifact getPath() {
return path;
}
/**
* Libraries that processors depend on.
* @return the required dependencies.
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
@@ -66,6 +67,8 @@ public class ForgeNewInstallTask extends TaskResult<Version> {
this.gameRepository = dependencyManager.getGameRepository();
this.version = version;
this.installer = installer;
setSignificance(TaskSignificance.MINOR);
}
private <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> plainConverter) throws E {
@@ -89,11 +92,6 @@ public class ForgeNewInstallTask extends TaskResult<Version> {
return dependencies;
}
@Override
public String getId() {
return "version";
}
@Override
public boolean doPreExecute() {
return true;
@@ -112,6 +110,14 @@ public class ForgeNewInstallTask extends TaskResult<Version> {
FileUtils.copyFile(file, dest);
}
}
{
Path mainJar = profile.getPath().getPath(fs.getPath("maven"));
if (Files.exists(mainJar)) {
Path dest = gameRepository.getArtifactFile(version, profile.getPath());
FileUtils.copyFile(mainJar, dest);
}
}
}
dependents.add(new GameLibrariesTask(dependencyManager, version, profile.getLibraries()));
@@ -119,6 +125,9 @@ public class ForgeNewInstallTask extends TaskResult<Version> {
@Override
public void execute() throws Exception {
if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
throw new OptiFineInstallTask.UnsupportedOptiFineInstallationException();
Path temp = Files.createTempDirectory("forge_installer");
int finished = 0;
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
@@ -42,6 +42,8 @@ public class ForgeOldInstallTask extends TaskResult<Version> {
this.dependencyManager = dependencyManager;
this.version = version;
this.installer = installer;
setSignificance(TaskSignificance.MINOR);
}
@Override
@@ -49,11 +51,6 @@ public class ForgeOldInstallTask extends TaskResult<Version> {
return dependencies;
}
@Override
public String getId() {
return "version";
}
@Override
public boolean doPreExecute() {
return true;
@@ -89,6 +89,7 @@ public final class GameAssetDownloadTask extends Task {
String url = dependencyManager.getDownloadProvider().getAssetBaseURL() + assetObject.getLocation();
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
task.setCaching(true);
dependencies.add(task
.setCacheRepository(dependencyManager.getCacheRepository())
.setCaching(true)
@@ -33,15 +33,9 @@ import java.util.stream.Collectors;
public class LibrariesUniqueTask extends TaskResult<Version> {
private final Version version;
private final String id;
public LibrariesUniqueTask(Version version) {
this(version, "version");
}
public LibrariesUniqueTask(Version version, String id) {
this.version = version;
this.id = id;
}
@Override
@@ -99,9 +93,4 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
setResult(version.setLibraries(multimap.values().stream().sorted().collect(Collectors.toList())));
}
@Override
public String getId() {
return id;
}
}
@@ -128,12 +128,12 @@ public class LibraryDownloadTask extends Task {
try {
URL packXz = NetworkUtils.toURL(url + ".pack.xz");
if (NetworkUtils.urlExists(packXz)) {
task = new FileDownloadTask(packXz, xzFile, null);
task = new FileDownloadTask(packXz, xzFile, null).setCaching(true);
xz = true;
} else {
task = new FileDownloadTask(NetworkUtils.toURL(url),
jar,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null).setCaching(true);
xz = false;
}
} catch (IOException e) {
@@ -225,16 +225,16 @@ public class LibraryDownloadTask extends Task {
int x = decompressed.length;
int len = decompressed[(x - 8)] & 0xFF | (decompressed[(x - 7)] & 0xFF) << 8 | (decompressed[(x - 6)] & 0xFF) << 16 | (decompressed[(x - 5)] & 0xFF) << 24;
File temp = FileUtils.createTempFile("minecraft", ".pack");
Path temp = Files.createTempFile("minecraft", ".pack");
byte[] checksums = Arrays.copyOfRange(decompressed, decompressed.length - len - 8, decompressed.length - 8);
OutputStream out = new FileOutputStream(temp);
out.write(decompressed, 0, decompressed.length - len - 8);
out.close();
try (OutputStream out = Files.newOutputStream(temp)) {
out.write(decompressed, 0, decompressed.length - len - 8);
}
try (FileOutputStream jarBytes = new FileOutputStream(dest); JarOutputStream jos = new JarOutputStream(jarBytes)) {
Pack200.newUnpacker().unpack(temp, jos);
Pack200.newUnpacker().unpack(temp.toFile(), jos);
JarEntry checksumsFile = new JarEntry("checksums.sha1");
checksumsFile.setTime(0L);
@@ -243,6 +243,6 @@ public class LibraryDownloadTask extends Task {
jos.closeEntry();
}
temp.delete();
Files.delete(temp);
}
}
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.util.Collection;
@@ -32,7 +33,7 @@ import java.util.List;
*
* @author huangyuhui
*/
public final class VersionJsonDownloadTask extends Task {
public final class VersionJsonDownloadTask extends TaskResult<String> {
private final String gameVersion;
private final DefaultDependencyManager dependencyManager;
private final List<Task> dependents = new LinkedList<>();
@@ -65,8 +66,6 @@ public final class VersionJsonDownloadTask extends Task {
RemoteVersion remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
.orElseThrow(() -> new IllegalStateException("Cannot find specific version " + gameVersion + " in remote repository"));
String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl());
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), ID));
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL)).storeTo(this::setResult));
}
public static final String ID = "raw_version_json";
}
@@ -19,19 +19,18 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
/**
* This task is to save the version json.
*
* @author huangyuhui
*/
public final class VersionJsonSaveTask extends Task {
public final class VersionJsonSaveTask extends TaskResult<Version> {
private final DefaultGameRepository repository;
private final Version version;
@@ -47,13 +46,12 @@ public final class VersionJsonSaveTask extends Task {
this.version = version;
setSignificance(TaskSignificance.MODERATE);
setResult(version);
}
@Override
public void execute() throws Exception {
File json = repository.getVersionJson(version.getId()).getAbsoluteFile();
if (!FileUtils.makeFile(json))
throw new IOException("Cannot create file " + json);
FileUtils.writeText(json, JsonUtils.GSON.toJson(version));
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
@@ -61,11 +60,6 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
return dependencies;
}
@Override
public String getId() {
return "version";
}
@Override
public void execute() {
Library library = new Library(
@@ -59,11 +59,6 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
return dependencies;
}
@Override
public String getId() {
return "version";
}
@Override
public boolean isRelyingOnDependencies() {
return false;
@@ -71,6 +66,9 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
@Override
public void execute() {
if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
throw new UnsupportedOptiFineInstallationException();
String remoteVersion = remote.getGameVersion() + "_" + remote.getSelfVersion();
Library library = new Library(
@@ -95,4 +93,6 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
dependencies.add(dependencyManager.checkLibraryCompletionAsync(version.setLibraries(libraries)));
}
public static class UnsupportedOptiFineInstallationException extends UnsupportedOperationException {
}
}
@@ -168,8 +168,8 @@ public class DefaultGameRepository implements GameRepository {
versions.remove(id);
if (FileUtils.isMovingToTrashSupported()) {
return FileUtils.moveToTrash(removedFile);
if (FileUtils.isMovingToTrashSupported() && FileUtils.moveToTrash(removedFile)) {
return true;
}
// remove json files first to ensure HMCL will not recognize this folder as a valid version.
@@ -140,7 +140,7 @@ public final class CurseCompletionTask extends Task {
for (CurseManifestFile file : newManifest.getFiles())
if (StringUtils.isNotBlank(file.getFileName())) {
if (!modManager.hasSimpleMod(file.getFileName())) {
dependencies.add(new FileDownloadTask(file.getUrl(), modManager.getSimpleModPath(file.getFileName()).toFile()));
dependencies.add(new FileDownloadTask(file.getUrl(), modManager.getSimpleModPath(file.getFileName()).toFile()).setCaching(true));
}
}
@@ -24,7 +24,8 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -128,7 +129,7 @@ public class Datapack {
isMultiple = false;
try {
PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class);
info.add(new Pack(path, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this));
Platform.runLater(() -> info.add(new Pack(path, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this)));
} catch (IOException | JsonParseException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + path, e);
}
@@ -192,7 +193,7 @@ public class Datapack {
}
}
this.info.setAll(info);
Platform.runLater(() -> this.info.setAll(info));
}
public static class Pack {
@@ -23,7 +23,7 @@ import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
@@ -119,7 +119,7 @@ public final class ForgeModMetadata {
Path mcmod = fs.getPath("mcmod.info");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod.");
List<ForgeModMetadata> modList = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)),
List<ForgeModMetadata> modList = JsonUtils.GSON.fromJson(FileUtils.readText(mcmod),
new TypeToken<List<ForgeModMetadata>>() {
}.getType());
if (modList == null || modList.isEmpty())
@@ -20,7 +20,9 @@ package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ImmediateBooleanProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.io.File;
import java.io.IOException;
@@ -42,7 +44,7 @@ public final class ModInfo implements Comparable<ModInfo> {
private final String gameVersion;
private final String url;
private final String fileName;
private final ImmediateBooleanProperty activeProperty;
private final BooleanProperty activeProperty;
public ModInfo(ModManager modManager, File file, String name, String description) {
this(modManager, file, name, description, "", "", "", "");
@@ -57,7 +59,7 @@ public final class ModInfo implements Comparable<ModInfo> {
this.gameVersion = gameVersion;
this.url = url;
activeProperty = new ImmediateBooleanProperty(this, "active", !modManager.isDisabled(file)) {
activeProperty = new SimpleBooleanProperty(this, "active", !modManager.isDisabled(file)) {
@Override
protected void invalidated() {
Path path = ModInfo.this.file.toAbsolutePath();
@@ -104,7 +106,7 @@ public final class ModInfo implements Comparable<ModInfo> {
return url;
}
public ImmediateBooleanProperty activeProperty() {
public BooleanProperty activeProperty() {
return activeProperty;
}
@@ -85,13 +85,15 @@ public final class ModManager {
public void refreshMods() throws IOException {
modInfos.clear();
for (Path subitem : Files.newDirectoryStream(getModsDirectory())) {
if (Files.isDirectory(subitem) && VersionNumber.isIntVersionNumber(FileUtils.getName(subitem))) {
// If the folder name is game version, forge will search mod in this subdirectory
for (Path subsubitem : Files.newDirectoryStream(subitem))
addModInfo(subsubitem.toFile());
} else {
addModInfo(subitem.toFile());
if (Files.isDirectory(getModsDirectory())) {
for (Path subitem : Files.newDirectoryStream(getModsDirectory())) {
if (Files.isDirectory(subitem) && VersionNumber.isIntVersionNumber(FileUtils.getName(subitem))) {
// If the folder name is game version, forge will search mod in this subdirectory
for (Path subsubitem : Files.newDirectoryStream(subitem))
addModInfo(subsubitem.toFile());
} else {
addModInfo(subitem.toFile());
}
}
}
loaded = true;
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.task.SilentException;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -49,11 +48,6 @@ public class ModpackUpdateTask extends Task {
}
}
@Override
public boolean isRelyingOnDependencies() {
return false;
}
@Override
public Collection<? extends Task> getDependencies() {
return Collections.singleton(updateTask);
@@ -78,8 +72,6 @@ public class ModpackUpdateTask extends Task {
repository.removeVersionFromDisk(id);
FileUtils.copyDirectory(backupFolder, repository.getVersionRoot(id).toPath());
throw new SilentException();
}
}
}
@@ -156,7 +156,7 @@ public final class MultiMCModpackInstallTask extends Task {
}
}
dependencies.add(new MaintainTask(version).then(var -> new VersionJsonSaveTask(repository, var.get(MaintainTask.ID))));
dependencies.add(new MaintainTask(version).thenCompose(maintainedVersion -> new VersionJsonSaveTask(repository, maintainedVersion)));
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/" + manifest.getName() + "/minecraft", manifest, MODPACK_TYPE, repository.getModpackConfiguration(name)));
}
@@ -18,57 +18,53 @@
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* A task that combines two tasks and make sure [pred] runs before succ.
*
* @author huangyuhui
*/
final class CoupleTask<P extends Task> extends Task {
final class CoupleTask extends Task {
private final boolean relyingOnDependents;
private final Collection<Task> dependents;
private final List<Task> dependencies = new LinkedList<>();
private final ExceptionalFunction<AutoTypingMap<String>, Task, ?> succ;
private final Task pred;
private Task succ;
private final ExceptionalSupplier<Task, ?> supplier;
/**
* A task that combines two tasks and make sure pred runs before succ.
*
* @param pred the task that runs before succ.
* @param succ a callback that returns the task runs after pred, succ will be executed asynchronously. You can do something that relies on the result of pred.
* @param pred the task that runs before supplier.
* @param supplier a callback that returns the task runs after pred, succ will be executed asynchronously. You can do something that relies on the result of pred.
* @param relyingOnDependents true if this task chain will be broken when task pred fails.
*/
public CoupleTask(P pred, ExceptionalFunction<AutoTypingMap<String>, Task, ?> succ, boolean relyingOnDependents) {
this.dependents = pred == null ? Collections.emptySet() : Collections.singleton(pred);
this.succ = succ;
CoupleTask(Task pred, ExceptionalSupplier<Task, ?> supplier, boolean relyingOnDependents) {
this.pred = pred;
this.supplier = supplier;
this.relyingOnDependents = relyingOnDependents;
setSignificance(TaskSignificance.MODERATE);
setName(succ.toString());
setName(supplier.toString());
}
@Override
public void execute() throws Exception {
setName(succ.toString());
Task task = succ.apply(getVariables());
if (task != null)
dependencies.add(task);
setName(supplier.toString());
succ = supplier.get();
}
@Override
public Collection<Task> getDependents() {
return dependents;
return pred == null ? Collections.emptySet() : Collections.singleton(pred);
}
@Override
public Collection<Task> getDependencies() {
return dependencies;
return succ == null ? Collections.emptySet() : Collections.singleton(succ);
}
@Override
@@ -33,6 +33,7 @@ import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Optional;
@@ -212,7 +213,7 @@ public class FileDownloadTask extends Task {
break;
}
File temp = null;
Path temp = null;
try {
updateProgress(0);
@@ -242,15 +243,15 @@ public class FileDownloadTask extends Task {
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
throw new IOException("Could not make directory " + file.getAbsoluteFile().getParent());
temp = FileUtils.createTempFile();
rFile = new RandomAccessFile(temp, "rw");
temp = Files.createTempFile(null, null);
rFile = new RandomAccessFile(temp.toFile(), "rw");
MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
stream = con.getInputStream();
int lastDownloaded = 0, downloaded = 0;
long lastTime = System.currentTimeMillis();
byte buffer[] = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
while (true) {
if (Thread.interrupted()) {
Thread.currentThread().interrupt();
@@ -283,16 +284,15 @@ public class FileDownloadTask extends Task {
// Restore temp file to original name.
if (Thread.interrupted()) {
temp.delete();
temp.toFile().delete();
Thread.currentThread().interrupt();
break;
} else {
if (file.exists() && !file.delete())
throw new IOException("Unable to delete existent file " + file);
Files.deleteIfExists(file.toPath());
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
throw new IOException("Unable to make parent directory " + file);
try {
FileUtils.moveFile(temp, file);
FileUtils.moveFile(temp.toFile(), file);
} catch (Exception e) {
throw new IOException("Unable to move temp file from " + temp + " to " + file, e);
}
@@ -321,7 +321,7 @@ public class FileDownloadTask extends Task {
return;
} catch (IOException | IllegalStateException e) {
if (temp != null)
temp.delete();
temp.toFile().delete();
exception = e;
} finally {
closeFiles();
@@ -1,24 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.AutoTypingMap;
public interface FinalizedCallback {
void execute(AutoTypingMap<String> variables, boolean isDependentsSucceeded) throws Exception;
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.task;
import java.util.Collection;
import java.util.Collections;
/**
* A task that combines two tasks and make sure [pred] runs before succ.
*
* @author huangyuhui
*/
final class FinalizedTask extends Task {
private final Collection<Task> dependents;
private final FinalizedCallback callback;
private final Scheduler scheduler;
/**
* A task that combines two tasks and make sure pred runs before succ.
*
* @param pred the task that runs before succ.
* @param callback a callback that returns the task runs after pred, succ will be executed asynchronously. You can do something that relies on the result of pred.
*/
public FinalizedTask(Task pred, Scheduler scheduler, FinalizedCallback callback, String name) {
this.dependents = Collections.singleton(pred);
this.scheduler = scheduler;
this.callback = callback;
setSignificance(TaskSignificance.MODERATE);
setName(name);
}
@Override
public Scheduler getScheduler() {
return scheduler;
}
@Override
public void execute() throws Exception {
callback.execute(getVariables(), isDependentsSucceeded());
if (!isDependentsSucceeded())
throw new SilentException();
}
@Override
public Collection<Task> getDependents() {
return dependents;
}
@Override
public boolean isRelyingOnDependents() {
return false;
}
}
@@ -72,11 +72,6 @@ public final class GetTask extends TaskResult<String> {
return Schedulers.io();
}
@Override
public String getId() {
return id;
}
public GetTask setCacheRepository(CacheRepository repository) {
this.repository = repository;
return this;
@@ -1,28 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.task;
/**
* If a task throws {@link SilentException},
* the task will be marked as failure but do not log the stacktrace.
*
* @author huangyuhui
*/
public final class SilentException extends Exception {
}
@@ -17,8 +17,7 @@
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
/**
*
@@ -26,16 +25,16 @@ import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
*/
class SimpleTask extends Task {
private final ExceptionalConsumer<AutoTypingMap<String>, ?> consumer;
private final ExceptionalRunnable<?> closure;
private final Scheduler scheduler;
public SimpleTask(String name, ExceptionalConsumer<AutoTypingMap<String>, ?> consumer, Scheduler scheduler) {
this.consumer = consumer;
public SimpleTask(String name, ExceptionalRunnable<?> closure, Scheduler scheduler) {
this.closure = closure;
this.scheduler = scheduler;
if (name == null) {
setSignificance(TaskSignificance.MINOR);
setName(consumer.toString());
setName(closure.toString());
} else {
setName(name);
}
@@ -48,6 +47,6 @@ class SimpleTask extends Task {
@Override
public void execute() throws Exception {
consumer.accept(getVariables());
closure.run();
}
}
@@ -17,24 +17,30 @@
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
public final class SimpleTaskResult<V> extends TaskResult<V> {
private final String id;
private final ExceptionalSupplier<V, ?> supplier;
import java.util.concurrent.Callable;
public SimpleTaskResult(String id, ExceptionalSupplier<V, ?> supplier) {
this.id = id;
this.supplier = supplier;
/**
*
* @author huangyuhui
*/
class SimpleTaskResult<V> extends TaskResult<V> {
private final Callable<V> callable;
public SimpleTaskResult(Callable<V> callable) {
this.callable = callable;
}
public SimpleTaskResult(ExceptionalSupplier<V, ?> supplier) {
this.callable = supplier.toCallable();
}
@Override
public void execute() throws Exception {
setResult(supplier.get());
}
@Override
public String getId() {
return id;
setResult(callable.call());
}
}
@@ -23,17 +23,17 @@ import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ReflectionHelper;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
/**
@@ -68,13 +68,13 @@ public abstract class Task {
this.state = state;
}
private Throwable lastException = null;
private Exception lastException;
public Throwable getLastException() {
public Exception getLastException() {
return lastException;
}
void setLastException(Throwable e) {
void setLastException(Exception e) {
lastException = e;
}
@@ -134,16 +134,6 @@ public abstract class Task {
return this;
}
private AutoTypingMap<String> variables = null;
public AutoTypingMap<String> getVariables() {
return variables;
}
void setVariables(AutoTypingMap<String> variables) {
this.variables = variables;
}
public boolean doPreExecute() {
return false;
}
@@ -165,8 +155,15 @@ public abstract class Task {
}
/**
* This method will be called after dependency tasks terminated all together.
*
* You can check whether dependencies succeed in this method by calling
* {@link Task#isDependenciesSucceeded()} no matter when
* {@link Task#isRelyingOnDependencies()} returns true or false.
*
* @throws InterruptedException if current thread is interrupted
* @see Thread#isInterrupted
* @see Task#isDependenciesSucceeded()
*/
public void postExecute() throws Exception {}
@@ -276,121 +273,198 @@ public abstract class Task {
return executor().test();
}
public final void subscribe(Task subscriber) {
new TaskExecutor(then(subscriber)).start();
}
public final void subscribe(Scheduler scheduler, ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
subscribe(of(scheduler, closure));
}
public final void subscribe(Scheduler scheduler, ExceptionalRunnable<?> closure) {
subscribe(of(scheduler, ExceptionalConsumer.fromRunnable(closure)));
}
public final void subscribe(ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
subscribe(of(closure));
}
public final void subscribe(ExceptionalRunnable<?> closure) {
subscribe(of(closure));
}
public final Task then(Task b) {
return then(convert(b));
}
public final Task then(ExceptionalFunction<AutoTypingMap<String>, Task, ?> b) {
return new CoupleTask<>(this, b, true);
public final Task then(ExceptionalSupplier<Task, ?> b) {
return new CoupleTask(this, b, true);
}
/**
* Returns a new TaskResult that, when this task completes
* normally, is executed using the default Scheduler.
*
* @param fn the function to use to compute the value of the returned TaskResult
* @param <U> the function's return type
* @return the new TaskResult
*/
public final <U> TaskResult<U> thenSupply(Callable<U> fn) {
return thenCompose(() -> Task.ofResult(fn));
}
/**
* Returns a new TaskResult that, when this task completes
* normally, is executed.
*
* @param fn the function returning a new TaskResult
* @param <U> the type of the returned TaskResult's result
* @return the TaskResult
*/
public final <U> TaskResult<U> thenCompose(ExceptionalSupplier<TaskResult<U>, ?> fn) {
return new TaskResult<U>() {
TaskResult<U> then;
@Override
public Collection<? extends Task> getDependents() {
return Collections.singleton(Task.this);
}
@Override
public void execute() throws Exception {
then = fn.get().storeTo(this::setResult);
}
@Override
public Collection<? extends Task> getDependencies() {
return then == null ? Collections.emptyList() : Collections.singleton(then);
}
};
}
public final Task with(Task b) {
return with(convert(b));
}
public final <E extends Exception> Task with(ExceptionalFunction<AutoTypingMap<String>, Task, E> b) {
return new CoupleTask<>(this, b, false);
public final <E extends Exception> Task with(ExceptionalSupplier<Task, E> b) {
return new CoupleTask(this, b, false);
}
public final Task finalized(FinalizedCallback b) {
return finalized(Schedulers.defaultScheduler(), b);
/**
* Returns a new Task with the same exception as this task, that executes
* the given action when this task completes.
*
* <p>When this task is complete, the given action is invoked, a boolean
* value represents the execution status of this task, and the exception
* (or {@code null} if none) of this task as arguments. The returned task
* is completed when the action returns. If the supplied action itself
* encounters an exception, then the returned task exceptionally completes
* with this exception unless this task also completed exceptionally.
*
* @param action the action to perform
* @return the new Task
*/
public final Task whenComplete(FinalizedCallback action) {
return whenComplete(Schedulers.defaultScheduler(), action);
}
public final Task finalized(Scheduler scheduler, FinalizedCallback b) {
return new FinalizedTask(this, scheduler, b, ReflectionHelper.getCaller().toString());
/**
* Returns a new Task with the same exception as this task, that executes
* the given action when this task completes.
*
* <p>When this task is complete, the given action is invoked, a boolean
* value represents the execution status of this task, and the exception
* (or {@code null} if none, which means when isDependentSucceeded is false,
* exception may be null) of this task as arguments. The returned task
* is completed when the action returns. If the supplied action itself
* encounters an exception, then the returned task exceptionally completes
* with this exception unless this task also completed exceptionally.
*
* @param action the action to perform
* @param scheduler the executor to use for asynchronous execution
* @return the new Task
*/
public final Task whenComplete(Scheduler scheduler, FinalizedCallback action) {
return new Task() {
{
setSignificance(TaskSignificance.MODERATE);
}
@Override
public Scheduler getScheduler() {
return scheduler;
}
@Override
public void execute() throws Exception {
action.execute(isDependentsSucceeded(), Task.this.getLastException());
if (!isDependentsSucceeded()) {
setSignificance(TaskSignificance.MINOR);
if (Task.this.getLastException() == null)
throw new CancellationException();
else
throw Task.this.getLastException();
}
}
@Override
public Collection<Task> getDependents() {
return Collections.singleton(Task.this);
}
@Override
public boolean isRelyingOnDependents() {
return false;
}
}.setName(getCaller());
}
public final <T extends Exception, K extends Exception> Task finalized(Scheduler scheduler, ExceptionalConsumer<AutoTypingMap<String>, T> success, ExceptionalConsumer<Exception, K> failure) {
return finalized(scheduler, (variables, isDependentsSucceeded) -> {
if (isDependentsSucceeded) {
/**
* Returns a new Task with the same exception as this task, that executes
* the given actions when this task completes.
*
* <p>When this task is complete, the given success action is invoked, the
* given failure action is invoked with the exception of this task. The
* returned task is completed when the action returns. If the supplied
* action itself encounters an exception, then the returned task exceptionally
* completes with this exception unless this task also
* completed exceptionally.
*
* @param success the action to perform when this task successfully completed
* @param failure the action to perform when this task exceptionally returned
* @return the new Task
*/
public final <E1 extends Exception, E2 extends Exception> Task whenComplete(Scheduler scheduler, ExceptionalRunnable<E1> success, ExceptionalConsumer<Exception, E2> failure) {
return whenComplete(scheduler, (isDependentSucceeded, exception) -> {
if (isDependentSucceeded) {
if (success != null)
try {
success.accept(variables);
success.run();
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to execute " + success, e);
if (failure != null)
failure.accept(e);
}
}
else {
} else {
if (failure != null)
failure.accept(variables.get(TaskExecutor.LAST_EXCEPTION_ID));
failure.accept(exception);
}
});
}
public static Task of(String name, ExceptionalRunnable<?> runnable) {
return of(name, ExceptionalConsumer.fromRunnable(runnable));
}
public static Task of(ExceptionalRunnable<?> runnable) {
return of(ExceptionalConsumer.fromRunnable(runnable));
}
public static Task of(String name, ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
return of(name, Schedulers.defaultScheduler(), closure);
}
public static Task of(ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
public static Task of(ExceptionalRunnable<?> closure) {
return of(Schedulers.defaultScheduler(), closure);
}
public static Task of(String name, Scheduler scheduler, ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
return new SimpleTask(name, closure, scheduler);
}
public static Task of(Scheduler scheduler, ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
return of(ReflectionHelper.getCaller().toString(), scheduler, closure);
}
public static Task of(String name, Scheduler scheduler, ExceptionalRunnable<?> closure) {
return new SimpleTask(name, ExceptionalConsumer.fromRunnable(closure), scheduler);
public static Task of(String name, ExceptionalRunnable<?> closure) {
return of(name, Schedulers.defaultScheduler(), closure);
}
public static Task of(Scheduler scheduler, ExceptionalRunnable<?> closure) {
return of(ReflectionHelper.getCaller().toString(), scheduler, closure);
return of(getCaller(), scheduler, closure);
}
public static Task ofThen(ExceptionalFunction<AutoTypingMap<String>, Task, ?> b) {
return new CoupleTask<>(null, b, true);
public static Task of(String name, Scheduler scheduler, ExceptionalRunnable<?> closure) {
return new SimpleTask(name, closure, scheduler);
}
public static Task ofThen(ExceptionalSupplier<Task, ?> b) {
return new CoupleTask(null, b, true);
}
public static <V> TaskResult<V> ofResult(Callable<V> callable) {
return ofResult("", callable);
return ofResult(getCaller(), callable);
}
public static <V> TaskResult<V> ofResult(String id, Callable<V> callable) {
return new TaskCallable<>(id, callable);
public static <V> TaskResult<V> ofResult(String name, Callable<V> callable) {
return new SimpleTaskResult<>(callable).setName(name);
}
public static <V> TaskResult<V> ofResult(String id, ExceptionalFunction<AutoTypingMap<String>, V, ?> closure) {
return new TaskCallable<>(id, closure);
}
private static ExceptionalFunction<AutoTypingMap<String>, Task, ?> convert(Task t) {
return new ExceptionalFunction<AutoTypingMap<String>, Task, Exception>() {
private static ExceptionalSupplier<Task, ?> convert(Task t) {
return new ExceptionalSupplier<Task, Exception>() {
@Override
public Task apply(AutoTypingMap<String> autoTypingMap) {
public Task get() {
return t;
}
@@ -422,4 +496,12 @@ public abstract class Task {
SUCCEEDED,
FAILED
}
public interface FinalizedCallback {
void execute(boolean isDependentSucceeded, Exception exception) throws Exception;
}
static String getCaller() {
return ReflectionHelper.getCaller(packageName -> !"org.jackhuang.hmcl.task".equals(packageName)).toString();
}
}
@@ -1,52 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import java.util.concurrent.Callable;
/**
*
* @author huangyuhui
*/
class TaskCallable<V> extends TaskResult<V> {
private final String id;
private final ExceptionalFunction<AutoTypingMap<String>, V, ?> callable;
public TaskCallable(String id, Callable<V> callable) {
this(id, variables -> callable.call());
}
public TaskCallable(String id, ExceptionalFunction<AutoTypingMap<String>, V, ?> callable) {
this.id = id;
this.callable = callable;
}
@Override
public String getId() {
return id;
}
@Override
public void execute() throws Exception {
setResult(callable.apply(getVariables()));
}
}
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more