重构 FutureCallback (#5268)

This commit is contained in:
Glavo
2026-01-23 22:19:43 +08:00
committed by GitHub
parent 65a4b6159d
commit 165d77df30
11 changed files with 81 additions and 64 deletions

View File

@@ -325,13 +325,13 @@ public final class LauncherHelper {
message = i18n("launch.failed.command_too_long"); message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) { } else if (ex instanceof ExecutionPolicyLimitException) {
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"), Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
(result, resolve, reject) -> { (result, handler) -> {
if (CommandBuilder.setExecutionPolicy()) { if (CommandBuilder.setExecutionPolicy()) {
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'"); LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
resolve.run(); handler.resolve();
} else { } else {
LOG.warning("Failed to set ExecutionPolicy"); LOG.warning("Failed to set ExecutionPolicy");
reject.accept(i18n("launch.failed.execution_policy.failed_to_set")); handler.reject(i18n("launch.failed.execution_policy.failed_to_set"));
} }
}) })
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint"))) .addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))

View File

@@ -72,13 +72,19 @@ public class InputDialogPane extends JFXDialogLayout implements DialogAware {
acceptButton.setOnAction(e -> { acceptButton.setOnAction(e -> {
acceptPane.showSpinner(); acceptPane.showSpinner();
onResult.call(textField.getText(), () -> { onResult.call(textField.getText(), new FutureCallback.ResultHandler() {
@Override
public void resolve() {
acceptPane.hideSpinner(); acceptPane.hideSpinner();
future.complete(textField.getText()); future.complete(textField.getText());
fireEvent(new DialogCloseEvent()); fireEvent(new DialogCloseEvent());
}, msg -> { }
@Override
public void reject(String reason) {
acceptPane.hideSpinner(); acceptPane.hideSpinner();
lblCreationWarning.setText(msg); lblCreationWarning.setText(reason);
}
}); });
}); });
textField.setOnAction(event -> acceptButton.fire()); textField.setOnAction(event -> acceptButton.fire());

View File

@@ -117,11 +117,17 @@ public class PromptDialogPane extends DialogPane {
protected void onAccept() { protected void onAccept() {
setLoading(); setLoading();
builder.callback.call(builder.questions, () -> { builder.callback.call(builder.questions, new FutureCallback.ResultHandler() {
@Override
public void resolve() {
future.complete(builder.questions); future.complete(builder.questions);
runInFX(this::onSuccess); runInFX(() -> onSuccess());
}, msg -> { }
runInFX(() -> onFailure(msg));
@Override
public void reject(String reason) {
runInFX(() -> onFailure(reason));
}
}); });
} }

View File

@@ -134,7 +134,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory(); Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> { Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
Path dest = runDirectory.resolve(subdirectoryName).resolve(result); Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
Controllers.taskDialog(Task.composeAsync(() -> { Controllers.taskDialog(Task.composeAsync(() -> {
@@ -152,7 +152,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Controllers.showToast(i18n("install.success")); Controllers.showToast(i18n("install.success"));
} }
}), i18n("message.downloading"), TaskCancellationAction.NORMAL); }), i18n("message.downloading"), TaskCancellationAction.NORMAL);
resolve.run(); handler.resolve();
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); }, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
} }

View File

@@ -124,37 +124,37 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} }
private void onChooseRemoteFile() { private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> { Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, handler) -> {
try { try {
if (url.endsWith("server-manifest.json")) { if (url.endsWith("server-manifest.json")) {
// if urlString ends with .json, we assume that the url is server-manifest.json // if urlString ends with .json, we assume that the url is server-manifest.json
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> { Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class); ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
if (manifest == null) { if (manifest == null) {
reject.accept(i18n("modpack.type.server.malformed")); handler.reject(i18n("modpack.type.server.malformed"));
} else if (e == null) { } else if (e == null) {
resolve.run(); handler.resolve();
controller.getSettings().put(MODPACK_SERVER_MANIFEST, manifest); controller.getSettings().put(MODPACK_SERVER_MANIFEST, manifest);
controller.onNext(); controller.onNext();
} else { } else {
reject.accept(e.getMessage()); handler.reject(e.getMessage());
} }
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL); }).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
} else { } else {
// otherwise we still consider the file as modpack zip file // otherwise we still consider the file as modpack zip file
// since casually the url may not ends with ".zip" // since casually the url may not ends with ".zip"
Path modpack = Files.createTempFile("modpack", ".zip"); Path modpack = Files.createTempFile("modpack", ".zip");
resolve.run(); handler.resolve();
Controllers.taskDialog( Controllers.taskDialog(
new FileDownloadTask(url, modpack) new FileDownloadTask(url, modpack)
.whenComplete(Schedulers.javafx(), e -> { .whenComplete(Schedulers.javafx(), e -> {
if (e == null) { if (e == null) {
resolve.run(); handler.resolve();
controller.getSettings().put(MODPACK_FILE, modpack); controller.getSettings().put(MODPACK_FILE, modpack);
controller.onNext(); controller.onNext();
} else { } else {
reject.accept(e.getMessage()); handler.reject(e.getMessage());
} }
}).executor(true), }).executor(true),
i18n("message.downloading"), i18n("message.downloading"),
@@ -162,7 +162,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
); );
} }
} catch (IOException e) { } catch (IOException e) {
reject.accept(e.getMessage()); handler.reject(e.getMessage());
} }
}, "", new URLValidator()); }, "", new URLValidator());
} }

View File

@@ -245,19 +245,19 @@ public class TerracottaControllerPage extends StackPane {
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc")); guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
guest.setRightIcon(SVG.ARROW_FORWARD); guest.setRightIcon(SVG.ARROW_FORWARD);
FXUtils.onClicked(guest, () -> { FXUtils.onClicked(guest, () -> {
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, resolve, reject) -> { Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, handler) -> {
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code); Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
if (task != null) { if (task != null) {
task.whenComplete(Schedulers.javafx(), (s, e) -> { task.whenComplete(Schedulers.javafx(), (s, e) -> {
if (e != null) { if (e != null) {
reject.accept(i18n("terracotta.status.waiting.guest.prompt.invalid")); handler.reject(i18n("terracotta.status.waiting.guest.prompt.invalid"));
} else { } else {
resolve.run(); handler.resolve();
UI_STATE.set(s); UI_STATE.set(s);
} }
}).setSignificance(Task.TaskSignificance.MINOR).start(); }).setSignificance(Task.TaskSignificance.MINOR).start();
} else { } else {
resolve.run(); handler.resolve();
} }
}); });
}); });

View File

@@ -163,30 +163,30 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
Controllers.dialog(new InputDialogPane( Controllers.dialog(new InputDialogPane(
i18n("schematics.create_directory.prompt"), i18n("schematics.create_directory.prompt"),
"", "",
(result, resolve, reject) -> { (result, handler) -> {
if (StringUtils.isBlank(result)) { if (StringUtils.isBlank(result)) {
reject.accept(i18n("schematics.create_directory.failed.empty_name")); handler.reject(i18n("schematics.create_directory.failed.empty_name"));
return; return;
} }
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) { if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
reject.accept(i18n("schematics.create_directory.failed.invalid_name")); handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
return; return;
} }
Path targetDir = parent.resolve(result); Path targetDir = parent.resolve(result);
if (Files.exists(targetDir)) { if (Files.exists(targetDir)) {
reject.accept(i18n("schematics.create_directory.failed.already_exists")); handler.reject(i18n("schematics.create_directory.failed.already_exists"));
return; return;
} }
try { try {
Files.createDirectories(targetDir); Files.createDirectories(targetDir);
resolve.run(); handler.resolve();
refresh(); refresh();
} catch (IOException e) { } catch (IOException e) {
LOG.warning("Failed to create directory: " + targetDir, e); LOG.warning("Failed to create directory: " + targetDir, e);
reject.accept(i18n("schematics.create_directory.failed", targetDir)); handler.reject(i18n("schematics.create_directory.failed", targetDir));
} }
})); }));
} }

View File

@@ -129,13 +129,13 @@ public final class Versions {
} }
public static CompletableFuture<String> renameVersion(Profile profile, String version) { public static CompletableFuture<String> renameVersion(Profile profile, String version) {
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> { return Controllers.prompt(i18n("version.manage.rename.message"), (newName, handler) -> {
if (newName.equals(version)) { if (newName.equals(version)) {
resolve.run(); handler.resolve();
return; return;
} }
if (profile.getRepository().renameVersion(version, newName)) { if (profile.getRepository().renameVersion(version, newName)) {
resolve.run(); handler.resolve();
profile.getRepository().refreshVersionsAsync() profile.getRepository().refreshVersionsAsync()
.thenRunAsync(Schedulers.javafx(), () -> { .thenRunAsync(Schedulers.javafx(), () -> {
if (profile.getRepository().hasVersion(newName)) { if (profile.getRepository().hasVersion(newName)) {
@@ -143,7 +143,7 @@ public final class Versions {
} }
}).start(); }).start();
} else { } else {
reject.accept(i18n("version.manage.rename.fail")); handler.reject(i18n("version.manage.rename.fail"));
} }
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), }, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version))); new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
@@ -159,16 +159,16 @@ public final class Versions {
public static void duplicateVersion(Profile profile, String version) { public static void duplicateVersion(Profile profile, String version) {
Controllers.prompt( Controllers.prompt(
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> { new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue(); String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue(); boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves)) Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync()) .thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.whenComplete(Schedulers.javafx(), (result, exception) -> { .whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) { if (exception == null) {
resolve.run(); handler.resolve();
} else { } else {
reject.accept(StringUtils.getStackTrace(exception)); handler.reject(StringUtils.getStackTrace(exception));
if (!profile.getRepository().versionIdConflicts(newVersionName)) { if (!profile.getRepository().versionIdConflicts(newVersionName)) {
profile.getRepository().removeVersionFromDisk(newVersionName); profile.getRepository().removeVersionFromDisk(newVersionName);
} }

View File

@@ -156,18 +156,18 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
// Or too many input dialogs are popped. // Or too many input dialogs are popped.
Task.supplyAsync(() -> new World(zipFile)) Task.supplyAsync(() -> new World(zipFile))
.whenComplete(Schedulers.javafx(), world -> { .whenComplete(Schedulers.javafx(), world -> {
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> { Controllers.prompt(i18n("world.name.enter"), (name, handler) -> {
Task.runAsync(() -> world.install(savesDir, name)) Task.runAsync(() -> world.install(savesDir, name))
.whenComplete(Schedulers.javafx(), () -> { .whenComplete(Schedulers.javafx(), () -> {
resolve.run(); handler.resolve();
refresh(); refresh();
}, e -> { }, e -> {
if (e instanceof FileAlreadyExistsException) if (e instanceof FileAlreadyExistsException)
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists"))); handler.reject(i18n("world.import.failed", i18n("world.import.already_exists")));
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException) else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed"))); handler.reject(i18n("world.import.failed", i18n("install.new_game.malformed")));
else else
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage())); handler.reject(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
}).start(); }).start();
}, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); }, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
}, e -> { }, e -> {

View File

@@ -92,20 +92,20 @@ public final class WorldManageUIUtils {
Controllers.dialog(new InputDialogPane( Controllers.dialog(new InputDialogPane(
i18n("world.duplicate.prompt"), i18n("world.duplicate.prompt"),
"", "",
(result, resolve, reject) -> { (result, handler) -> {
if (StringUtils.isBlank(result)) { if (StringUtils.isBlank(result)) {
reject.accept(i18n("world.duplicate.failed.empty_name")); handler.reject(i18n("world.duplicate.failed.empty_name"));
return; return;
} }
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) { if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
reject.accept(i18n("world.duplicate.failed.invalid_name")); handler.reject(i18n("world.duplicate.failed.invalid_name"));
return; return;
} }
Path targetDir = worldPath.resolveSibling(result); Path targetDir = worldPath.resolveSibling(result);
if (Files.exists(targetDir)) { if (Files.exists(targetDir)) {
reject.accept(i18n("world.duplicate.failed.already_exists")); handler.reject(i18n("world.duplicate.failed.already_exists"));
return; return;
} }
@@ -118,9 +118,9 @@ public final class WorldManageUIUtils {
} }
).whenComplete(Schedulers.javafx(), (throwable) -> { ).whenComplete(Schedulers.javafx(), (throwable) -> {
if (throwable == null) { if (throwable == null) {
resolve.run(); handler.resolve();
} else { } else {
reject.accept(i18n("world.duplicate.failed")); handler.reject(i18n("world.duplicate.failed"));
LOG.warning("Failed to duplicate world " + world.getFile(), throwable); LOG.warning("Failed to duplicate world " + world.getFile(), throwable);
} }
}) })

View File

@@ -17,18 +17,23 @@
*/ */
package org.jackhuang.hmcl.util; package org.jackhuang.hmcl.util;
import java.util.function.Consumer;
@FunctionalInterface @FunctionalInterface
public interface FutureCallback<T> { public interface FutureCallback<T> {
/** /// Callback of future, called after future finishes.
* Callback of future, called after future finishes. /// This callback gives the feedback whether the result of future is acceptable or not,
* This callback gives the feedback whether the result of future is acceptable or not, /// if not, giving the reason, and future will be relaunched when necessary.
* if not, giving the reason, and future will be relaunched when necessary. ///
* @param result result of the future /// @param result result of the future
* @param resolve accept the result /// @param handler handler to accept or reject the result
* @param reject reject the result with failure reason void call(T result, ResultHandler handler);
*/
void call(T result, Runnable resolve, Consumer<String> reject); interface ResultHandler {
/// Accept the result.
void resolve();
/// Reject the result with given reason.
void reject(String reason);
}
} }