Update Readme.md for HMCLCore introduction

This commit is contained in:
huangyuhui
2018-01-16 19:34:53 +08:00
parent 4b39c046a7
commit b90d6b7b8c
90 changed files with 4192 additions and 4615 deletions

View File

@@ -0,0 +1,98 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.util.ResourceBundle;
import java.util.logging.Level;
public final class Main extends Application {
@Override
public void start(Stage primaryStage) {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
primaryStage.show();
}
public static void main(String[] args) {
NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
launch(args);
}
public static void stopApplication() {
JFXUtilities.runInFX(() -> {
stopWithoutPlatform();
Platform.exit();
});
}
public static void stopWithoutPlatform() {
JFXUtilities.runInFX(() -> {
Controllers.getStage().close();
Schedulers.shutdown();
});
}
public static String i18n(String key) {
try {
return RESOURCE_BUNDLE.getString(key);
} catch (Exception e) {
Logging.LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e);
return key;
}
}
public static File getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return new File(home, "." + folder + "/");
case WINDOWS:
String appdata = System.getenv("APPDATA");
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
case OSX:
return new File(home, "Library/Application Support/" + folder);
default:
return new File(home, folder + "/");
}
}
public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft");
public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final File APPDATA = getWorkingDirectory("hmcl");
public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
}

View File

@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
@@ -80,9 +80,9 @@ public final class AccountHelper {
}
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
if (account.getSelectedProfile() == null) return FXUtilsKt.DEFAULT_ICON;
if (account.getSelectedProfile() == null) return FXUtils.DEFAULT_ICON;
String name = account.getSelectedProfile().getName();
if (name == null) return FXUtilsKt.DEFAULT_ICON;
if (name == null) return FXUtils.DEFAULT_ICON;
File file = getSkinFile(name);
if (file.exists()) {
Image original = new Image("file:" + file.getAbsolutePath());
@@ -91,7 +91,7 @@ public final class AccountHelper {
original.getHeight() * scaleRatio,
false, false);
}
return FXUtilsKt.DEFAULT_ICON;
return FXUtils.DEFAULT_ICON;
}
public static Rectangle2D getViewport(double scaleRatio) {
@@ -128,7 +128,7 @@ public final class AccountHelper {
@Override
public void execute() throws Exception {
if (account.canLogIn() && (account.getSelectedProfile() == null || refresh))
DialogController.INSTANCE.logIn(account);
DialogController.logIn(account);
GameProfile profile = account.getSelectedProfile();
if (profile == null) return;

View File

@@ -26,32 +26,28 @@ import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.Constants;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public final class HMCLModpackInstallTask extends Task {
private final File zipFile;
private final String version;
private final String id;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
private final List<Task> dependencies = new LinkedList<>();
private final List<Task> dependents = new LinkedList<>();
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String id) throws IOException {
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String id) {
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.version = id;
this.id = id;
if (repository.hasVersion(id))
throw new IllegalArgumentException("Version " + id + " already exists");
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = Constants.GSON.fromJson(json, Version.class).setJar(null);
dependents.add(dependency.gameBuilder().name(id).gameVersion(modpack.getGameVersion()).buildAsync());
dependencies.add(new VersionJsonSaveTask(repository, version));
onDone().register(event -> {
if (event.isFailed()) repository.removeVersionFromDisk(id);
@@ -70,7 +66,11 @@ public final class HMCLModpackInstallTask extends Task {
@Override
public void execute() throws Exception {
CompressingUtils.unzip(zipFile, repository.getRunDirectory(version),
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = Constants.GSON.fromJson(json, Version.class).setJar(null);
dependencies.add(new VersionJsonSaveTask(repository, version));
CompressingUtils.unzip(zipFile, repository.getRunDirectory(id),
"minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), false);
}
}

View File

@@ -66,31 +66,31 @@ public final class LauncherHelper {
Version version = repository.getVersion(selectedVersion);
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Controllers.INSTANCE.dialog(launchingStepsPane);
TaskExecutor executor = Task.of(v -> emitStatus(LoadingState.DEPENDENCIES), Schedulers.javafx())
Controllers.dialog(launchingStepsPane);
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(dependencyManager.checkGameCompletionAsync(version))
.then(Task.of(v -> emitStatus(LoadingState.MODS), Schedulers.javafx()))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(new CurseCompletionTask(dependencyManager, selectedVersion))
.then(Task.of(v -> emitStatus(LoadingState.LOGIN), Schedulers.javafx()))
.then(Task.of(v -> {
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGIN)))
.then(Task.of(variables -> {
try {
v.set("account", account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy()));
variables.set("account", account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy()));
} catch (AuthenticationException e) {
v.set("account", DialogController.INSTANCE.logIn(account));
JFXUtilities.runInFX(() -> Controllers.INSTANCE.dialog(launchingStepsPane));
variables.set("account", DialogController.logIn(account));
JFXUtilities.runInFX(() -> Controllers.dialog(launchingStepsPane));
}
}))
.then(Task.of(v -> emitStatus(LoadingState.LAUNCHING), Schedulers.javafx()))
.then(Task.of(v -> {
v.set("launcher", new HMCLGameLauncher(
repository, selectedVersion, v.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(v.get("account"), setting)
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
.then(Task.of(variables -> {
variables.set("launcher", new HMCLGameLauncher(
repository, selectedVersion, variables.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(variables.get("account"), setting)
));
}))
.then(v -> v.<DefaultLauncher>get("launcher").launchAsync())
.then(Task.of(v -> {
PROCESSES.add(v.get(DefaultLauncher.LAUNCH_ASYNC_ID));
.then(variables -> variables.<DefaultLauncher>get("launcher").launchAsync())
.then(Task.of(variables -> {
PROCESSES.add(variables.get(DefaultLauncher.LAUNCH_ASYNC_ID));
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
Main.Companion.stop();
Main.stopApplication();
}))
.executor();
@@ -106,7 +106,7 @@ public final class LauncherHelper {
@Override
public void onTerminate() {
Platform.runLater(Controllers.INSTANCE::closeDialog);
Platform.runLater(Controllers::closeDialog);
}
});
@@ -122,7 +122,7 @@ public final class LauncherHelper {
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE)
Controllers.INSTANCE.closeDialog();
Controllers.closeDialog();
launchingStepsPane.setCurrentState(state.toString());
launchingStepsPane.setSteps((state.ordinal() + 1) + " / " + LoadingState.values().length);
@@ -131,7 +131,7 @@ public final class LauncherHelper {
private void checkExit(LauncherVisibility v) {
switch (v) {
case HIDE_AND_REOPEN:
Platform.runLater(Controllers.INSTANCE.getStage()::show);
Platform.runLater(Controllers.getStage()::show);
break;
case KEEP:
// No operations here
@@ -143,7 +143,7 @@ public final class LauncherHelper {
// Shut down the platform when user closed log window.
Platform.setImplicitExit(true);
// If we use Main.stop(), log window will be halt immediately.
Main.Companion.stopWithoutJavaFXPlatform();
Main.stopWithoutPlatform();
});
break;
}
@@ -215,7 +215,7 @@ public final class LauncherHelper {
switch (visibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
Controllers.INSTANCE.getStage().hide();
Controllers.getStage().hide();
emitStatus(LoadingState.DONE);
});
break;
@@ -226,7 +226,7 @@ public final class LauncherHelper {
break;
case HIDE:
Platform.runLater(() -> {
Controllers.INSTANCE.getStage().close();
Controllers.getStage().close();
emitStatus(LoadingState.DONE);
});
break;

View File

@@ -34,7 +34,7 @@ public final class Config {
private String backgroundImage = null;
@SerializedName("commonpath")
private String commonDirectory = Main.getMinecraftDirectory().getAbsolutePath();
private String commonDirectory = Main.MINECRAFT_DIRECTORY.getAbsolutePath();
@SerializedName("proxyType")
private int proxyType = 0;

View File

@@ -130,7 +130,7 @@ public class Settings {
else {
Logging.LOG.config("No settings file here, may be first loading.");
if (!c.getConfigurations().containsKey(HOME_PROFILE))
c.getConfigurations().put(HOME_PROFILE, new Profile(HOME_PROFILE, Main.getMinecraftDirectory()));
c.getConfigurations().put(HOME_PROFILE, new Profile(HOME_PROFILE, Main.MINECRAFT_DIRECTORY));
}
return c;
}

View File

@@ -0,0 +1,143 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import com.jfoenix.controls.JFXRadioButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
public final class AccountItem extends StackPane {
private final Account account;
@FXML
private Pane icon;
@FXML private VBox content;
@FXML private StackPane header;
@FXML private StackPane body;
@FXML private JFXButton btnDelete;
@FXML private JFXButton btnRefresh;
@FXML private Label lblUser;
@FXML private JFXRadioButton chkSelected;
@FXML private Label lblType;
@FXML private JFXProgressBar pgsSkin;
@FXML private ImageView portraitView;
@FXML private HBox buttonPane;
public AccountItem(int i, Account account, ToggleGroup toggleGroup) {
this.account = account;
FXUtils.loadFXML(this, "/assets/fxml/account-item.fxml");
FXUtils.limitWidth(this, 160);
FXUtils.limitHeight(this, 156);
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -0.5, 1.0));
chkSelected.setToggleGroup(toggleGroup);
btnDelete.setGraphic(SVG.delete("black", 15, 15));
btnRefresh.setGraphic(SVG.refresh("black", 15, 15));
// create content
String headerColor = getDefaultColor(i % 12);
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor);
// create image view
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 32.0, header.boundsInParentProperty(), icon.heightProperty()));
chkSelected.getProperties().put("account", account);
chkSelected.setSelected(Settings.INSTANCE.getSelectedAccount() == account);
lblUser.setText(account.getUsername());
lblType.setText(AccountsPage.accountType(account));
if (account instanceof YggdrasilAccount) {
btnRefresh.setOnMouseClicked(e -> {
pgsSkin.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), this::loadSkin);
});
AccountHelper.loadSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), this::loadSkin);
}
if (account instanceof OfflineAccount) { // Offline Account cannot be refreshed,
buttonPane.getChildren().remove(btnRefresh);
}
}
private void loadSkin() {
if (!(account instanceof YggdrasilAccount))
return;
pgsSkin.setVisible(false);
portraitView.setViewport(AccountHelper.getViewport(4));
portraitView.setImage(AccountHelper.getSkin((YggdrasilAccount) account, 4));
FXUtils.limitSize(portraitView, 32, 32);
}
private String getDefaultColor(int i) {
switch (i) {
case 0: return "#8F3F7E";
case 1: return "#B5305F";
case 2: return "#CE584A";
case 3: return "#DB8D5C";
case 4: return "#DA854E";
case 5: return "#E9AB44";
case 6: return "#FEE435";
case 7: return "#99C286";
case 8: return "#01A05E";
case 9: return "#4A8895";
case 10: return "#16669B";
case 11: return "#2F65A5";
case 12: return "#4E6A9C";
default: return "#FFFFFF";
}
}
public Account getAccount() {
return account;
}
public void setSelected(boolean selected) {
chkSelected.setSelected(selected);
}
public void setOnDeleteButtonMouseClicked(EventHandler<? super MouseEvent> eventHandler) {
btnDelete.setOnMouseClicked(eventHandler);
}
}

View File

@@ -0,0 +1,180 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.OfflineAccount;
import org.jackhuang.hmcl.auth.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public final class AccountsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", "Accounts");
@FXML
private ScrollPane scrollPane;
@FXML private JFXMasonryPane masonryPane;
@FXML private JFXDialog dialog;
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXProgressBar progressBar;
{
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
getChildren().remove(dialog);
dialog.setDialogContainer(this);
FXUtils.smoothScrolling(scrollPane);
FXUtils.setValidateWhileTextChanged(txtUsername);
FXUtils.setValidateWhileTextChanged(txtPassword);
cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> {
txtPassword.setVisible(newValue.intValue() != 0);
});
cboType.getSelectionModel().select(0);
txtPassword.setOnAction(e -> onCreationAccept());
txtUsername.setOnAction(e -> onCreationAccept());
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), account -> {
for (Node node : masonryPane.getChildren())
if (node instanceof AccountItem)
((AccountItem) node).setSelected(account == ((AccountItem) node).getAccount());
});
loadAccounts();
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount();
}
public void loadAccounts() {
List<Node> children = new LinkedList<>();
int i = 0;
ToggleGroup group = new ToggleGroup();
for (Map.Entry<String, Account> entry : Settings.INSTANCE.getAccounts().entrySet()) {
children.add(buildNode(++i, entry.getValue(), group));
}
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (newValue != null)
Settings.INSTANCE.setSelectedAccount((Account) newValue.getProperties().get("account"));
});
FXUtils.resetChildren(masonryPane, children);
Platform.runLater(() -> {
masonryPane.requestLayout();
scrollPane.requestLayout();
});
}
private Node buildNode(int i, Account account, ToggleGroup group) {
AccountItem item = new AccountItem(i, account, group);
item.setOnDeleteButtonMouseClicked(e -> {
Settings.INSTANCE.deleteAccount(account.getUsername());
Platform.runLater(this::loadAccounts);
});
return item;
}
public void addNewAccount() {
txtUsername.setText("");
txtPassword.setText("");
dialog.show();
}
public void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex();
String username = txtUsername.getText();
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> {
try {
Account account;
switch (type) {
case 0: account = OfflineAccountFactory.INSTANCE.fromUsername(username); break;
case 1: account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password); break;
default: throw new Error();
}
account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy());
return account;
} catch (Exception e) {
return e;
}
}).subscribe(Schedulers.javafx(), variables -> {
Object account = variables.get("create_account");
if (account instanceof Account) {
Settings.INSTANCE.addAccount((Account) account);
dialog.close();
loadAccounts();
} else if (account instanceof InvalidCredentialsException) {
lblCreationWarning.setText(Main.i18n("login.wrong_password"));
} else if (account instanceof Exception) {
lblCreationWarning.setText(((Exception) account).getLocalizedMessage());
}
progressBar.setVisible(false);
});
}
public void onCreationCancel() {
dialog.close();
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return Main.i18n("login.methods.offline");
else if (account instanceof YggdrasilAccount) return Main.i18n("login.methods.yggdrasil");
else throw new Error(Main.i18n("login.methods.no_method") + ": " + account);
}
}

View File

@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
public class AdvancedListBox extends ScrollPane {
private final VBox container = new VBox();
{
setContent(container);
FXUtils.smoothScrolling(this);
setFitToHeight(true);
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
container.setSpacing(5);
container.getStyleClass().add("advanced-list-box-content");
}
public AdvancedListBox add(Node child) {
if (child instanceof Pane)
container.getChildren().add(child);
else {
StackPane pane = new StackPane();
pane.getStyleClass().add("advanced-list-box-item");
pane.getChildren().setAll(child);
container.getChildren().add(pane);
}
return this;
}
public AdvancedListBox startCategory(String category) {
return add(new ClassTitle(category));
}
}

View File

@@ -0,0 +1,113 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.JavaVersion;
public final class Controllers {
private static Scene scene;
private static Stage stage;
private static MainPage mainPage = new MainPage();
private static SettingsPage settingsPage = null;
private static VersionPage versionPage = null;
private static LeftPaneController leftPaneController;
private static Decorator decorator;
public static Scene getScene() {
return scene;
}
public static Stage getStage() {
return stage;
}
// FXThread
public static SettingsPage getSettingsPage() {
if (settingsPage == null)
settingsPage = new SettingsPage();
return settingsPage;
}
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionPage();
return versionPage;
}
public static Decorator getDecorator() {
return decorator;
}
public static MainPage getMainPage() {
return mainPage;
}
public static LeftPaneController getLeftPaneController() {
return leftPaneController;
}
public static void initialize(Stage stage) {
Controllers.stage = stage;
decorator = new Decorator(stage, mainPage, Main.TITLE, false, true);
decorator.showPage(null);
leftPaneController = new LeftPaneController(decorator.getLeftPane());
Settings.INSTANCE.onProfileLoading();
Task.of(JavaVersion::initialize).start();
decorator.setCustomMaximize(false);
scene = new Scene(decorator, 804, 521);
scene.getStylesheets().addAll(FXUtils.STYLESHEETS);
stage.setMinWidth(800);
stage.setMaxWidth(800);
stage.setMinHeight(480);
stage.setMaxHeight(480);
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.setTitle(Main.TITLE);
}
public static JFXDialog dialog(Region content) {
return decorator.showDialog(content);
}
public static void dialog(String text) {
dialog(new MessageDialogPane(text, decorator.getDialog()));
}
public static void closeDialog() {
decorator.getDialog().close();
}
public static void navigate(Node node) {
decorator.showPage(node);
}
}

View File

@@ -0,0 +1,519 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
import com.jfoenix.controls.JFXHamburger;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.svg.SVGGlyph;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.wizard.*;
import org.jackhuang.hmcl.util.Lang;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public final class Decorator extends StackPane implements AbstractWizardDisplayer {
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
private static final SVGGlyph resizeMax = Lang.apply(new SVGGlyph(0, "RESIZE_MAX", "M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34 25-60t59-26z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private static final SVGGlyph resizeMin = Lang.apply(new SVGGlyph(0, "RESIZE_MIN", "M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private static final SVGGlyph close = Lang.apply(new SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>(Main::stopApplication);
private final BooleanProperty customMaximize = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private final Node mainPage;
private final boolean max, min;
private final WizardController wizardController = new WizardController(this);
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging, dialogShown, maximized;
private BoundingBox originalBox, maximizedBox;
private TransitionHandler animationHandler;
@FXML
private StackPane contentPlaceHolder;
@FXML
private StackPane drawerWrapper;
@FXML
private BorderPane titleContainer;
@FXML
private BorderPane leftRootPane;
@FXML
private HBox buttonsContainer;
@FXML
private JFXButton backNavButton;
@FXML
private JFXButton refreshNavButton;
@FXML
private JFXButton closeNavButton;
@FXML
private JFXButton refreshMenuButton;
@FXML
private JFXButton addMenuButton;
@FXML
private Label titleLabel;
@FXML
private Label lblTitle;
@FXML
private AdvancedListBox leftPane;
@FXML
private JFXDrawer drawer;
@FXML
private StackPane titleBurgerContainer;
@FXML
private JFXHamburger titleBurger;
@FXML
private JFXDialog dialog;
@FXML
private JFXButton btnMin;
@FXML
private JFXButton btnMax;
@FXML
private JFXButton btnClose;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
}
public Decorator(Stage primaryStage, Node mainPage, String title, boolean max, boolean min) {
this.primaryStage = primaryStage;
this.mainPage = mainPage;
this.max = max;
this.min = min;
FXUtils.loadFXML(this, "/assets/fxml/decorator.fxml");
primaryStage.initStyle(StageStyle.UNDECORATED);
btnClose.setGraphic(close);
btnMin.setGraphic(minus);
btnMax.setGraphic(resizeMax);
lblTitle.setText(title);
buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
titleContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getClickCount() == 2)
btnMax.fire();
});
drawerWrapper.getChildren().remove(dialog);
dialog.setDialogContainer(drawerWrapper);
dialog.setOnDialogClosed(e -> dialogShown = false);
dialog.setOnDialogOpened(e -> dialogShown = true);
if (!min) buttonsContainer.getChildren().remove(btnMin);
if (!max) buttonsContainer.getChildren().remove(btnMax);
JFXDepthManager.setDepth(titleContainer, 1);
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
if (!isDragging) allowMove = false;
});
animationHandler = new TransitionHandler(contentPlaceHolder);
FXUtils.setOverflowHidden((Pane) lookup("#contentPlaceHolderRoot"));
FXUtils.setOverflowHidden(drawerWrapper);
}
public void onMouseMoved(MouseEvent mouseEvent) {
if (!primaryStage.isMaximized() && !primaryStage.isFullScreen() && !maximized) {
if (!primaryStage.isResizable())
updateInitMouseValues(mouseEvent);
else {
double x = mouseEvent.getX(), y = mouseEvent.getY();
Bounds boundsInParent = getBoundsInParent();
if (getBorder() != null && getBorder().getStrokes().size() > 0) {
double borderWidth = this.contentPlaceHolder.snappedLeftInset();
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
setCursor(Cursor.NE_RESIZE);
} else if (y > this.getHeight() - borderWidth) {
setCursor(Cursor.SE_RESIZE);
} else {
setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
setCursor(Cursor.NW_RESIZE);
} else if (y > this.getHeight() - borderWidth) {
setCursor(Cursor.SW_RESIZE);
} else {
setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
setCursor(Cursor.N_RESIZE);
} else if (this.isBottomEdge(x, y, boundsInParent)) {
setCursor(Cursor.S_RESIZE);
} else {
setCursor(Cursor.DEFAULT);
}
this.updateInitMouseValues(mouseEvent);
}
}
} else {
setCursor(Cursor.DEFAULT);
}
}
public void onMouseReleased() {
isDragging = false;
}
public void onMouseDragged(MouseEvent mouseEvent) {
this.isDragging = true;
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltax = this.newX - this.initX;
double deltay = this.newY - this.initY;
Cursor cursor = this.getCursor();
if (Cursor.E_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltax);
mouseEvent.consume();
} else if (Cursor.NE_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
this.primaryStage.setY(this.primaryStage.getY() + deltay);
}
this.setStageWidth(this.primaryStage.getWidth() + deltax);
mouseEvent.consume();
} else if (Cursor.SE_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltax);
this.setStageHeight(this.primaryStage.getHeight() + deltay);
mouseEvent.consume();
} else if (Cursor.S_RESIZE == cursor) {
this.setStageHeight(this.primaryStage.getHeight() + deltay);
mouseEvent.consume();
} else if (Cursor.W_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
this.primaryStage.setX(this.primaryStage.getX() + deltax);
}
mouseEvent.consume();
} else if (Cursor.SW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
this.primaryStage.setX(this.primaryStage.getX() + deltax);
}
this.setStageHeight(this.primaryStage.getHeight() + deltay);
mouseEvent.consume();
} else if (Cursor.NW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
this.primaryStage.setX(this.primaryStage.getX() + deltax);
}
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
this.primaryStage.setY(this.primaryStage.getY() + deltay);
}
mouseEvent.consume();
} else if (Cursor.N_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
this.primaryStage.setY(this.primaryStage.getY() + deltay);
}
mouseEvent.consume();
} else if (this.allowMove) {
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
mouseEvent.consume();
}
}
}
}
public void onMin() {
primaryStage.setIconified(true);
}
public void onMax() {
if (!max) return;
if (!this.isCustomMaximize()) {
this.primaryStage.setMaximized(!this.primaryStage.isMaximized());
this.maximized = this.primaryStage.isMaximized();
if (this.primaryStage.isMaximized()) {
this.btnMax.setGraphic(resizeMin);
this.btnMax.setTooltip(new Tooltip("Restore Down"));
} else {
this.btnMax.setGraphic(resizeMax);
this.btnMax.setTooltip(new Tooltip("Maximize"));
}
} else {
if (!this.maximized) {
this.originalBox = new BoundingBox(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight());
Screen screen = Screen.getScreensForRectangle(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight()).get(0);
Rectangle2D bounds = screen.getVisualBounds();
this.maximizedBox = new BoundingBox(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
primaryStage.setX(this.maximizedBox.getMinX());
primaryStage.setY(this.maximizedBox.getMinY());
primaryStage.setWidth(this.maximizedBox.getWidth());
primaryStage.setHeight(this.maximizedBox.getHeight());
this.btnMax.setGraphic(resizeMin);
this.btnMax.setTooltip(new Tooltip("Restore Down"));
} else {
primaryStage.setX(this.originalBox.getMinX());
primaryStage.setY(this.originalBox.getMinY());
primaryStage.setWidth(this.originalBox.getWidth());
primaryStage.setHeight(this.originalBox.getHeight());
this.originalBox = null;
this.btnMax.setGraphic(resizeMax);
this.btnMax.setTooltip(new Tooltip("Maximize"));
}
this.maximized = !this.maximized;
}
}
public void onClose() {
onCloseButtonAction.get().run();
}
private void updateInitMouseValues(MouseEvent mouseEvent) {
initX = mouseEvent.getScreenX();
initY = mouseEvent.getScreenY();
xOffset = mouseEvent.getSceneX();
yOffset = mouseEvent.getSceneY();
}
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
return x < getWidth() && x > getWidth() - contentPlaceHolder.snappedLeftInset();
}
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
return y >= 0 && y < contentPlaceHolder.snappedLeftInset();
}
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
return y < getHeight() && y > getHeight() - contentPlaceHolder.snappedLeftInset();
}
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
return x >= 0 && x < contentPlaceHolder.snappedLeftInset();
}
private boolean setStageWidth(double width) {
if (width >= primaryStage.getMinWidth() && width >= titleContainer.getMinWidth()) {
primaryStage.setWidth(width);
initX = newX;
return true;
} else {
if (width >= primaryStage.getMinWidth() && width <= titleContainer.getMinWidth())
primaryStage.setWidth(titleContainer.getMinWidth());
return false;
}
}
private boolean setStageHeight(double height) {
if (height >= primaryStage.getMinHeight() && height >= titleContainer.getHeight()) {
primaryStage.setHeight(height);
initY = newY;
return true;
} else {
if (height >= primaryStage.getMinHeight() && height <= titleContainer.getHeight())
primaryStage.setHeight(titleContainer.getHeight());
return false;
}
}
public void setMaximized(boolean maximized) {
if (this.maximized != maximized) {
Platform.runLater(btnMax::fire);
}
}
private void setContent(Node content, AnimationProducer animation) {
animationHandler.setContent(content, animation);
if (content instanceof Region) {
((Region) content).setMinSize(0, 0);
FXUtils.setOverflowHidden((Region) content);
}
backNavButton.setDisable(!wizardController.canPrev());
if (content instanceof Refreshable)
refreshNavButton.setVisible(true);
if (content != mainPage)
closeNavButton.setVisible(true);
String prefix = category == null ? "" : category + " - ";
titleLabel.textProperty().unbind();
if (content instanceof WizardPage)
titleLabel.setText(prefix + ((WizardPage) content).getTitle());
if (content instanceof DecoratorPage)
titleLabel.textProperty().bind(((DecoratorPage) content).titleProperty());
}
private String category;
private Node nowPage;
public void showPage(Node content) {
Node c = content == null ? mainPage : content;
onEnd();
if (nowPage instanceof DecoratorPage)
((DecoratorPage) nowPage).onClose();
nowPage = content;
setContent(c, ContainerAnimations.FADE.getAnimationProducer());
if (c instanceof Region) {
// Let root pane fix window size.
StackPane parent = (StackPane) c.getParent();
((Region) c).prefWidthProperty().bind(parent.widthProperty());
((Region) c).prefHeightProperty().bind(parent.heightProperty());
}
}
public JFXDialog showDialog(Region content) {
dialog.setContent(content);
if (!dialogShown)
dialog.show();
return dialog;
}
public void startWizard(WizardProvider wizardProvider) {
startWizard(wizardProvider, null);
}
public void startWizard(WizardProvider wizardProvider, String category) {
this.category = category;
wizardController.setProvider(wizardProvider);
wizardController.onStart();
}
@Override
public void onStart() {
backNavButton.setVisible(true);
backNavButton.setDisable(false);
closeNavButton.setVisible(true);
refreshNavButton.setVisible(false);
}
@Override
public void onEnd() {
backNavButton.setVisible(false);
closeNavButton.setVisible(false);
refreshNavButton.setVisible(false);
}
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
setContent(page, nav.getAnimation().getAnimationProducer());
}
public void onRefresh() {
((Refreshable) contentPlaceHolder.getChildren().get(0)).refresh();
}
public void onCloseNav() {
wizardController.onCancel();
showPage(null);
}
public void onBack() {
wizardController.onPrev(true);
}
@Override
public Queue<Object> getCancelQueue() {
return cancelQueue;
}
public Runnable getOnCloseButtonAction() {
return onCloseButtonAction.get();
}
public ObjectProperty<Runnable> onCloseButtonActionProperty() {
return onCloseButtonAction;
}
public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
this.onCloseButtonAction.set(onCloseButtonAction);
}
public boolean isCustomMaximize() {
return customMaximize.get();
}
public BooleanProperty customMaximizeProperty() {
return customMaximize;
}
public void setCustomMaximize(boolean customMaximize) {
this.customMaximize.set(customMaximize);
}
@Override
public WizardController getWizardController() {
return wizardController;
}
public JFXDialog getDialog() {
return dialog;
}
public JFXButton getAddMenuButton() {
return addMenuButton;
}
public AdvancedListBox getLeftPane() {
return leftPane;
}
}

View File

@@ -29,9 +29,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
public final class DialogController {
public static final DialogController INSTANCE = new DialogController();
private DialogController() {}
public static AuthInfo logIn(Account account) throws Exception {
if (account instanceof YggdrasilAccount) {
@@ -41,14 +38,12 @@ public final class DialogController {
YggdrasilAccountLoginPane pane = new YggdrasilAccountLoginPane((YggdrasilAccount) account, it -> {
res.set(it);
latch.countDown();
Controllers.INSTANCE.closeDialog();
return Unit.INSTANCE;
Controllers.closeDialog();
}, () -> {
latch.countDown();
Controllers.INSTANCE.closeDialog();
return Unit.INSTANCE;
Controllers.closeDialog();
});
pane.dialog = Controllers.INSTANCE.dialog(pane);
pane.setDialog(Controllers.dialog(pane));
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);

View File

@@ -0,0 +1,303 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.ReflectionHelper.call;
import static org.jackhuang.hmcl.util.ReflectionHelper.construct;
public final class FXUtils {
private FXUtils() {
}
public static <T> void onChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener((a, b, c) -> consumer.accept(c));
}
public static <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener(new WeakChangeListener<>((a, b, c) -> consumer.accept(c)));
}
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onChange(value, consumer);
consumer.accept(value.getValue());
}
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onWeakChange(value, consumer);
consumer.accept(value.getValue());
}
public static void limitSize(ImageView imageView, double maxWidth, double maxHeight) {
imageView.setPreserveRatio(true);
onChangeAndOperate(imageView.imageProperty(), image -> {
if (image != null && (image.getWidth() > maxWidth || image.getHeight() > maxHeight)) {
imageView.setFitHeight(maxHeight);
imageView.setFitWidth(maxWidth);
} else {
imageView.setFitHeight(-1);
imageView.setFitWidth(-1);
}
});
}
public static void setValidateWhileTextChanged(JFXTextField field) {
field.textProperty().addListener(o -> field.validate());
field.validate();
}
public static void setValidateWhileTextChanged(JFXPasswordField field) {
field.textProperty().addListener(o -> field.validate());
field.validate();
}
public static void setOverflowHidden(Region region) {
Rectangle rectangle = new Rectangle();
rectangle.widthProperty().bind(region.widthProperty());
rectangle.heightProperty().bind(region.heightProperty());
region.setClip(rectangle);
}
public static void limitWidth(Region region, double width) {
region.setMaxWidth(width);
region.setMinWidth(width);
region.setPrefWidth(width);
}
public static void limitHeight(Region region, double height) {
region.setMaxHeight(height);
region.setMinHeight(height);
region.setPrefHeight(height);
}
public static void smoothScrolling(ScrollPane scrollPane) {
JFXScrollPane.smoothScrolling(scrollPane);
}
public static void loadFXML(Node node, String absolutePath) {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Main.RESOURCE_BUNDLE);
loader.setRoot(node);
loader.setController(node);
Lang.invoke(() -> loader.load());
}
public static WritableImage takeSnapshot(Parent node, double width, double height) {
Scene scene = new Scene(node, width, height);
scene.getStylesheets().addAll(STYLESHEETS);
return scene.snapshot(null);
}
public static void resetChildren(JFXMasonryPane pane, List<Node> children) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null);
pane.getChildren().setAll(children);
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
try {
call(construct(Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"), new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false),
"install", node, tooltip);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e);
Tooltip.install(node, tooltip);
}
}
public static boolean alert(Alert.AlertType type, String title, String contentText) {
return alert(type, title, contentText, null);
}
public static boolean alert(Alert.AlertType type, String title, String contentText, String headerText) {
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(headerText);
alert.setContentText(contentText);
Optional<ButtonType> result = alert.showAndWait();
return result.isPresent() && result.get() == ButtonType.OK;
}
public static Optional<String> inputDialog(String title, String contentText) {
return inputDialog(title, contentText, null);
}
public static Optional<String> inputDialog(String title, String contentText, String headerText) {
return inputDialog(title, contentText, headerText, "");
}
public static Optional<String> inputDialog(String title, String contentText, String headerText, String defaultValue) {
TextInputDialog dialog = new TextInputDialog(defaultValue);
dialog.setTitle(title);
dialog.setHeaderText(headerText);
dialog.setContentText(contentText);
return dialog.showAndWait();
}
public static void openFolder(File file) {
file.mkdirs();
String path = file.getAbsolutePath();
switch (OperatingSystem.CURRENT_OS) {
case OSX:
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
}
break;
default:
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
}
}
public static void bindInt(JFXTextField textField, Property<?> property) {
textField.textProperty().unbind();
textField.textProperty().bindBidirectional((Property<Integer>) property, SafeIntStringConverter.INSTANCE);
}
public static void bindString(JFXTextField textField, Property<String> property) {
textField.textProperty().unbind();
textField.textProperty().bindBidirectional(property);
}
public static void bindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
toggleButton.selectedProperty().unbind();
toggleButton.selectedProperty().bindBidirectional(property);
}
public static void bindBoolean(JFXCheckBox checkBox, Property<Boolean> property) {
checkBox.selectedProperty().unbind();
checkBox.selectedProperty().bindBidirectional(property);
}
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum> property) {
unbindEnum(comboBox);
ChangeListener<Number> listener = (a, b, newValue) -> {
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
};
comboBox.getSelectionModel().select(property.getValue().ordinal());
comboBox.getProperties().put("listener", listener);
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
}
public static void unbindEnum(JFXComboBox<?> comboBox) {
ChangeListener listener = Lang.get(comboBox.getProperties(), "listener", ChangeListener.class, null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
public static void smoothScrolling(ListView<?> listView) {
listView.skinProperty().addListener(o -> {
ScrollBar bar = (ScrollBar) listView.lookup(".scroll-bar");
Node virtualFlow = listView.lookup(".virtual-flow");
double[] frictions = new double[]{0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
double[] pushes = new double[]{1};
double[] derivatives = new double[frictions.length];
Timeline timeline = new Timeline();
bar.addEventHandler(MouseEvent.DRAG_DETECTED, e -> timeline.stop());
EventHandler<ScrollEvent> scrollEventHandler = event -> {
if (event.getEventType() == ScrollEvent.SCROLL) {
int direction = event.getDeltaY() > 0 ? -1 : 1;
for (int i = 0; i < pushes.length; ++i)
derivatives[i] += direction * pushes[i];
if (timeline.getStatus() == Animation.Status.STOPPED)
timeline.play();
event.consume();
}
};
bar.addEventHandler(ScrollEvent.ANY, scrollEventHandler);
virtualFlow.setOnScroll(scrollEventHandler);
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
for (int i = 0; i < derivatives.length; ++i)
derivatives[i] *= frictions[i];
for (int i = 1; i < derivatives.length; ++i)
derivatives[i] += derivatives[i - 1];
double dy = derivatives[derivatives.length - 1];
double height = listView.getLayoutBounds().getHeight();
bar.setValue(Math.min(Math.max(bar.getValue() + dy / height, 0), 1));
if (Math.abs(dy) < 0.001)
timeline.stop();
listView.requestLayout();
}));
timeline.setCycleCount(Animation.INDEFINITE);
});
}
public static final Image DEFAULT_ICON = new Image("/assets/img/icon.png");
public static final String[] STYLESHEETS = new String[]{
FXUtils.class.getResource("/css/jfoenix-fonts.css").toExternalForm(),
FXUtils.class.getResource("/css/jfoenix-design.css").toExternalForm(),
FXUtils.class.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm()
};
public static final Interpolator SINE = new Interpolator() {
@Override
protected double curve(double t) {
return Math.sin(t * Math.PI / 2);
}
@Override
public String toString() {
return "Interpolator.SINE";
}
};
}

View File

@@ -43,7 +43,7 @@ public class InstallerController {
private String optiFine;
public void initialize() {
FXUtilsKt.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
}
public void loadVersion(Profile profile, String versionId) {
@@ -59,8 +59,8 @@ public class InstallerController {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new VersionJsonSaveTask(profile.getRepository(), version.setLibraries(newList))
.with(Task.of(e -> profile.getRepository().refreshVersions()))
.with(Task.of(e -> loadVersion(this.profile, this.versionId)))
.with(Task.of(profile.getRepository()::refreshVersions))
.with(Task.of(() -> loadVersion(this.profile, this.versionId)))
.start();
};
@@ -85,6 +85,6 @@ public class InstallerController {
// TODO: if minecraftVersion returns null.
if (gameVersion == null) return;
Controllers.INSTANCE.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion, version, forge, liteLoader, optiFine));
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion, version, forge, liteLoader, optiFine));
}
}

View File

@@ -38,7 +38,7 @@ public class InstallerItem extends BorderPane {
public InstallerItem(String artifact, String version, Consumer<InstallerItem> deleteCallback) {
this.deleteCallback = deleteCallback;
FXUtilsKt.loadFXML(this, "/assets/fxml/version/installer-item.fxml");
FXUtils.loadFXML(this, "/assets/fxml/version/installer-item.fxml");
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);

View File

@@ -31,10 +31,10 @@ public class LaunchingStepsPane extends StackPane {
private Label lblSteps;
public LaunchingStepsPane() {
FXUtilsKt.loadFXML(this, "/assets/fxml/launching-steps.fxml");
FXUtils.loadFXML(this, "/assets/fxml/launching-steps.fxml");
FXUtilsKt.limitHeight(this, 200);
FXUtilsKt.limitWidth(this, 400);
FXUtils.limitHeight(this, 200);
FXUtils.limitWidth(this, 400);
}
public void setCurrentState(String currentState) {

View File

@@ -0,0 +1,118 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Paint;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import java.util.LinkedList;
import java.util.Objects;
public final class LeftPaneController {
private final AdvancedListBox leftPane;
private final VBox profilePane = new VBox();
private final VersionListItem accountItem = new VersionListItem("No Account", "unknown");
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
leftPane.startCategory("ACCOUNTS")
.add(Lang.apply(new RipplerContainer(accountItem), rippler -> {
rippler.setOnMouseClicked(e -> Controllers.navigate(new AccountsPage()));
accountItem.setOnSettingsButtonClicked(() -> Controllers.navigate(new AccountsPage()));
}))
.startCategory("LAUNCHER")
.add(Lang.apply(new IconedItem(SVG.gear("black", 20, 20), Main.i18n("launcher.title.launcher")), iconedItem -> {
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
}))
.startCategory(Main.i18n("ui.label.profile"))
.add(profilePane);
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
Controllers.getDecorator().getAddMenuButton().setOnMouseClicked(e ->
Controllers.getDecorator().showPage(new ProfilePage(null))
);
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), it -> {
if (it == null) {
accountItem.setVersionName("mojang@mojang.com");
accountItem.setGameVersion("Yggdrasil");
} else {
accountItem.setVersionName(it.getUsername());
accountItem.setGameVersion(AccountsPage.accountType(it));
}
if (it instanceof YggdrasilAccount)
accountItem.setImage(AccountHelper.getSkin((YggdrasilAccount) it, 4), AccountHelper.getViewport(4));
else
accountItem.setImage(FXUtils.DEFAULT_ICON, null);
});
if (Settings.INSTANCE.getAccounts().isEmpty())
Controllers.navigate(new AccountsPage());
}
public void onProfileChanged(ProfileChangedEvent event) {
Profile profile = event.getProfile();
for (Node node : profilePane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof Pair<?, ?>) {
((RipplerContainer) node).setSelected(Objects.equals(((Pair) node.getProperties().get("profile")).getKey(), profile.getName()));
}
}
}
public void onProfilesLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
for (Profile profile : Settings.INSTANCE.getProfiles()) {
VersionListItem item = new VersionListItem(profile.getName());
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(() -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
ripplerContainer.setRipplerFill(Paint.valueOf("#89E1F9"));
ripplerContainer.setOnMouseClicked(e -> {
// clean selected property
for (Node node : profilePane.getChildren())
if (node instanceof RipplerContainer)
((RipplerContainer) node).setSelected(false);
ripplerContainer.setSelected(true);
Settings.INSTANCE.setSelectedProfile(profile);
});
ripplerContainer.getProperties().put("profile", new Pair<>(profile.getName(), item));
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
list.add(ripplerContainer);
}
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
}

View File

@@ -31,7 +31,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.MainKt;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
@@ -61,8 +61,8 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(FXUtilsKt.getStylesheets());
setTitle(MainKt.i18n("logwindow.title"));
getScene().getStylesheets().addAll(FXUtils.STYLESHEETS);
setTitle(Main.i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -164,7 +164,7 @@ public final class LogWindow extends Stage {
Document document;
LogWindowImpl() {
FXUtilsKt.loadFXML(this, "/assets/fxml/log.fxml");
FXUtils.loadFXML(this, "/assets/fxml/log.fxml");
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))

View File

@@ -0,0 +1,127 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXMasonryPane;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
public final class MainPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("launcher.title.main"));
@FXML
private JFXButton btnRefresh;
@FXML
private JFXButton btnAdd;
@FXML
private JFXMasonryPane masonryPane;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(() -> Platform.runLater(this::loadVersions));
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), "Install New Game"));
btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersions());
}
private Node buildNode(Profile profile, String version, String game) {
VersionItem item = new VersionItem();
item.setGameVersion(game);
item.setVersionName(version);
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Main.i18n("login.no_Player007"));
else
LauncherHelper.INSTANCE.launch(version);
});
item.setOnDeleteButtonClicked(e -> {
profile.getRepository().removeVersionFromDisk(version);
Platform.runLater(this::loadVersions);
});
item.setOnSettingsButtonClicked(e -> {
Controllers.getDecorator().showPage(Controllers.getVersionPage());
Controllers.getVersionPage().load(version, profile);
});
File iconFile = profile.getRepository().getVersionIcon(version);
if (iconFile.exists())
item.setImage(new Image("file:" + iconFile.getAbsolutePath()));
return item;
}
public void onProfilesLoading() {
// TODO: Profiles
}
public void onProfileChanged(ProfileChangedEvent event) {
Platform.runLater(() -> loadVersions(event.getProfile()));
}
private void loadVersions() {
loadVersions(Settings.INSTANCE.getSelectedProfile());
}
private void loadVersions(Profile profile) {
List<Node> children = new LinkedList<>();
for (Version version : profile.getRepository().getVersions()) {
children.add(buildNode(profile, version.getId(), Lang.nonNull(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version.getId())), "Unknown")));
}
FXUtils.resetChildren(masonryPane, children);
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
}

View File

@@ -36,7 +36,7 @@ public final class MessageDialogPane extends StackPane {
this.text = text;
this.dialog = dialog;
FXUtilsKt.loadFXML(this, "/assets/fxml/message-dialog.fxml");
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
content.setText(text);
acceptButton.setOnMouseClicked(e -> dialog.close());
}

View File

@@ -0,0 +1,153 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.controls.JFXTabPane;
import javafx.application.Platform;
import javafx.beans.value.WeakChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Stream;
public final class ModController {
@FXML
private ScrollPane scrollPane;
@FXML private StackPane rootPane;
@FXML private VBox modPane;
@FXML private StackPane contentPane;
@FXML private JFXSpinner spinner;
private JFXTabPane parentTab;
private ModManager modManager;
private String versionId;
public void initialize() {
FXUtils.smoothScrolling(scrollPane);
rootPane.setOnDragOver(event -> {
if (event.getGestureSource() != rootPane && event.getDragboard().hasFiles())
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
event.consume();
});
rootPane.setOnDragDropped(event -> {
List<File> mods = event.getDragboard().getFiles();
Stream<File> stream = null;
if (mods != null)
stream = mods.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)));
if (stream != null && stream.findAny().isPresent()) {
stream.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
}
event.consume();
});
}
public void loadMods(ModManager modManager, String versionId) {
this.modManager = modManager;
this.versionId = versionId;
Task.of(variables -> {
synchronized (ModController.this) {
Platform.runLater(() -> {
rootPane.getChildren().remove(contentPane);
spinner.setVisible(true);
});
modManager.refreshMods(versionId);
// Surprisingly, if there are a great number of mods, this processing will cause a long UI pause,
// constructing UI elements.
// We must do this asynchronously.
LinkedList<ModItem> list = new LinkedList<>();
for (ModInfo modInfo : modManager.getMods(versionId)) {
ModItem item = new ModItem(modInfo, i -> {
modManager.removeMods(versionId, modInfo);
loadMods(modManager, versionId);
});
modInfo.activeProperty().addListener((a, b, newValue) -> {
if (newValue)
item.getStyleClass().remove("disabled");
else
item.getStyleClass().add("disabled");
});
if (!modInfo.isActive())
item.getStyleClass().add("disabled");
list.add(item);
}
Platform.runLater(() -> {
rootPane.getChildren().add(contentPane);
spinner.setVisible(false);
});
variables.set("list", list);
}
}).subscribe(Schedulers.javafx(), variables -> {
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModController.this)
modPane.getChildren().setAll(variables.<List<ModItem>>get("list"));
});
});
}
public void onAdd() {
FileChooser chooser = new FileChooser();
chooser.setTitle(Main.i18n("mods.choose_mod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter("Mod", "*.jar", "*.zip", "*.litemod"));
File res = chooser.showOpenDialog(Controllers.getStage());
if (res == null) return;
Task.of(() -> modManager.addMod(versionId, res))
.subscribe(Task.of(Schedulers.javafx(), () -> loadMods(modManager, versionId)));
}
public void setParentTab(JFXTabPane parentTab) {
this.parentTab = parentTab;
}
}

View File

@@ -0,0 +1,121 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Optional;
public final class ProfilePage extends StackPane implements DecoratorPage {
private final StringProperty title;
private final StringProperty location;
private final Profile profile;
@FXML
private JFXTextField txtProfileName;
@FXML
private FileItem gameDir;
@FXML private JFXButton btnSave;
@FXML private JFXButton btnDelete;
/**
* @param profile null if creating a new profile.
*/
public ProfilePage(Profile profile) {
this.profile = profile;
title = new SimpleStringProperty(this, "title",
profile == null ? Main.i18n("ui.newProfileWindow.title") : Main.i18n("ui.label.profile") + " - " + profile.getName());
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(""));
FXUtils.loadFXML(this, "/assets/fxml/profile.fxml");
txtProfileName.setText(Optional.ofNullable(profile).map(Profile::getName).orElse(""));
FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
gameDir.setProperty(location);
FXUtils.onChangeAndOperate(location, it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
if (profile == null)
btnDelete.setVisible(false);
}
public void onDelete() {
if (profile != null) {
Settings.INSTANCE.deleteProfile(profile);
Controllers.navigate(null);
}
}
public void onSave() {
if (profile != null) {
profile.setName(txtProfileName.getText());
if (StringUtils.isNotBlank(getLocation()))
profile.setGameDir(new File(getLocation()));
} else {
if (StringUtils.isBlank(getLocation())) {
gameDir.onExplore();
}
Settings.INSTANCE.putProfile(new Profile(txtProfileName.getText(), new File(getLocation())));
}
Settings.INSTANCE.onProfileLoading();
Controllers.navigate(null);
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getLocation() {
return location.get();
}
public StringProperty locationProperty() {
return location;
}
public void setLocation(String location) {
this.location.set(location);
}
}

View File

@@ -0,0 +1,96 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.shape.SVGPath;
public final class SVG {
private SVG() {
}
private static Node createSVGPath(String d, String fill, double width, double height) {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(d);
path.setStyle("-fx-fill: " + fill + ";");
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
svg.setScaleX(scale);
svg.setScaleY(scale);
return svg;
}
// default fill: white, width: 20, height 20
public static Node gear(String fill, double width, double height) {
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
}
public static Node back(String fill, double width, double height) {
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
}
public static Node close(String fill, double width, double height) {
return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height);
}
public static Node dotsVertical(String fill, double width, double height) {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
}
public static Node delete(String fill, double width, double height) {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
}
public static Node accountEdit(String fill, double width, double height) {
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
}
public static Node expand(String fill, double width, double height) {
return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height);
}
public static Node collapse(String fill, double width, double height) {
return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height);
}
public static Node navigate(String fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node launch(String fill, double width, double height) {
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
}
public static Node pencil(String fill, double width, double height) {
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
}
public static Node refresh(String fill, double width, double height) {
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
}
public static Node folderOpen(String fill, double width, double height) {
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
}
}

View File

@@ -26,6 +26,11 @@ import java.util.Optional;
* @author huangyuhui
*/
public final class SafeIntStringConverter extends StringConverter<Integer> {
public static final SafeIntStringConverter INSTANCE = new SafeIntStringConverter();
private SafeIntStringConverter() {
}
@Override
public Integer fromString(String string) {
return Optional.ofNullable(string).map(Lang::toIntOrNull).orElse(null);

View File

@@ -0,0 +1,135 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Locales;
import org.jackhuang.hmcl.setting.Proxies;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.FontComboBox;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
public final class SettingsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("launcher.title.launcher"));
@FXML
private JFXTextField txtProxyHost;
@FXML
private JFXTextField txtProxyPort;
@FXML
private JFXTextField txtProxyUsername;
@FXML
private JFXTextField txtProxyPassword;
@FXML
private JFXTextField txtFontSize;
@FXML
private JFXComboBox<?> cboProxyType;
@FXML
private JFXComboBox<Label> cboLanguage;
@FXML
private JFXComboBox<?> cboDownloadSource;
@FXML
private FontComboBox cboFont;
@FXML
private FileItem fileCommonLocation;
@FXML
private FileItem fileBackgroundLocation;
@FXML
private Label lblDisplay;
{
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
FXUtils.limitWidth(cboLanguage, 400);
FXUtils.limitWidth(cboDownloadSource, 400);
txtProxyHost.setText(Settings.INSTANCE.getProxyHost());
txtProxyHost.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyHost(newValue));
txtProxyPort.setText(Settings.INSTANCE.getProxyPort());
txtProxyPort.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPort(newValue));
txtProxyUsername.setText(Settings.INSTANCE.getProxyUser());
txtProxyUsername.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyUser(newValue));
txtProxyPassword.setText(Settings.INSTANCE.getProxyPass());
txtProxyPassword.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPass(newValue));
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.INSTANCE.getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
cboFont.getSelectionModel().select(Settings.INSTANCE.getFont().getFamily());
cboFont.valueProperty().addListener((a, b, newValue) -> {
Font font = Font.font(newValue, Settings.INSTANCE.getFont().getSize());
Settings.INSTANCE.setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
});
txtFontSize.setText(Double.toString(Settings.INSTANCE.getFont().getSize()));
txtFontSize.getValidators().add(new Validator(it -> Lang.toDoubleOrNull(it) != null));
txtFontSize.textProperty().addListener((a, b, newValue) -> {
if (txtFontSize.validate()) {
Font font = Font.font(Settings.INSTANCE.getFont().getFamily(), Double.parseDouble(newValue));
Settings.INSTANCE.setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
}
});
lblDisplay.setStyle("-fx-font: " + Settings.INSTANCE.getFont().getSize() + " \"" + Settings.INSTANCE.getFont().getFamily() + "\";");
ObservableList<Label> list = FXCollections.observableArrayList();
for (Locales.SupportedLocale locale : Locales.LOCALES)
list.add(new Label(locale.getName(Settings.INSTANCE.getLocale().getResourceBundle())));
cboLanguage.setItems(list);
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(Settings.INSTANCE.getLocale()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setLocale(Locales.getLocale(newValue.intValue())));
cboProxyType.getSelectionModel().select(Proxies.PROXIES.indexOf(Settings.INSTANCE.getProxyType()));
cboProxyType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyType(Proxies.getProxyType(newValue.intValue())));
fileCommonLocation.setProperty(Settings.INSTANCE.commonPathProperty());
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
}

View File

@@ -55,16 +55,16 @@ public final class VersionItem extends StackPane {
private ImageView iconView;
public VersionItem() {
FXUtilsKt.loadFXML(this, "/assets/fxml/version-item.fxml");
FXUtilsKt.limitWidth(this, 160);
FXUtilsKt.limitHeight(this, 156);
FXUtils.loadFXML(this, "/assets/fxml/version-item.fxml");
FXUtils.limitWidth(this, 160);
FXUtils.limitHeight(this, 156);
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear("black", 15, 15));
btnDelete.setGraphic(SVG.delete("black", 15, 15));
btnLaunch.setGraphic(SVG.launch("black", 15, 15));
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
FXUtilsKt.limitSize(iconView, 32, 32);
FXUtils.limitSize(iconView, 32, 32);
}
public void setVersionName(String versionName) {

View File

@@ -40,21 +40,21 @@ public final class VersionListItem extends StackPane {
}
public VersionListItem(String versionName, String gameVersion) {
FXUtilsKt.loadFXML(this, "/assets/fxml/version-list-item.fxml");
FXUtils.loadFXML(this, "/assets/fxml/version-list-item.fxml");
lblVersionName.setText(versionName);
lblGameVersion.setText(gameVersion);
FXUtilsKt.limitSize(imageView, 32, 32);
FXUtilsKt.limitWidth(imageViewContainer, 32);
FXUtilsKt.limitHeight(imageViewContainer, 32);
FXUtils.limitSize(imageView, 32, 32);
FXUtils.limitWidth(imageViewContainer, 32);
FXUtils.limitHeight(imageViewContainer, 32);
}
public void onSettings() {
handler.run();
}
public void onSettingsButtonClicked(Runnable handler) {
public void setOnSettingsButtonClicked(Runnable handler) {
this.handler = handler;
}

View File

@@ -0,0 +1,189 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXTabPane;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Tab;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils;
import java.io.File;
import java.util.Optional;
public final class VersionPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", null);
@FXML
private VersionSettingsController versionSettingsController;
@FXML
private Tab modTab;
@FXML
private ModController modController;
@FXML
private InstallerController installerController;
@FXML
private JFXListView<?> browseList;
@FXML
private JFXListView<?> managementList;
@FXML
private JFXButton btnBrowseMenu;
@FXML
private JFXButton btnManagementMenu;
@FXML
private JFXButton btnExport;
@FXML
private StackPane rootPane;
@FXML
private StackPane contentPane;
@FXML
private JFXTabPane tabPane;
private JFXPopup browsePopup;
private JFXPopup managementPopup;
private Profile profile;
private String version;
{
FXUtils.loadFXML(this, "/assets/fxml/version/version.fxml");
getChildren().removeAll(browseList, managementList);
browsePopup = new JFXPopup(browseList);
managementPopup = new JFXPopup(managementList);
FXUtils.installTooltip(btnBrowseMenu, 0, 5000, 0, new Tooltip(Main.i18n("settings.explore")));
FXUtils.installTooltip(btnManagementMenu, 0, 5000, 0, new Tooltip(Main.i18n("settings.manage")));
FXUtils.installTooltip(btnExport, 0, 5000, 0, new Tooltip(Main.i18n("settings.save")));
}
public void load(String id, Profile profile) {
this.version = id;
this.profile = profile;
title.set(Main.i18n("launcher.title.game") + " - " + id);
versionSettingsController.loadVersionSetting(profile, id, profile.getVersionSetting(id));
modController.setParentTab(tabPane);
modTab.setUserData(modController);
modController.loadMods(profile.getModManager(), id);
installerController.loadVersion(profile, id);
}
public void onBrowseMenu() {
browseList.getSelectionModel().select(-1);
;
browsePopup.show(btnBrowseMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12, 15);
}
public void onManagementMenu() {
managementList.getSelectionModel().select(-1);
;
managementPopup.show(btnManagementMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12, 15);
}
public void onExport() {
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), Main.i18n("modpack.wizard"));
}
public void onBrowse() {
String sub;
switch (browseList.getSelectionModel().getSelectedIndex()) {
case 0:
sub = "";
break;
case 1:
sub = "mods";
break;
case 2:
sub = "coremods";
break;
case 3:
sub = "config";
break;
case 4:
sub = "resourcepacks";
break;
case 5:
sub = "screenshots";
break;
case 6:
sub = "saves";
break;
default:
throw new RuntimeException();
}
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
}
public void onManagement() {
switch (managementList.getSelectionModel().getSelectedIndex()) {
case 0: // rename a version
Optional<String> res = FXUtils.inputDialog("Input", Main.i18n("versions.manage.rename.message"), null, version);
if (res.isPresent()) {
if (profile.getRepository().renameVersion(version, res.get())) {
profile.getRepository().refreshVersions();
Controllers.navigate(null);
}
}
break;
case 1: // remove a version
if (FXUtils.alert(Alert.AlertType.CONFIRMATION, "Confirm", Main.i18n("versions.manage.remove.confirm") + version)) {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersions();
Controllers.navigate(null);
}
}
break;
case 2: // redownload asset index
new GameAssetIndexDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version).resolve(profile.getRepository())).start();
break;
case 3: // delete libraries
FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries"));
break;
case 4:
throw new Error();
}
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
}

View File

@@ -0,0 +1,269 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.NumberValidator;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
public final class VersionSettingsController {
private VersionSetting lastVersionSetting = null;
private Profile profile;
private String versionId;
@FXML private VBox rootPane;
@FXML private ScrollPane scroll;
@FXML private JFXTextField txtWidth;
@FXML private JFXTextField txtHeight;
@FXML private JFXTextField txtMaxMemory;
@FXML private JFXTextField txtJVMArgs;
@FXML private JFXTextField txtGameArgs;
@FXML private JFXTextField txtMetaspace;
@FXML private JFXTextField txtWrapper;
@FXML private JFXTextField txtPrecallingCommand;
@FXML private JFXTextField txtServerIP;
@FXML private ComponentList advancedSettingsPane;
@FXML private JFXComboBox<?> cboLauncherVisibility;
@FXML private JFXCheckBox chkFullscreen;
@FXML private Label lblPhysicalMemory;
@FXML private JFXToggleButton chkNoJVMArgs;
@FXML private JFXToggleButton chkNoCommon;
@FXML private JFXToggleButton chkNoGameCheck;
@FXML private MultiFileItem javaItem;
@FXML private MultiFileItem gameDirItem;
@FXML private JFXToggleButton chkShowLogs;
@FXML private ImageView iconView;
public void initialize() {
lblPhysicalMemory.setText(Main.i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
FXUtils.smoothScrolling(scroll);
double limitWidth = 300;
FXUtils.limitWidth(txtMaxMemory, limitWidth);
FXUtils.limitWidth(cboLauncherVisibility, limitWidth);
double limitHeight = 10;
FXUtils.limitHeight(chkNoJVMArgs, limitHeight);
FXUtils.limitHeight(chkNoCommon, limitHeight);
FXUtils.limitHeight(chkNoGameCheck, limitHeight);
FXUtils.limitHeight(chkShowLogs, limitHeight);
NumberValidator nonnull = new NumberValidator("Must be a number.", false);
NumberValidator nullable = new NumberValidator("Must be a number.", true);
txtWidth.setValidators(nonnull);
FXUtils.setValidateWhileTextChanged(txtWidth);
txtHeight.setValidators(nonnull);
FXUtils.setValidateWhileTextChanged(txtHeight);
txtMaxMemory.setValidators(nonnull);
FXUtils.setValidateWhileTextChanged(txtMaxMemory);
txtMetaspace.setValidators(nullable);
FXUtils.setValidateWhileTextChanged(txtMetaspace);
Task.of(variables -> {
variables.set("list", JavaVersion.getJREs().values().stream().map(javaVersion ->
javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion)
).collect(Collectors.toList()));
}).subscribe(Schedulers.javafx(), variables ->
javaItem.loadChildren(variables.<Collection<Node>>get("list"))
);
gameDirItem.loadChildren(Arrays.asList(
gameDirItem.createChildren(Main.i18n("advancedsettings.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(Main.i18n("advancedsettings.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
));
}
public void loadVersionSetting(Profile profile, String versionId, VersionSetting versionSetting) {
rootPane.getChildren().remove(advancedSettingsPane);
this.profile = profile;
this.versionId = versionId;
if (lastVersionSetting != null) {
lastVersionSetting.widthProperty().unbind();
lastVersionSetting.heightProperty().unbind();
lastVersionSetting.maxMemoryProperty().unbind();
lastVersionSetting.javaArgsProperty().unbind();
lastVersionSetting.minecraftArgsProperty().unbind();
lastVersionSetting.permSizeProperty().unbind();
lastVersionSetting.wrapperProperty().unbind();
lastVersionSetting.preLaunchCommandProperty().unbind();
lastVersionSetting.serverIpProperty().unbind();
lastVersionSetting.fullscreenProperty().unbind();
lastVersionSetting.notCheckGameProperty().unbind();
lastVersionSetting.noCommonProperty().unbind();
lastVersionSetting.javaDirProperty().unbind();
lastVersionSetting.showLogsProperty().unbind();
FXUtils.unbindEnum(cboLauncherVisibility);
}
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
FXUtils.bindInt(txtHeight, versionSetting.heightProperty());
FXUtils.bindInt(txtMaxMemory, versionSetting.maxMemoryProperty());
FXUtils.bindString(javaItem.getTxtCustom(), versionSetting.javaDirProperty());
FXUtils.bindString(gameDirItem.getTxtCustom(), versionSetting.gameDirProperty());
FXUtils.bindString(txtJVMArgs, versionSetting.javaArgsProperty());
FXUtils.bindString(txtGameArgs, versionSetting.minecraftArgsProperty());
FXUtils.bindString(txtMetaspace, versionSetting.permSizeProperty());
FXUtils.bindString(txtWrapper, versionSetting.wrapperProperty());
FXUtils.bindString(txtPrecallingCommand, versionSetting.preLaunchCommandProperty());
FXUtils.bindString(txtServerIP, versionSetting.serverIpProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
FXUtils.bindBoolean(chkFullscreen, versionSetting.fullscreenProperty());
FXUtils.bindBoolean(chkNoGameCheck, versionSetting.notCheckGameProperty());
FXUtils.bindBoolean(chkNoCommon, versionSetting.noCommonProperty());
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
String javaGroupKey = "java_group.listener";
Lang.get(javaItem.getGroup().getProperties(), javaGroupKey, ChangeListener.class)
.ifPresent(javaItem.getGroup().selectedToggleProperty()::removeListener);
boolean flag = false;
JFXRadioButton defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle instanceof JFXRadioButton) {
if (toggle.getUserData() == Lang.invoke(versionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = (JFXRadioButton) toggle;
}
}
}
ChangeListener<Toggle> listener = (a, b, newValue) -> {
if (newValue == javaItem.getRadioCustom()) {
versionSetting.setJava("Custom");
} else {
versionSetting.setJava(((JavaVersion) newValue.getUserData()).getVersion());
}
};
javaItem.getGroup().getProperties().put(javaGroupKey, listener);
javaItem.getGroup().selectedToggleProperty().addListener(listener);
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
String gameDirKey = "game_dir.listener";
Lang.get(gameDirItem.getGroup().getProperties(), gameDirKey, ChangeListener.class)
.ifPresent(gameDirItem.getGroup().selectedToggleProperty()::removeListener);
for (Toggle toggle : gameDirItem.getGroup().getToggles()) {
if (toggle instanceof JFXRadioButton) {
if (toggle.getUserData() == versionSetting.getGameDirType()) {
toggle.setSelected(true);
flag = true;
}
}
}
gameDirItem.setCustomUserData(EnumGameDirectory.CUSTOM);
ChangeListener<Toggle> gameDirListener = (a, b, newValue) -> {
versionSetting.setGameDirType((EnumGameDirectory) newValue.getUserData());
};
gameDirItem.getGroup().getProperties().put(gameDirKey, gameDirListener);
gameDirItem.getGroup().selectedToggleProperty().addListener(gameDirListener);
versionSetting.gameDirProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
versionSetting.gameDirTypeProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
initGameDirSubtitle(versionSetting);
lastVersionSetting = versionSetting;
loadIcon();
}
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(File::getAbsolutePath).orElse("Invalid Java Directory"))));
}
private void initGameDirSubtitle(VersionSetting versionSetting) {
gameDirItem.setSubtitle(profile.getRepository().getRunDirectory(versionId).getAbsolutePath());
}
public void onShowAdvanced() {
if (!rootPane.getChildren().contains(advancedSettingsPane))
rootPane.getChildren().add(advancedSettingsPane);
else
rootPane.getChildren().remove(advancedSettingsPane);
}
public void onExploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image", "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIcon(versionId);
try {
FileUtils.copyFile(selectedFile, iconFile);
loadIcon();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
}
}
}
private void loadIcon() {
File iconFile = profile.getRepository().getVersionIcon(versionId);
if (iconFile.exists())
iconView.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconView.setImage(FXUtils.DEFAULT_ICON);
FXUtils.limitSize(iconView, 32, 32);
}
}

View File

@@ -27,7 +27,7 @@ public class WebStage extends Stage {
public WebStage() {
setScene(new Scene(webView, 800, 480));
getScene().getStylesheets().addAll(FXUtilsKt.getStylesheets());
getScene().getStylesheets().addAll(FXUtils.STYLESHEETS);
getIcons().add(new Image("/assets/img/icon.png"));
}

View File

@@ -0,0 +1,97 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.function.Consumer;
public class YggdrasilAccountLoginPane extends StackPane {
private final YggdrasilAccount oldAccount;
private final Consumer<AuthInfo> success;
private final Runnable failed;
@FXML
private Label lblUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar;
private JFXDialog dialog;
public YggdrasilAccountLoginPane(YggdrasilAccount oldAccount, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
this.success = success;
this.failed = failed;
FXUtils.loadFXML(this, "/assets/fxml/yggdrasil-account-login.fxml");
lblUsername.setText(oldAccount.getUsername());
txtPassword.setOnAction(e -> onAccept());
}
public void onAccept() {
String username = oldAccount.getUsername();
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult("login", () -> {
try {
Account account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password);
return account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy());
} catch (Exception e) {
return e;
}
}).subscribe(Schedulers.javafx(), variable -> {
Object account = variable.get("login");
if (account instanceof AuthInfo) {
success.accept(((AuthInfo) account));
dialog.close();
} else if (account instanceof InvalidCredentialsException) {
lblCreationWarning.setText(Main.i18n("login.wrong_password"));
} else if (account instanceof Exception) {
lblCreationWarning.setText(account.getClass().toString() + ": " + ((Exception) account).getLocalizedMessage());
}
progressBar.setVisible(false);
});
}
public void onCancel() {
failed.run();
dialog.close();
}
public void setDialog(JFXDialog dialog) {
this.dialog = dialog;
}
}

View File

@@ -26,7 +26,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
public final class TransitionHandler implements AnimationHandler {
private final StackPane view;
@@ -91,7 +91,7 @@ public final class TransitionHandler implements AnimationHandler {
WritableImage image;
if (content != null && content instanceof Parent) {
view.getChildren().setAll();
image = FXUtilsKt.takeSnapshot((Parent) content, view.getWidth(), view.getHeight());
image = FXUtils.takeSnapshot((Parent) content, view.getWidth(), view.getHeight());
view.getChildren().setAll(content);
} else
image = view.snapshot(new SnapshotParameters(), new WritableImage((int) view.getWidth(), (int) view.getHeight()));

View File

@@ -28,7 +28,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
/**
@@ -100,7 +100,7 @@ public class ComponentListCell extends StackPane {
VBox container = new VBox();
container.setStyle("-fx-padding: 8 0 0 0;");
FXUtilsKt.limitHeight(container, 0);
FXUtils.limitHeight(container, 0);
Rectangle clipRect = new Rectangle();
clipRect.widthProperty().bind(container.widthProperty());
clipRect.heightProperty().bind(container.heightProperty());
@@ -129,8 +129,8 @@ public class ComponentListCell extends StackPane {
animatedHeight = newAnimatedHeight;
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
new KeyValue(container.minHeightProperty(), contentHeight, FXUtilsKt.SINE),
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtilsKt.SINE)
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
));
if (!isExpanded()) {

View File

@@ -61,7 +61,7 @@ public class FileItem extends BorderPane {
public void onExplore() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.titleProperty().bind(titleProperty());
File selectedDir = chooser.showDialog(Controllers.INSTANCE.getStage());
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
property.setValue(selectedDir.getAbsolutePath());
chooser.titleProperty().unbind();

View File

@@ -20,7 +20,7 @@ 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.MainKt;
import org.jackhuang.hmcl.Main;
import javax.swing.UIManager;
import java.util.Optional;
@@ -29,7 +29,7 @@ public final class MessageBox {
private MessageBox() {
}
private static final String TITLE = MainKt.i18n("message.info");
private static final String TITLE = Main.i18n("message.info");
/**
* User Operation: Yes

View File

@@ -0,0 +1,157 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.io.File;
import java.util.Collection;
public class MultiFileItem extends ComponentList {
private final StringProperty customText = new SimpleStringProperty(this, "customText", "Custom");
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", "Select a file");
private final ToggleGroup group = new ToggleGroup();
private final JFXTextField txtCustom = new JFXTextField();
private final JFXButton btnSelect = new JFXButton();
private final JFXRadioButton radioCustom = new JFXRadioButton();
private final BorderPane custom = new BorderPane();
private final VBox pane = new VBox();
{
BorderPane.setAlignment(txtCustom, Pos.CENTER_RIGHT);
btnSelect.setGraphic(SVG.folderOpen("black", 15, 15));
btnSelect.setOnMouseClicked(e -> {
// TODO
});
radioCustom.textProperty().bind(customTextProperty());
radioCustom.setToggleGroup(group);
txtCustom.disableProperty().bind(radioCustom.selectedProperty().not());
btnSelect.disableProperty().bind(radioCustom.selectedProperty().not());
custom.setLeft(radioCustom);
custom.setStyle("-fx-padding: 3;");
HBox right = new HBox();
right.setSpacing(3);
right.getChildren().addAll(txtCustom, btnSelect);
custom.setRight(right);
FXUtils.limitHeight(custom, 20);
pane.setStyle("-fx-padding: 0 0 10 0;");
pane.setSpacing(8);
pane.getChildren().add(custom);
addChildren(pane);
}
public Node createChildren(String title) {
return createChildren(title, null);
}
public Node createChildren(String title, Object userData) {
return createChildren(title, "", userData);
}
public Node createChildren(String title, String subtitle, Object userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
FXUtils.limitHeight(pane, 20);
JFXRadioButton left = new JFXRadioButton(title);
left.setToggleGroup(group);
left.setUserData(userData);
pane.setLeft(left);
Label right = new Label(subtitle);
right.getStyleClass().add("subtitle-label");
right.setStyle("-fx-font-size: 10;");
pane.setRight(right);
return pane;
}
public void loadChildren(Collection<Node> list) {
pane.getChildren().setAll(list);
pane.getChildren().add(custom);
}
public void onExploreJavaDir() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(Main.i18n(getChooserTitle()));
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
txtCustom.setText(selectedDir.getAbsolutePath());
}
public ToggleGroup getGroup() {
return group;
}
public String getCustomText() {
return customText.get();
}
public StringProperty customTextProperty() {
return customText;
}
public void setCustomText(String customText) {
this.customText.set(customText);
}
public String getChooserTitle() {
return chooserTitle.get();
}
public StringProperty chooserTitleProperty() {
return chooserTitle;
}
public void setChooserTitle(String chooserTitle) {
this.chooserTitle.set(chooserTitle);
}
public void setCustomUserData(Object userData) {
radioCustom.setUserData(userData);
}
public JFXRadioButton getRadioCustom() {
return radioCustom;
}
public JFXTextField getTxtCustom() {
return txtCustom;
}
}

View File

@@ -32,6 +32,11 @@ public class NumberValidator extends ValidatorBase {
this.nullable = nullable;
}
public NumberValidator(String message, boolean nullable) {
super(message);
this.nullable = nullable;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
@@ -43,7 +48,7 @@ public class NumberValidator extends ValidatorBase {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(false);
hasErrors.set(nullable);
else
try {
Integer.parseInt(textField.getText());

View File

@@ -0,0 +1,193 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.NamedArg;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.Lang;
@DefaultProperty("container")
public class RipplerContainer extends StackPane {
private final ObjectProperty<Node> container = new SimpleObjectProperty<>(this, "container", null);
private final ObjectProperty<Paint> ripplerFill = new SimpleObjectProperty<>(this, "ripplerFill", null);
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false);
private final StackPane buttonContainer = new StackPane();
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
@Override
protected Node getMask() {
StackPane mask = new StackPane();
mask.shapeProperty().bind(buttonContainer.shapeProperty());
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(new BackgroundFill(Color.WHITE, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getRadii() : defaultRadii, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)), buttonContainer.backgroundProperty()));
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset());
return mask;
}
@Override
protected void initListeners() {
this.ripplerPane.setOnMousePressed(event -> {
if (releaseManualRippler != null)
releaseManualRippler.run();
releaseManualRippler = null;
createRipple(event.getX(), event.getY());
});
}
};
private Transition clickedAnimation;
private final CornerRadii defaultRadii = new CornerRadii(3);
private Runnable releaseManualRippler;
public RipplerContainer(@NamedArg("container") Node container) {
setContainer(container);
getStyleClass().add("rippler-container");
buttonContainer.getChildren().add(buttonRippler);
setOnMousePressed(event -> {
if (clickedAnimation != null) {
clickedAnimation.setRate(1);
clickedAnimation.play();
}
});
setOnMouseReleased(event -> {
if (clickedAnimation != null) {
clickedAnimation.setRate(-1);
clickedAnimation.play();
}
});
focusedProperty().addListener((a, b, newValue) -> {
if (newValue) {
if (!isPressed())
buttonRippler.showOverlay();
} else {
buttonRippler.hideOverlay();
}
});
pressedProperty().addListener(o -> buttonRippler.hideOverlay());
setPickOnBounds(false);
buttonContainer.setPickOnBounds(false);
buttonContainer.shapeProperty().bind(shapeProperty());
buttonContainer.borderProperty().bind(borderProperty());
buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
if (getBackground() == null || isJavaDefaultBackground(getBackground()) || isJavaDefaultClickedBackground(getBackground()))
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
try {
return new Background(new BackgroundFill(getBackground() != null ? getBackground().getFills().get(0).getFill() : Color.TRANSPARENT,
getBackground() != null ? getBackground().getFills().get(0).getRadii() : defaultRadii, Insets.EMPTY));
} catch (Exception e) {
return getBackground();
}
}, backgroundProperty()));
ripplerFillProperty().addListener((a, b, newValue) -> buttonRippler.setRipplerFill(newValue));
if (getBackground() == null || isJavaDefaultBackground(getBackground()))
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
containerProperty().addListener(o -> updateChildren());
updateChildren();
selectedProperty().addListener(o -> {
if (isSelected()) setBackground(new Background(new BackgroundFill(getRipplerFill(), defaultRadii, null)));
else setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
});
setShape(Lang.apply(new Rectangle(), rectangle -> {
rectangle.widthProperty().bind(widthProperty());
rectangle.heightProperty().bind(heightProperty());
}));
}
protected void updateChildren() {
getChildren().addAll(buttonContainer, getContainer());
for (int i = 1; i < getChildren().size(); ++i)
getChildren().get(i).setPickOnBounds(false);
}
private boolean isJavaDefaultBackground(Background background) {
try {
String firstFill = background.getFills().get(0).getFill().toString();
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffffbd".equals(firstFill);
} catch (Exception e) {
return false;
}
}
private boolean isJavaDefaultClickedBackground(Background background) {
try {
String firstFill = background.getFills().get(0).getFill().toString();
return "0x039ed3ff".equals(firstFill);
} catch (Exception e) {
return false;
}
}
public Node getContainer() {
return container.get();
}
public ObjectProperty<Node> containerProperty() {
return container;
}
public void setContainer(Node container) {
this.container.set(container);
}
public Paint getRipplerFill() {
return ripplerFill.get();
}
public ObjectProperty<Paint> ripplerFillProperty() {
return ripplerFill;
}
public void setRipplerFill(Paint ripplerFill) {
this.ripplerFill.set(ripplerFill);
}
public boolean isSelected() {
return selected.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setSelected(boolean selected) {
this.selected.set(selected);
}
}

View File

@@ -32,6 +32,12 @@ public final class Validator extends ValidatorBase {
this.validator = validator;
}
public Validator(String message, Predicate<String> validator) {
this(validator);
setMessage(message);
}
@Override
protected void eval() {
if (this.srcControl.get() instanceof TextInputControl) {

View File

@@ -24,7 +24,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Lang;
@@ -55,7 +55,7 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
this.repository = repository;
this.downloadProvider = downloadProvider;
FXUtilsKt.loadFXML(this, "/assets/fxml/download/additional-installers.fxml");
FXUtils.loadFXML(this, "/assets/fxml/download/additional-installers.fxml");
lblGameVersion.setText(provider.getGameVersion());
lblVersionName.setText(provider.getVersion().getId());

View File

@@ -0,0 +1,134 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackManifest;
import org.jackhuang.hmcl.game.MultiMCInstallVersionSettingTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import java.io.File;
import java.util.Map;
public final class DownloadWizardProvider implements WizardProvider {
private Profile profile;
@Override
public void start(Map<String, Object> settings) {
profile = Settings.INSTANCE.getSelectedProfile();
settings.put(PROFILE, profile);
}
private Task finishVersionDownloadingAsync(Map<String, Object> settings) {
GameBuilder builder = profile.getDependency().gameBuilder();
builder.name((String) settings.get("name"));
builder.gameVersion((String) settings.get("game"));
if (settings.containsKey("forge"))
builder.version("forge", (String) settings.get("forge"));
if (settings.containsKey("liteloader"))
builder.version("liteloader", (String) settings.get("liteloader"));
if (settings.containsKey("optifine"))
builder.version("optifine", (String) settings.get("optifine"));
return builder.buildAsync();
}
private Task finishModpackInstallingAsync(Map<String, Object> settings) {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null;
File selected = Lang.get(settings, ModpackPage.MODPACK_FILE, File.class, null);
Modpack modpack = Lang.get(settings, ModpackPage.MODPACK_CURSEFORGE_MANIFEST, Modpack.class, null);
String name = Lang.get(settings, ModpackPage.MODPACK_NAME, String.class, null);
if (selected == null || modpack == null || name == null) return null;
profile.getRepository().markVersionAsModpack(name);
Task finalizeTask = Task.of(() -> {
profile.getRepository().refreshVersions();
VersionSetting vs = profile.specializeVersionSetting(name);
profile.getRepository().undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
});
if (modpack.getManifest() instanceof CurseManifest)
return new CurseInstallTask(profile.getDependency(), selected, ((CurseManifest) modpack.getManifest()), name)
.with(finalizeTask);
else if (modpack.getManifest() instanceof HMCLModpackManifest)
return new HMCLModpackInstallTask(profile, selected, modpack, name)
.with(finalizeTask);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), selected, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.with(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name))
.with(finalizeTask);
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
}
@Override
public Object finish(Map<String, Object> settings) {
switch (Lang.parseInt(settings.get(InstallTypePage.INSTALL_TYPE), -1)) {
case 0: return finishVersionDownloadingAsync(settings);
case 1: return finishModpackInstallingAsync(settings);
default: return null;
}
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
DownloadProvider provider = profile.getDependency().getDownloadProvider();
switch (step) {
case 0:
return new InstallTypePage(controller);
case 1:
int subStep = Lang.parseInt(settings.get(InstallTypePage.INSTALL_TYPE), -1);
switch (subStep) {
case 0:
return new VersionsPage(controller, "", provider, "game", () -> controller.onNext(new InstallersPage(controller, profile.getRepository(), provider)));
case 1:
return new ModpackPage(controller);
default:
throw new IllegalStateException("Error step " + step + ", subStep " + subStep + ", settings: " + settings);
}
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings);
}
}
@Override
public boolean cancel() {
return true;
}
public static final String PROFILE = "PROFILE";
}

View File

@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXListView;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
public final class InstallTypePage extends StackPane implements WizardPage {
private final WizardController controller;
@FXML private JFXListView<Object> list;
public InstallTypePage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/dltype.fxml");
list.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALL_TYPE, list.getSelectionModel().getSelectedIndex());
controller.onNext();
});
}
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(INSTALL_TYPE);
}
@Override
public String getTitle() {
return "Select an operation";
}
public static final String INSTALL_TYPE = "INSTALL_TYPE";
}

View File

@@ -89,9 +89,7 @@ public final class InstallerWizardProvider implements WizardProvider {
if (settings.containsKey("optifine"))
ret = ret.with(profile.getDependency().installLibraryAsync(gameVersion, version, "optifine", (String) settings.get("optifine")));
return ret.with(Task.of(v -> {
profile.getRepository().refreshVersions();
}));
return ret.with(Task.of(profile.getRepository()::refreshVersions));
}
@Override

View File

@@ -0,0 +1,137 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Map;
public class InstallersPage extends StackPane implements WizardPage {
private final WizardController controller;
private final GameRepository repository;
private final DownloadProvider downloadProvider;
@FXML
private VBox list;
@FXML
private JFXButton btnForge;
@FXML
private JFXButton btnLiteLoader;
@FXML
private JFXButton btnOptiFine;
@FXML
private Label lblGameVersion;
@FXML
private Label lblForge;
@FXML
private Label lblLiteLoader;
@FXML
private Label lblOptiFine;
@FXML
private JFXTextField txtName;
@FXML
private JFXButton btnInstall;
public InstallersPage(WizardController controller, GameRepository repository, DownloadProvider downloadProvider) {
this.controller = controller;
this.repository = repository;
this.downloadProvider = downloadProvider;
FXUtils.loadFXML(this, "/assets/fxml/download/installers.fxml");
String gameVersion = (String) controller.getSettings().get("game");
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(Main.i18n("version.already_exists"));
txtName.getValidators().add(hasVersion);
txtName.textProperty().addListener(e -> btnInstall.setDisable(!txtName.validate()));
txtName.setText(gameVersion);
btnForge.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 0);
controller.onNext(new VersionsPage(controller, gameVersion, downloadProvider, "forge", () -> controller.onPrev(false)));
});
btnLiteLoader.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 1);
controller.onNext(new VersionsPage(controller, gameVersion, downloadProvider, "liteloader", () -> controller.onPrev(false)));
});
btnOptiFine.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 2);
controller.onNext(new VersionsPage(controller, gameVersion, downloadProvider, "optifine", () -> controller.onPrev(false)));
});
}
@Override
public String getTitle() {
return "Choose a game version";
}
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText("Current Game Version: " + controller.getSettings().get("game"));
if (controller.getSettings().containsKey("forge"))
lblForge.setText("Forge Version: " + controller.getSettings().get("forge"));
else
lblForge.setText("Forge not installed");
if (controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText("LiteLoader Version: " + controller.getSettings().get("liteloader"));
else
lblLiteLoader.setText("LiteLoader not installed");
if (controller.getSettings().containsKey("optifine"))
lblOptiFine.setText("OptiFine Version: " + controller.getSettings().get("optifine"));
else
lblOptiFine.setText("OptiFine not installed");
}
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(INSTALLER_TYPE);
}
public void onInstall() {
controller.getSettings().put("name", txtName.getText());
controller.onFinish();
}
public static final String INSTALLER_TYPE = "INSTALLER_TYPE";
}

View File

@@ -0,0 +1,133 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebStage;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Map;
public final class ModpackPage extends StackPane implements WizardPage {
private final WizardController controller;
private Modpack manifest = null;
@FXML
private Region borderPane;
@FXML
private Label lblName;
@FXML
private Label lblVersion;
@FXML
private Label lblAuthor;
@FXML
private Label lblModpackLocation;
@FXML
private JFXTextField txtModpackName;
@FXML
private JFXButton btnInstall;
public ModpackPage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/modpack.fxml");
Profile profile = (Profile) controller.getSettings().get("PROFILE");
FileChooser chooser = new FileChooser();
chooser.setTitle(Main.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) Platform.runLater(controller::onFinish);
else {
// TODO: original HMCL modpack support.
controller.getSettings().put(MODPACK_FILE, selectedFile);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
txtModpackName.getValidators().add(new Validator(Main.i18n("version.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)));
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
try {
manifest = ModpackHelper.readModpackManifest(selectedFile);
controller.getSettings().put(MODPACK_CURSEFORGE_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
} catch (Exception e) {
// TODO
txtModpackName.setText(Main.i18n("modpack.task.install.error"));
}
}
//FXUtils.limitHeight(borderPane, 100.0);
FXUtils.limitWidth(borderPane, 500.0);
}
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(MODPACK_FILE);
}
public void onInstall() {
if (!txtModpackName.validate()) return;
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
controller.onFinish();
}
public void onDescribe() {
if (manifest != null) {
WebStage stage = new WebStage();
stage.getWebView().getEngine().loadContent(manifest.getDescription());
stage.setTitle(Main.i18n("modpack.wizard.step.3"));
stage.showAndWait();
}
}
@Override
public String getTitle() {
return Main.i18n("modpack.task.install");
}
public static final String MODPACK_FILE = "MODPACK_FILE";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_CURSEFORGE_MANIFEST = "CURSEFORGE_MANIFEST";
}

View File

@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
@@ -59,7 +59,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
this.versionList = downloadProvider.getVersionListById(libraryId);
FXUtilsKt.loadFXML(this, "/assets/fxml/download/versions.fxml");
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
getChildren().setAll(spinner);
list.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
controller.getSettings().put(libraryId, newValue.getRemoteVersion().getSelfVersion());
@@ -70,7 +70,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@Override
public void refresh() {
executor = versionList.refreshAsync(downloadProvider).subscribe(Schedulers.javafx(), v -> {
executor = versionList.refreshAsync(downloadProvider).subscribe(Schedulers.javafx(), () -> {
versionList.getVersions(gameVersion).stream()
.sorted(RemoteVersion.RemoteVersionComparator.INSTANCE)
.forEach(version -> {

View File

@@ -21,7 +21,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
/**
* @author huangyuhui
@@ -36,7 +36,7 @@ public final class VersionsPageItem extends StackPane {
public VersionsPageItem(RemoteVersion<?> remoteVersion) {
this.remoteVersion = remoteVersion;
FXUtilsKt.loadFXML(this, "/assets/fxml/download/versions-list-item.fxml");
FXUtils.loadFXML(this, "/assets/fxml/download/versions-list-item.fxml");
lblSelfVersion.setText(remoteVersion.getSelfVersion());
lblGameVersion.setText(remoteVersion.getGameVersion());
}

View File

@@ -25,10 +25,10 @@ import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.MainKt;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.game.ModAdviser;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -59,7 +59,7 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
this.version = version;
this.adviser = adviser;
FXUtilsKt.loadFXML(this, "/assets/fxml/modpack/selection.fxml");
FXUtils.loadFXML(this, "/assets/fxml/modpack/selection.fxml");
rootNode = getTreeItem(profile.getRepository().getRunDirectory(version), "minecraft");
treeView.setRoot(rootNode);
treeView.setSelectionModel(NoneMultipleSelectionModel.getInstance());
@@ -146,23 +146,23 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
@Override
public String getTitle() {
return MainKt.i18n("modpack.wizard.step.2.title");
return Main.i18n("modpack.wizard.step.2.title");
}
public static final String MODPACK_FILE_SELECTION = "modpack.accepted";
private static final Map<String, String> TRANSLATION = Lang.mapOf(
new Pair<>("minecraft/servers.dat", MainKt.i18n("modpack.files.servers_dat")),
new Pair<>("minecraft/saves", MainKt.i18n("modpack.files.saves")),
new Pair<>("minecraft/mods", MainKt.i18n("modpack.files.mods")),
new Pair<>("minecraft/config", MainKt.i18n("modpack.files.config")),
new Pair<>("minecraft/liteconfig", MainKt.i18n("modpack.files.liteconfig")),
new Pair<>("minecraft/resourcepacks", MainKt.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/resources", MainKt.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/options.txt", MainKt.i18n("modpack.files.options_txt")),
new Pair<>("minecraft/optionsshaders.txt", MainKt.i18n("modpack.files.optionsshaders_txt")),
new Pair<>("minecraft/mods/VoxelMods", MainKt.i18n("modpack.files.mods.voxelmods")),
new Pair<>("minecraft/dumps", MainKt.i18n("modpack.files.dumps")),
new Pair<>("minecraft/blueprints", MainKt.i18n("modpack.files.blueprints")),
new Pair<>("minecraft/scripts", MainKt.i18n("modpack.files.scripts"))
new Pair<>("minecraft/servers.dat", Main.i18n("modpack.files.servers_dat")),
new Pair<>("minecraft/saves", Main.i18n("modpack.files.saves")),
new Pair<>("minecraft/mods", Main.i18n("modpack.files.mods")),
new Pair<>("minecraft/config", Main.i18n("modpack.files.config")),
new Pair<>("minecraft/liteconfig", Main.i18n("modpack.files.liteconfig")),
new Pair<>("minecraft/resourcepacks", Main.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/resources", Main.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/options.txt", Main.i18n("modpack.files.options_txt")),
new Pair<>("minecraft/optionsshaders.txt", Main.i18n("modpack.files.optionsshaders_txt")),
new Pair<>("minecraft/mods/VoxelMods", Main.i18n("modpack.files.mods.voxelmods")),
new Pair<>("minecraft/dumps", Main.i18n("modpack.files.dumps")),
new Pair<>("minecraft/blueprints", Main.i18n("modpack.files.blueprints")),
new Pair<>("minecraft/scripts", Main.i18n("modpack.files.scripts"))
);
}

View File

@@ -26,11 +26,11 @@ import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.MainKt;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtilsKt;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -55,8 +55,8 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
public ModpackInfoPage(WizardController controller, String version) {
this.controller = controller;
FXUtilsKt.loadFXML(this, "/assets/fxml/modpack/info.fxml");
FXUtilsKt.smoothScrolling(scroll);
FXUtils.loadFXML(this, "/assets/fxml/modpack/info.fxml");
FXUtils.smoothScrolling(scroll);
txtModpackName.setText(version);
txtModpackName.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.textProperty().addListener(e -> checkValidation());
@@ -71,11 +71,11 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
public void onNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(MainKt.i18n("modpack.wizard.step.initialization.save"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(MainKt.i18n("modpack"), "*.zip"));
File file = fileChooser.showSaveDialog(Controllers.INSTANCE.getStage());
fileChooser.setTitle(Main.i18n("modpack.wizard.step.initialization.save"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("modpack"), "*.zip"));
File file = fileChooser.showSaveDialog(Controllers.getStage());
if (file == null) {
Controllers.INSTANCE.navigate(null);
Controllers.navigate(null);
return;
}
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
@@ -99,7 +99,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return MainKt.i18n("modpack.wizard.step.1.title");
return Main.i18n("modpack.wizard.step.1.title");
}
public static final String MODPACK_NAME = "modpack.name";

View File

@@ -0,0 +1,150 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.wizard;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.util.Lang;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
public interface AbstractWizardDisplayer extends WizardDisplayer {
WizardController getWizardController();
Queue<Object> getCancelQueue();
@Override
default void handleDeferredWizardResult(Map<String, Object> settings, DeferredWizardResult deferredWizardResult) {
VBox vbox = new VBox();
JFXProgressBar progressBar = new JFXProgressBar();
Label label = new Label();
progressBar.setMaxHeight(10);
vbox.getChildren().addAll(progressBar, label);
StackPane root = new StackPane();
root.getChildren().add(vbox);
navigateTo(root, Navigation.NavigationDirection.FINISH);
getCancelQueue().add(Lang.thread(() -> {
deferredWizardResult.start(settings, new ResultProgressHandle() {
private boolean running = true;
@Override
public void setProgress(int currentStep, int totalSteps) {
progressBar.setProgress(1.0 * currentStep / totalSteps);
}
@Override
public void setProgress(String description, int currentStep, int totalSteps) {
label.setText(description);
progressBar.setProgress(1.0 * currentStep / totalSteps);
}
@Override
public void setBusy(String description) {
progressBar.setProgress(JFXProgressBar.INDETERMINATE_PROGRESS);
}
@Override
public void finished(Object result) {
running = false;
}
@Override
public void failed(String message, boolean canNavigateBack) {
running = false;
}
@Override
public boolean isRunning() {
return running;
}
});
Platform.runLater(this::navigateToSuccess);
}));
}
@Override
default void handleTask(Map<String, Object> settings, Task task) {
VBox vbox = new VBox();
JFXProgressBar progressBar = new JFXProgressBar();
Label label = new Label();
progressBar.setMaxHeight(10);
vbox.getChildren().addAll(progressBar, label);
StackPane root = new StackPane();
root.getChildren().add(vbox);
navigateTo(root, Navigation.NavigationDirection.FINISH);
AtomicInteger finishedTasks = new AtomicInteger(0);
TaskExecutor executor = task.with(Task.of(Schedulers.javafx(), this::navigateToSuccess)).executor(e -> new TaskListener() {
@Override
public void onReady(Task task) {
Platform.runLater(() -> progressBar.setProgress(finishedTasks.get() * 1.0 / e.getRunningTasks()));
}
@Override
public void onFinished(Task task) {
Platform.runLater(() -> {
label.setText(task.getName());
progressBar.setProgress(finishedTasks.incrementAndGet() * 1.0 / e.getRunningTasks());
});
}
@Override
public void onFailed(Task task, Throwable throwable) {
Platform.runLater(() -> {
label.setText(task.getName());
progressBar.setProgress(finishedTasks.incrementAndGet() * 1.0 / e.getRunningTasks());
});
}
@Override
public void onTerminate() {
Platform.runLater(AbstractWizardDisplayer.this::navigateToSuccess);
}
});
getCancelQueue().add(executor);
executor.start();
}
@Override
default void onCancel() {
while (!getCancelQueue().isEmpty()) {
Object x = getCancelQueue().poll();
if (x instanceof TaskExecutor) ((TaskExecutor) x).cancel();
else if (x instanceof Thread) ((Thread) x).interrupt();
else throw new IllegalStateException("Unrecognized cancel queue element: " + x);
}
}
default void navigateToSuccess() {
navigateTo(new Label("Successful"), Navigation.NavigationDirection.FINISH);
}
}

View File

@@ -0,0 +1,115 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.wizard;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXToolbar;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DefaultWizardDisplayer extends StackPane implements AbstractWizardDisplayer {
private final String prefix;
private final WizardController wizardController;
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private Node nowPage;
private TransitionHandler transitionHandler;
@FXML
private StackPane root;
@FXML
private JFXButton backButton;
@FXML
private JFXToolbar toolbar;
@FXML
private JFXButton refreshButton;
@FXML
private Label titleLabel;
public DefaultWizardDisplayer(String prefix, WizardProvider wizardProvider) {
this.prefix = prefix;
FXUtils.loadFXML(this, "/assets/fxml/wizard.fxml");
toolbar.setEffect(null);
wizardController = new WizardController(this);
wizardController.setProvider(wizardProvider);
}
@Override
public WizardController getWizardController() {
return wizardController;
}
@Override
public Queue<Object> getCancelQueue() {
return cancelQueue;
}
@Override
public void onStart() {
}
@Override
public void onEnd() {
}
@Override
public void onCancel() {
}
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
backButton.setDisable(!wizardController.canPrev());
transitionHandler.setContent(page, nav.getAnimation().getAnimationProducer());
String title = StringUtils.isBlank(prefix) ? "" : prefix + " - ";
if (page instanceof WizardPage)
titleLabel.setText(title + ((WizardPage) page).getTitle());
refreshButton.setVisible(page instanceof Refreshable);
nowPage = page;
}
public void initialize() {
transitionHandler = new TransitionHandler(root);
wizardController.onStart();
}
public void back() {
wizardController.onPrev(true);
}
public void close() {
wizardController.onCancel();
Controllers.navigate(null);
}
public void refresh() {
((Refreshable) nowPage).refresh();
}
}

View File

@@ -29,7 +29,7 @@ public final class Summary {
private final Object result;
public Summary(String[] items, Object result) {
JFXListView view = new JFXListView<String>();
JFXListView<String> view = new JFXListView<>();
view.getItems().addAll(items);
this.component = view;

View File

@@ -0,0 +1,125 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.task.Task;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
public class WizardController implements Navigation {
private final WizardDisplayer displayer;
private WizardProvider provider = null;
private final Map<String, Object> settings = new HashMap<>();
private final Stack<Node> pages = new Stack<>();
public WizardController(WizardDisplayer displayer) {
this.displayer = displayer;
}
public Map<String, Object> getSettings() {
return settings;
}
public WizardDisplayer getDisplayer() {
return displayer;
}
public void setProvider(WizardProvider provider) {
this.provider = provider;
}
@Override
public void onStart() {
Objects.requireNonNull(provider);
settings.clear();
provider.start(settings);
pages.clear();
Node page = navigatingTo(0);
pages.push(page);
if (page instanceof WizardPage)
((WizardPage) page).onNavigate(settings);
displayer.onStart();
displayer.navigateTo(page, NavigationDirection.START);
}
@Override
public void onNext() {
onNext(navigatingTo(pages.size()));
}
public void onNext(Node page) {
pages.push(page);
if (page instanceof WizardPage)
((WizardPage) page).onNavigate(settings);
displayer.navigateTo(page, NavigationDirection.NEXT);
}
@Override
public void onPrev(boolean cleanUp) {
Node page = pages.pop();
if (cleanUp && page instanceof WizardPage)
((WizardPage) page).cleanup(settings);
Node prevPage = pages.peek();
if (prevPage instanceof WizardPage)
((WizardPage) prevPage).onNavigate(settings);
displayer.navigateTo(prevPage, NavigationDirection.PREVIOUS);
}
@Override
public boolean canPrev() {
return pages.size() > 1;
}
@Override
public void onFinish() {
Object result = provider.finish(settings);
if (result instanceof DeferredWizardResult) displayer.handleDeferredWizardResult(settings, ((DeferredWizardResult) result));
else if (result instanceof Summary) displayer.navigateTo(((Summary) result).getComponent(), NavigationDirection.NEXT);
else if (result instanceof Task) displayer.handleTask(settings, ((Task) result));
else throw new IllegalStateException("Unrecognized wizard result: " + result);
}
@Override
public void onEnd() {
settings.clear();
pages.clear();
displayer.onEnd();
}
@Override
public void onCancel() {
displayer.onCancel();
onEnd();
}
protected Node navigatingTo(int step) {
return provider.createPage(this, step, settings);
}
}

View File

@@ -39,7 +39,7 @@ import java.util.zip.GZIPInputStream;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.MainKt;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -100,24 +100,24 @@ public class AppDataUpgrader extends IUpgrader {
if (!(ver instanceof IntVersionNumber))
return;
IntVersionNumber version = (IntVersionNumber) ver;
checker.requestDownloadLink().then(Task.of(v -> {
Map<String, String> map = v.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID);
checker.requestDownloadLink().then(Task.of(variables -> {
Map<String, String> map = variables.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID);
if (MessageBox.confirm(MainKt.i18n("update.newest_version") + version.get(0) + "." + version.get(1) + "." + version.get(2) + "\n"
+ MainKt.i18n("update.should_open_link"),
if (MessageBox.confirm(Main.i18n("update.newest_version") + version.get(0) + "." + version.get(1) + "." + version.get(2) + "\n"
+ Main.i18n("update.should_open_link"),
MessageBox.YES_NO_OPTION) == MessageBox.YES_OPTION)
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
Controllers.INSTANCE.dialog(MainKt.i18n("ui.message.downloading"));
Controllers.dialog(Main.i18n("ui.message.downloading"));
if (new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash).test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
Controllers.INSTANCE.closeDialog();
Controllers.closeDialog();
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
@@ -126,13 +126,13 @@ public class AppDataUpgrader extends IUpgrader {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
Controllers.INSTANCE.dialog(MainKt.i18n("ui.message.downloading"));
Controllers.dialog(Main.i18n("ui.message.downloading"));
if (new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash).test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
Controllers.INSTANCE.closeDialog();
Controllers.closeDialog();
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
@@ -150,7 +150,7 @@ public class AppDataUpgrader extends IUpgrader {
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(MainKt.i18n("update.no_browser"));
MessageBox.show(Main.i18n("update.no_browser"));
}
}
})).start();

View File

@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.MainKt;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.util.Charsets;
@@ -53,7 +53,7 @@ public class NewFileUpgrader extends IUpgrader {
URL url = requestDownloadLink();
if (url == null) return;
File newf = new File(url.getFile());
Controllers.INSTANCE.dialog(MainKt.i18n("ui.message.downloading"));
Controllers.dialog(Main.i18n("ui.message.downloading"));
if (new FileDownloadTask(url, newf).test()) {
try {
new ProcessBuilder(newf.getCanonicalPath(), "--removeOldLauncher", getRealPath())
@@ -64,7 +64,7 @@ public class NewFileUpgrader extends IUpgrader {
}
System.exit(0);
}
Controllers.INSTANCE.closeDialog();
Controllers.closeDialog();
}
private static String getRealPath() {

View File

@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.upgrade;
import com.google.gson.JsonSyntaxException;
import org.jackhuang.hmcl.MainKt;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.OutOfDateEvent;
@@ -77,7 +77,7 @@ public final class UpdateChecker {
if (value == null) {
Logging.LOG.warning("Failed to check update...");
if (showMessage)
MessageBox.show(MainKt.i18n("update.failed"));
MessageBox.show(Main.i18n("update.failed"));
} else if (base.compareTo(value) < 0)
outOfDate = true;
if (outOfDate)

View File

@@ -1,102 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl
import javafx.application.Application
import javafx.application.Platform
import javafx.stage.Stage
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Schedulers
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.runOnUiThread
import org.jackhuang.hmcl.util.Constants
import org.jackhuang.hmcl.util.Logging.LOG
import org.jackhuang.hmcl.util.NetworkUtils
import org.jackhuang.hmcl.util.OperatingSystem
import java.io.File
import java.util.logging.Level
fun i18n(key: String): String {
try {
return Main.RESOURCE_BUNDLE.getString(key)
} catch (e: Exception) {
LOG.log(Level.SEVERE, "Cannot find key $key in resource bundle", e)
return key
}
}
class Main : Application() {
override fun start(stage: Stage) {
// When launcher visibility is set to "hide and reopen" without [Platform.implicitExit] = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false)
Controllers.initialize(stage)
stage.isResizable = false
stage.scene = Controllers.scene
stage.show()
}
companion object {
@JvmField val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
@JvmField val NAME = "HMCL"
@JvmField val TITLE = "$NAME $VERSION"
@JvmField val APPDATA = getWorkingDirectory("hmcl")
@JvmStatic
fun main(args: Array<String>) {
NetworkUtils.setUserAgentSupplier { "Hello Minecraft! Launcher" }
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
launch(Main::class.java, *args)
}
@JvmStatic
fun getWorkingDirectory(folder: String): File {
val userhome = System.getProperty("user.home", ".")
return when (OperatingSystem.CURRENT_OS) {
OperatingSystem.LINUX -> File(userhome, ".$folder/")
OperatingSystem.WINDOWS -> {
val appdata: String? = System.getenv("APPDATA")
File(appdata ?: userhome, ".$folder/")
}
OperatingSystem.OSX -> File(userhome, "Library/Application Support/" + folder)
else -> File(userhome, "$folder/")
}
}
@JvmStatic
fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft")
fun stop() = runOnUiThread {
stopWithoutJavaFXPlatform()
Platform.exit()
}
fun stopWithoutJavaFXPlatform() = runOnUiThread {
Controllers.stage.close()
Schedulers.shutdown()
}
val RESOURCE_BUNDLE = Settings.INSTANCE.locale.resourceBundle
}
}

View File

@@ -1,128 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXProgressBar
import com.jfoenix.controls.JFXRadioButton
import javafx.beans.binding.Bindings
import javafx.fxml.FXML
import javafx.geometry.Rectangle2D
import javafx.scene.control.Label
import javafx.scene.control.ToggleGroup
import javafx.scene.effect.BlurType
import javafx.scene.effect.DropShadow
import javafx.scene.image.ImageView
import javafx.scene.layout.HBox
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.game.AccountHelper
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Schedulers
import java.util.concurrent.Callable
class AccountItem(i: Int, val account: Account, group: ToggleGroup) : StackPane() {
@FXML lateinit var icon: Pane
@FXML lateinit var content: VBox
@FXML lateinit var header: StackPane
@FXML lateinit var body: StackPane
@FXML lateinit var btnDelete: JFXButton
@FXML lateinit var btnRefresh: JFXButton
@FXML lateinit var lblUser: Label
@FXML lateinit var chkSelected: JFXRadioButton
@FXML lateinit var lblType: Label
@FXML lateinit var pgsSkin: JFXProgressBar
@FXML lateinit var portraitView: ImageView
@FXML lateinit var buttonPane: HBox
init {
loadFXML("/assets/fxml/account-item.fxml")
limitWidth(160.0)
limitHeight(156.0)
effect = DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -0.5, 1.0)
chkSelected.toggleGroup = group
btnDelete.graphic = SVG.delete("black", 15.0, 15.0)
btnRefresh.graphic = SVG.refresh("black", 15.0, 15.0)
// create content
val headerColor = getDefaultColor(i % 12)
header.style = "-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor
// create image view
icon.translateYProperty().bind(Bindings.createDoubleBinding(Callable { header.boundsInParent.height - icon.height / 2 - 32.0 }, header.boundsInParentProperty(), icon.heightProperty()))
chkSelected.properties["account"] = account
chkSelected.isSelected = Settings.INSTANCE.selectedAccount == account
lblUser.text = account.username
lblType.text = accountType(account)
if (account is YggdrasilAccount) {
btnRefresh.setOnMouseClicked {
pgsSkin.isVisible = true
AccountHelper.refreshSkinAsync(account)
.subscribe(Schedulers.javafx()) { loadSkin() }
}
AccountHelper.loadSkinAsync(account)
.subscribe(Schedulers.javafx()) { loadSkin() }
}
if (account is OfflineAccount) { // Offline Account cannot be refreshed,
buttonPane.children -= btnRefresh
}
}
fun loadSkin() {
if (account !is YggdrasilAccount)
return
pgsSkin.isVisible = false
portraitView.viewport = AccountHelper.getViewport(4.0)
portraitView.image = AccountHelper.getSkin(account, 4.0)
portraitView.limitSize(32.0, 32.0)
}
private fun getDefaultColor(i: Int): String {
var color = "#FFFFFF"
when (i) {
0 -> color = "#8F3F7E"
1 -> color = "#B5305F"
2 -> color = "#CE584A"
3 -> color = "#DB8D5C"
4 -> color = "#DA854E"
5 -> color = "#E9AB44"
6 -> color = "#FEE435"
7 -> color = "#99C286"
8 -> color = "#01A05E"
9 -> color = "#4A8895"
10 -> color = "#16669B"
11 -> color = "#2F65A5"
12 -> color = "#4E6A9C"
else -> {
}
}
return color
}
}

View File

@@ -1,169 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.*
import javafx.application.Platform
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.control.ToggleGroup
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.MultiCharacterSelector
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.OfflineAccountFactory
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Schedulers
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.onChange
import org.jackhuang.hmcl.util.onChangeAndOperate
import org.jackhuang.hmcl.util.taskResult
class AccountsPage() : StackPane(), DecoratorPage {
private val titleProperty: StringProperty = SimpleStringProperty(this, "title", "Accounts")
override fun titleProperty() = titleProperty
@FXML lateinit var scrollPane: ScrollPane
@FXML lateinit var masonryPane: JFXMasonryPane
@FXML lateinit var dialog: JFXDialog
@FXML lateinit var txtUsername: JFXTextField
@FXML lateinit var txtPassword: JFXPasswordField
@FXML lateinit var lblCreationWarning: Label
@FXML lateinit var cboType: JFXComboBox<String>
@FXML lateinit var progressBar: JFXProgressBar
init {
loadFXML("/assets/fxml/account.fxml")
children.remove(dialog)
dialog.dialogContainer = this
scrollPane.smoothScrolling()
txtUsername.setValidateWhileTextChanged()
txtPassword.setValidateWhileTextChanged()
cboType.selectionModel.selectedIndexProperty().onChange {
val visible = it != 0
txtPassword.isVisible = visible
}
cboType.selectionModel.select(0)
txtPassword.setOnAction { onCreationAccept() }
txtUsername.setOnAction { onCreationAccept() }
Settings.INSTANCE.selectedAccountProperty().onChangeAndOperate { account ->
masonryPane.children.forEach { node ->
if (node is AccountItem) {
node.chkSelected.isSelected = account?.username == node.lblUser.text
}
}
}
loadAccounts()
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount()
}
fun loadAccounts() {
val children = mutableListOf<Node>()
var i = 0
val group = ToggleGroup()
for ((_, account) in Settings.INSTANCE.getAccounts()) {
children += buildNode(++i, account, group)
}
group.selectedToggleProperty().onChange {
if (it != null)
Settings.INSTANCE.selectedAccount = it.properties["account"] as Account
}
masonryPane.resetChildren(children)
Platform.runLater {
masonryPane.requestLayout()
scrollPane.requestLayout()
}
}
private fun buildNode(i: Int, account: Account, group: ToggleGroup): Node {
return AccountItem(i, account, group).apply {
btnDelete.setOnMouseClicked {
Settings.INSTANCE.deleteAccount(account.username)
Platform.runLater(this@AccountsPage::loadAccounts)
}
}
}
fun addNewAccount() {
txtUsername.text = ""
txtPassword.text = ""
dialog.show()
}
fun onCreationAccept() {
val type = cboType.selectionModel.selectedIndex
val username = txtUsername.text
val password = txtPassword.text
progressBar.isVisible = true
lblCreationWarning.text = ""
taskResult("create_account") {
try {
val account = when (type) {
0 -> OfflineAccountFactory.INSTANCE.fromUsername(username)
1 -> YggdrasilAccountFactory.INSTANCE.fromUsername(username, password)
else -> throw UnsupportedOperationException()
}
account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.proxy)
account
} catch (e: Exception) {
e
}
}.subscribe(Schedulers.javafx()) {
val account: Any = it["create_account"]
if (account is Account) {
Settings.INSTANCE.addAccount(account)
dialog.close()
loadAccounts()
} else if (account is InvalidCredentialsException) {
lblCreationWarning.text = i18n("login.wrong_password")
} else if (account is Exception) {
lblCreationWarning.text = account.localizedMessage
}
progressBar.isVisible = false
}
}
fun onCreationCancel() {
dialog.close()
}
}
fun accountType(account: Account) =
when(account) {
is OfflineAccount -> i18n("login.methods.offline")
is YggdrasilAccount -> i18n("login.methods.yggdrasil")
else -> throw Error("${i18n("login.methods.no_method")}: $account")
}

View File

@@ -1,55 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import javafx.scene.Node
import javafx.scene.control.ScrollPane
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
class AdvancedListBox: ScrollPane() {
val container = VBox()
init {
content = container
smoothScrolling()
isFitToHeight = true
isFitToWidth = true
hbarPolicy = ScrollBarPolicy.NEVER
container.spacing = 5.0
container.styleClass += "advanced-list-box-content"
}
fun add(child: Node): AdvancedListBox {
if (child is Pane) {
container.children += child
} else {
val pane = StackPane()
pane.styleClass += "advanced-list-box-item"
pane.children.setAll(child)
container.children += pane
}
return this
}
fun startCategory(category: String): AdvancedListBox = add(ClassTitle(category))
}

View File

@@ -1,81 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXDialog
import javafx.scene.Node
import javafx.scene.Scene
import javafx.scene.image.Image
import javafx.scene.layout.Region
import javafx.stage.Stage
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.util.JavaVersion
import org.jackhuang.hmcl.util.task
object Controllers {
lateinit var scene: Scene private set
lateinit var stage: Stage private set
val mainPane = MainPage()
val settingsPane by lazy { SettingsPage() }
val versionPane by lazy { VersionPage() }
lateinit var leftPaneController: LeftPaneController
lateinit var decorator: Decorator
fun initialize(stage: Stage) {
this.stage = stage
decorator = Decorator(stage, mainPane, Main.TITLE, max = false)
decorator.showPage(null)
leftPaneController = LeftPaneController(decorator.leftPane)
Settings.INSTANCE.onProfileLoading()
task { JavaVersion.initialize() }.start()
decorator.isCustomMaximize = false
scene = Scene(decorator, 804.0, 521.0)
scene.stylesheets.addAll(*stylesheets)
stage.minWidth = 800.0
stage.maxWidth = 800.0
stage.maxHeight = 480.0
stage.minHeight = 480.0
stage.icons += Image("/assets/img/icon.png")
stage.title = Main.TITLE
}
fun dialog(content: Region): JFXDialog {
return decorator.showDialog(content)
}
fun dialog(text: String) {
dialog(MessageDialogPane(text, decorator.dialog))
}
fun closeDialog() {
decorator.dialog.close()
}
fun navigate(node: Node?) {
decorator.showPage(node)
}
}

View File

@@ -1,457 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXDialog
import com.jfoenix.controls.JFXDrawer
import com.jfoenix.controls.JFXHamburger
import com.jfoenix.effects.JFXDepthManager
import com.jfoenix.svg.SVGGlyph
import javafx.animation.Timeline
import javafx.application.Platform
import javafx.beans.property.BooleanProperty
import javafx.beans.property.ObjectProperty
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.fxml.FXML
import javafx.geometry.BoundingBox
import javafx.geometry.Bounds
import javafx.geometry.Insets
import javafx.scene.Cursor
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.control.Tooltip
import javafx.scene.input.MouseEvent
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.stage.Screen
import javafx.stage.Stage
import javafx.stage.StageStyle
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.ui.animation.AnimationProducer
import org.jackhuang.hmcl.ui.animation.ContainerAnimations
import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.wizard.*
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class Decorator @JvmOverloads constructor(private val primaryStage: Stage, private val mainPage: Node, title: String, private val max: Boolean = true, min: Boolean = true) : StackPane(), AbstractWizardDisplayer {
override val wizardController: WizardController = WizardController(this)
private var xOffset: Double = 0.0
private var yOffset: Double = 0.0
private var newX: Double = 0.0
private var newY: Double = 0.0
private var initX: Double = 0.0
private var initY: Double = 0.0
private var allowMove: Boolean = false
private var isDragging: Boolean = false
private var windowDecoratorAnimation: Timeline? = null
private var dialogShown = false
@FXML lateinit var contentPlaceHolder: StackPane
@FXML lateinit var drawerWrapper: StackPane
@FXML lateinit var titleContainer: BorderPane
@FXML lateinit var leftRootPane: BorderPane
@FXML lateinit var buttonsContainer: HBox
@FXML lateinit var backNavButton: JFXButton
@FXML lateinit var refreshNavButton: JFXButton
@FXML lateinit var closeNavButton: JFXButton
@FXML lateinit var refreshMenuButton: JFXButton
@FXML lateinit var addMenuButton: JFXButton
@FXML lateinit var titleLabel: Label
@FXML lateinit var lblTitle: Label
@FXML lateinit var leftPane: AdvancedListBox
@FXML lateinit var drawer: JFXDrawer
@FXML lateinit var titleBurgerContainer: StackPane
@FXML lateinit var titleBurger: JFXHamburger
@FXML lateinit var dialog: JFXDialog
private val onCloseButtonActionProperty: ObjectProperty<Runnable> = SimpleObjectProperty(Runnable { Main.stop() })
@JvmName("onCloseButtonActionProperty") get
var onCloseButtonAction: Runnable by onCloseButtonActionProperty
val customMaximizeProperty: BooleanProperty = SimpleBooleanProperty(false)
@JvmName("customMaximizeProperty") get
var isCustomMaximize: Boolean by customMaximizeProperty
private var maximized: Boolean = false
private var originalBox: BoundingBox? = null
private var maximizedBox: BoundingBox? = null
@FXML lateinit var btnMin: JFXButton
@FXML lateinit var btnMax: JFXButton
@FXML lateinit var btnClose: JFXButton
private val minus = SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE)
.apply { setSize(12.0, 2.0); translateY = 4.0 }
private val resizeMax = SVGGlyph(0, "RESIZE_MAX", "M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34 25-60t59-26z", Color.WHITE)
.apply { setPrefSize(12.0, 12.0); setSize(12.0, 12.0) }
private val resizeMin = SVGGlyph(0, "RESIZE_MIN", "M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z", Color.WHITE)
.apply { setPrefSize(12.0, 12.0); setSize(12.0, 12.0) }
private val close = SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE)
.apply { setPrefSize(12.0, 12.0); setSize(12.0, 12.0) }
val animationHandler: TransitionHandler
override val cancelQueue: Queue<Any> = ConcurrentLinkedQueue<Any>()
init {
loadFXML("/assets/fxml/decorator.fxml")
this.primaryStage.initStyle(StageStyle.UNDECORATED)
btnClose.graphic = close
btnMin.graphic = minus
btnMax.graphic = resizeMax
lblTitle.text = title
buttonsContainer.background = Background(*arrayOf(BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)))
titleContainer.addEventHandler(MouseEvent.MOUSE_CLICKED) { mouseEvent ->
if (mouseEvent.clickCount == 2) {
btnMax.fire()
}
}
drawerWrapper.children -= dialog
dialog.dialogContainer = drawerWrapper
dialog.setOnDialogClosed { dialogShown = false }
dialog.setOnDialogOpened { dialogShown = true }
if (!min) buttonsContainer.children.remove(btnMin)
if (!max) buttonsContainer.children.remove(btnMax)
JFXDepthManager.setDepth(titleContainer, 1)
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED) { this.allowMove = true }
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED) { if (!this.isDragging) this.allowMove = false }
animationHandler = TransitionHandler(contentPlaceHolder)
(lookup("#contentPlaceHolderRoot") as Pane).setOverflowHidden()
drawerWrapper.setOverflowHidden()
}
fun onMouseMoved(mouseEvent: MouseEvent) {
if (!this.primaryStage.isMaximized && !this.primaryStage.isFullScreen && !this.maximized) {
if (!this.primaryStage.isResizable) {
this.updateInitMouseValues(mouseEvent)
} else {
val x = mouseEvent.x
val y = mouseEvent.y
val boundsInParent = this.boundsInParent
if (this.border != null && this.border.strokes.size > 0) {
val borderWidth = this.contentPlaceHolder.snappedLeftInset()
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
this.cursor = Cursor.NE_RESIZE
} else if (y > this.height - borderWidth) {
this.cursor = Cursor.SE_RESIZE
} else {
this.cursor = Cursor.E_RESIZE
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
this.cursor = Cursor.NW_RESIZE
} else if (y > this.height - borderWidth) {
this.cursor = Cursor.SW_RESIZE
} else {
this.cursor = Cursor.W_RESIZE
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
this.cursor = Cursor.N_RESIZE
} else if (this.isBottomEdge(x, y, boundsInParent)) {
this.cursor = Cursor.S_RESIZE
} else {
this.cursor = Cursor.DEFAULT
}
this.updateInitMouseValues(mouseEvent)
}
}
} else {
this.cursor = Cursor.DEFAULT
}
}
fun onMouseReleased() {
this.isDragging = false
}
fun onMouseDragged(mouseEvent: MouseEvent) {
this.isDragging = true
if (mouseEvent.isPrimaryButtonDown && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen && !mouseEvent.isStillSincePress && !this.primaryStage.isMaximized && !this.maximized) {
this.newX = mouseEvent.screenX
this.newY = mouseEvent.screenY
val deltax = this.newX - this.initX
val deltay = this.newY - this.initY
val cursor = this.cursor
if (Cursor.E_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.width + deltax)
mouseEvent.consume()
} else if (Cursor.NE_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.height - deltay)) {
this.primaryStage.y = this.primaryStage.y + deltay
}
this.setStageWidth(this.primaryStage.width + deltax)
mouseEvent.consume()
} else if (Cursor.SE_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.width + deltax)
this.setStageHeight(this.primaryStage.height + deltay)
mouseEvent.consume()
} else if (Cursor.S_RESIZE == cursor) {
this.setStageHeight(this.primaryStage.height + deltay)
mouseEvent.consume()
} else if (Cursor.W_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.width - deltax)) {
this.primaryStage.x = this.primaryStage.x + deltax
}
mouseEvent.consume()
} else if (Cursor.SW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.width - deltax)) {
this.primaryStage.x = this.primaryStage.x + deltax
}
this.setStageHeight(this.primaryStage.height + deltay)
mouseEvent.consume()
} else if (Cursor.NW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.width - deltax)) {
this.primaryStage.x = this.primaryStage.x + deltax
}
if (this.setStageHeight(this.primaryStage.height - deltay)) {
this.primaryStage.y = this.primaryStage.y + deltay
}
mouseEvent.consume()
} else if (Cursor.N_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.height - deltay)) {
this.primaryStage.y = this.primaryStage.y + deltay
}
mouseEvent.consume()
} else if (this.allowMove) {
this.primaryStage.x = mouseEvent.screenX - this.xOffset
this.primaryStage.y = mouseEvent.screenY - this.yOffset
mouseEvent.consume()
}
}
}
}
fun onMin() {
this.primaryStage.isIconified = true
}
fun onMax() {
if (!max) return
if (!this.isCustomMaximize) {
this.primaryStage.isMaximized = !this.primaryStage.isMaximized
this.maximized = this.primaryStage.isMaximized
if (this.primaryStage.isMaximized) {
this.btnMax.graphic = resizeMin
this.btnMax.tooltip = Tooltip("Restore Down")
} else {
this.btnMax.graphic = resizeMax
this.btnMax.tooltip = Tooltip("Maximize")
}
} else {
if (!this.maximized) {
this.originalBox = BoundingBox(primaryStage.x, primaryStage.y, primaryStage.width, primaryStage.height)
val screen = Screen.getScreensForRectangle(primaryStage.x, primaryStage.y, primaryStage.width, primaryStage.height)[0] as Screen
val bounds = screen.visualBounds
this.maximizedBox = BoundingBox(bounds.minX, bounds.minY, bounds.width, bounds.height)
primaryStage.x = this.maximizedBox!!.minX
primaryStage.y = this.maximizedBox!!.minY
primaryStage.width = this.maximizedBox!!.width
primaryStage.height = this.maximizedBox!!.height
this.btnMax.graphic = resizeMin
this.btnMax.tooltip = Tooltip("Restore Down")
} else {
primaryStage.x = this.originalBox!!.minX
primaryStage.y = this.originalBox!!.minY
primaryStage.width = this.originalBox!!.width
primaryStage.height = this.originalBox!!.height
this.originalBox = null
this.btnMax.graphic = resizeMax
this.btnMax.tooltip = Tooltip("Maximize")
}
this.maximized = !this.maximized
}
}
fun onClose() {
this.onCloseButtonAction.run()
}
private fun updateInitMouseValues(mouseEvent: MouseEvent) {
this.initX = mouseEvent.screenX
this.initY = mouseEvent.screenY
this.xOffset = mouseEvent.sceneX
this.yOffset = mouseEvent.sceneY
}
@Suppress("UNUSED_PARAMETER")
private fun isRightEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return x < this.width && x > this.width - this.contentPlaceHolder.snappedLeftInset()
}
@Suppress("UNUSED_PARAMETER")
private fun isTopEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return y >= 0.0 && y < this.contentPlaceHolder.snappedLeftInset()
}
@Suppress("UNUSED_PARAMETER")
private fun isBottomEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return y < this.height && y > this.height - this.contentPlaceHolder.snappedLeftInset()
}
@Suppress("UNUSED_PARAMETER")
private fun isLeftEdge(x: Double, y: Double, boundsInParent: Bounds): Boolean {
return x >= 0.0 && x < this.contentPlaceHolder.snappedLeftInset()
}
internal fun setStageWidth(width: Double): Boolean {
if (width >= this.primaryStage.minWidth && width >= this.titleContainer.minWidth) {
this.primaryStage.width = width
this.initX = this.newX
return true
} else {
if (width >= this.primaryStage.minWidth && width <= this.titleContainer.minWidth) {
this.primaryStage.width = this.titleContainer.minWidth
}
return false
}
}
internal fun setStageHeight(height: Double): Boolean {
if (height >= this.primaryStage.minHeight && height >= this.titleContainer.height) {
this.primaryStage.height = height
this.initY = this.newY
return true
} else {
if (height >= this.primaryStage.minHeight && height <= this.titleContainer.height) {
this.primaryStage.height = this.titleContainer.height
}
return false
}
}
fun setMaximized(maximized: Boolean) {
if (this.maximized != maximized) {
Platform.runLater { this.btnMax.fire() }
}
}
private fun setContent(content: Node, animation: AnimationProducer) {
animationHandler.setContent(content, animation)
if (content is Region) {
content.setMinSize(0.0, 0.0)
content.setOverflowHidden()
}
backNavButton.isDisable = !wizardController.canPrev()
if (content is Refreshable)
refreshNavButton.isVisible = true
if (content != mainPage)
closeNavButton.isVisible = true
val prefix = if (category == null) "" else category + " - "
titleLabel.textProperty().unbind()
if (content is WizardPage)
titleLabel.text = prefix + content.title
if (content is DecoratorPage)
titleLabel.textProperty().bind(content.titleProperty())
}
var category: String? = null
var nowPage: Node? = null
fun showPage(content: Node?) {
val c = content ?: mainPage
onEnd()
val nowPage = nowPage
if (nowPage is DecoratorPage)
nowPage.onClose()
this.nowPage = content
setContent(c, ContainerAnimations.FADE.animationProducer)
if (c is Region)
// Let root pane fix window size.
with(c.parent as StackPane) {
c.prefWidthProperty().bind(widthProperty())
c.prefHeightProperty().bind(heightProperty())
}
}
fun showDialog(content: Region): JFXDialog {
dialog.content = content
if (!dialogShown)
dialog.show()
return dialog
}
@JvmOverloads
fun startWizard(wizardProvider: WizardProvider, category: String? = null) {
this.category = category
wizardController.provider = wizardProvider
wizardController.onStart()
}
override fun onStart() {
backNavButton.isVisible = true
backNavButton.isDisable = false
closeNavButton.isVisible = true
refreshNavButton.isVisible = false
}
override fun onEnd() {
backNavButton.isVisible = false
closeNavButton.isVisible = false
refreshNavButton.isVisible = false
}
override fun navigateTo(page: Node, nav: Navigation.NavigationDirection) {
setContent(page, nav.animation.animationProducer)
}
fun onRefresh() {
(contentPlaceHolder.children.single() as Refreshable).refresh()
}
fun onCloseNav() {
wizardController.onCancel()
showPage(null)
}
fun onBack() {
wizardController.onPrev(true)
}
}

View File

@@ -1,270 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.adapters.ReflectionHelper
import com.jfoenix.concurrency.JFXUtilities
import com.jfoenix.controls.*
import javafx.animation.Animation
import javafx.animation.Interpolator
import javafx.animation.KeyFrame
import javafx.animation.Timeline
import javafx.beans.property.Property
import javafx.beans.value.ChangeListener
import javafx.event.ActionEvent
import javafx.event.EventHandler
import javafx.fxml.FXMLLoader
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import javafx.scene.image.WritableImage
import javafx.scene.input.MouseEvent
import javafx.scene.input.ScrollEvent
import javafx.scene.layout.Region
import javafx.scene.shape.Rectangle
import javafx.util.Duration
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.Logging.LOG
import org.jackhuang.hmcl.util.ReflectionHelper.call
import org.jackhuang.hmcl.util.ReflectionHelper.construct
import java.io.File
import java.io.IOException
import java.util.logging.Level
fun Node.loadFXML(absolutePath: String) {
val fxmlLoader = FXMLLoader(this.javaClass.getResource(absolutePath), Main.RESOURCE_BUNDLE)
fxmlLoader.setRoot(this)
fxmlLoader.setController(this)
fxmlLoader.load<Any>()
}
fun ListView<*>.smoothScrolling() {
skinProperty().onInvalidated {
val bar = lookup(".scroll-bar") as ScrollBar
val virtualFlow = lookup(".virtual-flow")
val frictions = doubleArrayOf(0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001)
val pushes = doubleArrayOf(1.0)
val derivatives = DoubleArray(frictions.size)
val timeline = Timeline()
bar.addEventHandler(MouseEvent.DRAG_DETECTED) { timeline.stop() }
val scrollEventHandler = EventHandler<ScrollEvent> { event ->
if (event.eventType == ScrollEvent.SCROLL) {
val direction = if (event.deltaY > 0) -1 else 1
for (i in pushes.indices) {
derivatives[i] += direction * pushes[i]
}
if (timeline.status == Animation.Status.STOPPED) {
timeline.play()
}
event.consume()
}
}
bar.addEventHandler(ScrollEvent.ANY, scrollEventHandler)
virtualFlow.onScroll = scrollEventHandler
timeline.keyFrames.add(KeyFrame(Duration.millis(3.0), EventHandler<ActionEvent> {
for (i in derivatives.indices) {
derivatives[i] *= frictions[i]
}
for (i in 1 until derivatives.size) {
derivatives[i] += derivatives[i - 1]
}
val dy = derivatives[derivatives.size - 1]
val height = layoutBounds.height
bar.value = Math.min(Math.max(bar.value + dy / height, 0.0), 1.0)
if (Math.abs(dy) < 0.001) {
timeline.stop()
}
requestLayout()
}))
timeline.cycleCount = Animation.INDEFINITE
}
}
fun ScrollPane.smoothScrolling() = JFXScrollPane.smoothScrolling(this)
fun runOnUiThread(runnable: () -> Unit) = JFXUtilities.runInFX(runnable)
fun takeSnapshot(node: Parent, width: Double, height: Double): WritableImage {
val scene = Scene(node, width, height)
scene.stylesheets.addAll(*stylesheets)
return scene.snapshot(null)
}
fun Region.setOverflowHidden() {
val rectangle = Rectangle()
rectangle.widthProperty().bind(widthProperty())
rectangle.heightProperty().bind(heightProperty())
clip = rectangle
}
val stylesheets = arrayOf(
Controllers::class.java.getResource("/css/jfoenix-fonts.css").toExternalForm(),
Controllers::class.java.getResource("/css/jfoenix-design.css").toExternalForm(),
Controllers::class.java.getResource("/assets/css/jfoenix-main-demo.css").toExternalForm())
fun Region.limitWidth(width: Double) {
maxWidth = width
minWidth = width
prefWidth = width
}
fun Region.limitHeight(height: Double) {
maxHeight = height
minHeight = height
prefHeight = height
}
fun bindInt(textField: JFXTextField, property: Property<*>) {
textField.textProperty().unbind()
@Suppress("UNCHECKED_CAST")
textField.textProperty().bindBidirectional(property as Property<Int>, SafeIntStringConverter())
}
fun bindString(textField: JFXTextField, property: Property<String>) {
textField.textProperty().unbind()
textField.textProperty().bindBidirectional(property)
}
fun bindBoolean(toggleButton: JFXToggleButton, property: Property<Boolean>) {
toggleButton.selectedProperty().unbind()
toggleButton.selectedProperty().bindBidirectional(property)
}
fun bindBoolean(checkBox: JFXCheckBox, property: Property<Boolean>) {
checkBox.selectedProperty().unbind()
checkBox.selectedProperty().bindBidirectional(property)
}
fun bindEnum(comboBox: JFXComboBox<*>, property: Property<out Enum<*>>) {
unbindEnum(comboBox)
val listener = ChangeListener<Number> { _, _, newValue ->
property.value = property.value.javaClass.enumConstants[newValue.toInt()]
}
comboBox.selectionModel.select(property.value.ordinal)
comboBox.properties["listener"] = listener
comboBox.selectionModel.selectedIndexProperty().addListener(listener)
}
fun unbindEnum(comboBox: JFXComboBox<*>) {
@Suppress("UNCHECKED_CAST")
val listener = comboBox.properties["listener"] as? ChangeListener<Number> ?: return
comboBox.selectionModel.selectedIndexProperty().removeListener(listener)
}
/**
* Built-in interpolator that provides discrete time interpolation. The
* return value of `interpolate()` is `endValue` only when the
* input `fraction` is 1.0, and `startValue` otherwise.
*/
@JvmField val SINE: Interpolator = object : Interpolator() {
override fun curve(t: Double): Double {
return Math.sin(t * Math.PI / 2)
}
override fun toString(): String {
return "Interpolator.DISCRETE"
}
}
fun JFXMasonryPane.resetChildren(children: List<Node>) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane::class.java, this, "oldBoxes", null)
this.children.setAll(children)
}
fun openFolder(f: File) {
f.mkdirs()
val path = f.absolutePath
when (OperatingSystem.CURRENT_OS) {
OperatingSystem.OSX ->
try {
Runtime.getRuntime().exec(arrayOf("/usr/bin/open", path));
} catch (ex: IOException) {
LOG.log(Level.SEVERE, "Failed to open $path through /usr/bin/open", ex);
}
else ->
try {
java.awt.Desktop.getDesktop().open(f);
} catch (ex: Throwable) {
LOG.log(Level.SEVERE, "Failed to open $path through java.awt.Desktop.getDesktop().open()", ex);
}
}
}
@JvmOverloads
fun alert(type: Alert.AlertType, title: String, contentText: String, headerText: String? = null): Boolean {
Alert(type).apply {
this.title = title
this.headerText = headerText
this.contentText = contentText
}.showAndWait().run {
return isPresent && get() == ButtonType.OK
}
}
@JvmOverloads
fun inputDialog(title: String, contentText: String, headerText: String? = null, defaultValue: String = "") =
TextInputDialog(defaultValue).apply {
this.title = title
this.headerText = headerText
this.contentText = contentText
}.showAndWait()
fun Node.installTooltip(openDelay: Double = 1000.0, visibleDelay: Double = 5000.0, closeDelay: Double = 200.0, tooltip: Tooltip) {
try {
call(construct(Class.forName("javafx.scene.control.Tooltip\$TooltipBehavior"), Duration(openDelay), Duration(visibleDelay), Duration(closeDelay), false),
"install", this, tooltip);
} catch (e: Throwable) {
LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e)
Tooltip.install(this, tooltip)
}
}
fun JFXTextField.setValidateWhileTextChanged() {
textProperty().onInvalidated(this::validate)
validate()
}
fun JFXPasswordField.setValidateWhileTextChanged() {
textProperty().onInvalidated(this::validate)
validate()
}
fun ImageView.limitSize(maxWidth: Double, maxHeight: Double) {
isPreserveRatio = true
imageProperty().onChangeAndOperate {
if (it != null && (it.width > maxWidth || it.height > maxHeight)) {
fitHeight = maxHeight
fitWidth = maxWidth
} else {
fitHeight = -1.0
fitWidth = -1.0
}
}
}
@JvmField val DEFAULT_ICON = Image("/assets/img/icon.png")

View File

@@ -1,116 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import javafx.scene.layout.VBox
import javafx.scene.paint.Paint
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.event.EventBus
import org.jackhuang.hmcl.event.ProfileChangedEvent
import org.jackhuang.hmcl.event.ProfileLoadingEvent
import org.jackhuang.hmcl.game.AccountHelper
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.construct.IconedItem
import org.jackhuang.hmcl.ui.construct.RipplerContainer
import org.jackhuang.hmcl.util.channel
import org.jackhuang.hmcl.util.onChangeAndOperate
import org.jackhuang.hmcl.util.plusAssign
import java.util.*
class LeftPaneController(private val leftPane: AdvancedListBox) {
val profilePane = VBox()
val accountItem = VersionListItem("No Account", "unknown")
init {
leftPane
.startCategory("ACCOUNTS")
.add(RipplerContainer(accountItem).apply {
setOnMouseClicked {
Controllers.navigate(AccountsPage())
}
accountItem.onSettingsButtonClicked {
Controllers.navigate(AccountsPage())
}
})
.startCategory("LAUNCHER")
.add(IconedItem(SVG.gear("black"), i18n("launcher.title.launcher")).apply {
prefWidthProperty().bind(leftPane.widthProperty())
setOnMouseClicked {
Controllers.navigate(Controllers.settingsPane)
}
})
.startCategory(i18n("ui.label.profile"))
.add(profilePane)
EventBus.EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
EventBus.EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged
Controllers.decorator.addMenuButton.setOnMouseClicked {
Controllers.decorator.showPage(ProfilePage(null))
}
Settings.INSTANCE.selectedAccountProperty().onChangeAndOperate {
if (it == null) {
accountItem.setVersionName("mojang@mojang.com")
accountItem.setGameVersion("Yggdrasil")
} else {
accountItem.setVersionName(it.username)
accountItem.setGameVersion(accountType(it))
}
if (it is YggdrasilAccount) {
accountItem.setImage(AccountHelper.getSkin(it, 4.0), AccountHelper.getViewport(4.0))
} else {
accountItem.setImage(DEFAULT_ICON, null)
}
}
if (Settings.INSTANCE.getAccounts().isEmpty())
Controllers.navigate(AccountsPage())
}
fun onProfileChanged(event: ProfileChangedEvent) {
val profile = event.profile
profilePane.children
.filter { it is RipplerContainer && it.properties["profile"] is Pair<*, *> }
.forEach { (it as RipplerContainer).selected = (it.properties["profile"] as Pair<*, *>).first == profile.name }
}
fun onProfilesLoading() {
val list = LinkedList<RipplerContainer>()
Settings.INSTANCE.profiles.forEach { profile ->
val item = VersionListItem(profile.name)
val ripplerContainer = RipplerContainer(item)
item.onSettingsButtonClicked {
Controllers.decorator.showPage(ProfilePage(profile))
}
ripplerContainer.ripplerFill = Paint.valueOf("#89E1F9")
ripplerContainer.setOnMouseClicked {
// clean selected property
profilePane.children.forEach { if (it is RipplerContainer) it.selected = false }
ripplerContainer.selected = true
Settings.INSTANCE.selectedProfile = profile
}
ripplerContainer.properties["profile"] = profile.name to item
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty())
list += ripplerContainer
}
runOnUiThread { profilePane.children.setAll(list) }
}
}

View File

@@ -1,107 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXMasonryPane
import javafx.application.Platform
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.image.Image
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.event.EventBus
import org.jackhuang.hmcl.event.ProfileChangedEvent
import org.jackhuang.hmcl.event.ProfileLoadingEvent
import org.jackhuang.hmcl.event.RefreshedVersionsEvent
import org.jackhuang.hmcl.game.GameVersion.minecraftVersion
import org.jackhuang.hmcl.game.LauncherHelper
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.channel
import org.jackhuang.hmcl.util.plusAssign
/**
* @see /assets/fxml/main.fxml
*/
class MainPage : StackPane(), DecoratorPage {
private val titleProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main"))
override fun titleProperty() = titleProperty
@FXML lateinit var btnRefresh: JFXButton
@FXML lateinit var btnAdd: JFXButton
@FXML lateinit var masonryPane: JFXMasonryPane
init {
loadFXML("/assets/fxml/main.fxml")
EventBus.EVENT_BUS.channel<RefreshedVersionsEvent>() += { -> runOnUiThread { loadVersions() } }
EventBus.EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
EventBus.EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged
btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") }
btnRefresh.setOnMouseClicked { Settings.INSTANCE.selectedProfile.repository.refreshVersions() }
}
private fun buildNode(i: Int, profile: Profile, version: String, game: String): Node {
return VersionItem().apply {
setGameVersion(game)
setVersionName(version)
setOnLaunchButtonClicked {
if (Settings.INSTANCE.selectedAccount == null) {
Controllers.dialog(i18n("login.no_Player007"))
} else
LauncherHelper.INSTANCE.launch(version)
}
setOnDeleteButtonClicked {
profile.repository.removeVersionFromDisk(version)
Platform.runLater { loadVersions() }
}
setOnSettingsButtonClicked {
Controllers.decorator.showPage(Controllers.versionPane)
Controllers.versionPane.load(version, profile)
}
val iconFile = profile.repository.getVersionIcon(version)
if (iconFile.exists())
setImage(Image("file:" + iconFile.absolutePath))
}
}
fun onProfilesLoading() {
// TODO: Profiles
}
fun onProfileChanged(event: ProfileChangedEvent) = runOnUiThread {
val profile = event.profile
loadVersions(profile)
}
private fun loadVersions(profile: Profile = Settings.INSTANCE.selectedProfile) {
val children = mutableListOf<Node>()
var i = 0
profile.repository.versions.forEach { version ->
children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown")
}
masonryPane.resetChildren(children)
}
}

View File

@@ -1,114 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXSpinner
import com.jfoenix.controls.JFXTabPane
import javafx.fxml.FXML
import javafx.scene.control.ScrollPane
import javafx.scene.input.TransferMode
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import javafx.stage.FileChooser
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.mod.ModManager
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Schedulers
import org.jackhuang.hmcl.util.onChange
import org.jackhuang.hmcl.util.onChangeAndOperateWeakly
import org.jackhuang.hmcl.util.task
import java.util.*
class ModController {
@FXML lateinit var scrollPane: ScrollPane
@FXML lateinit var rootPane: StackPane
@FXML lateinit var modPane: VBox
@FXML lateinit var contentPane: StackPane
@FXML lateinit var spinner: JFXSpinner
lateinit var parentTab: JFXTabPane
private lateinit var modManager: ModManager
private lateinit var versionId: String
fun initialize() {
scrollPane.smoothScrolling()
rootPane.setOnDragOver { event ->
if (event.gestureSource != rootPane && event.dragboard.hasFiles())
event.acceptTransferModes(*TransferMode.COPY_OR_MOVE)
event.consume()
}
rootPane.setOnDragDropped { event ->
val mods = event.dragboard.files
?.filter { it.extension in listOf("jar", "zip", "litemod") }
if (mods != null && mods.isNotEmpty()) {
mods.forEach { modManager.addMod(versionId, it) }
loadMods(modManager, versionId)
event.isDropCompleted = true
}
event.consume()
}
}
fun loadMods(modManager: ModManager, versionId: String) {
this.modManager = modManager
this.versionId = versionId
task {
synchronized(contentPane) {
runOnUiThread { rootPane.children -= contentPane; spinner.isVisible = true }
modManager.refreshMods(versionId)
// Surprisingly, if there are a great number of mods, this processing will cause a UI pause.
// We must do this asynchronously.
val list = LinkedList<ModItem>()
for (modInfo in modManager.getMods(versionId)) {
list += ModItem(modInfo) {
modManager.removeMods(versionId, modInfo)
loadMods(modManager, versionId)
}.apply {
modInfo.activeProperty().onChange {
if (it)
styleClass -= "disabled"
else
styleClass += "disabled"
}
if (!modInfo.isActive)
styleClass += "disabled"
}
}
runOnUiThread { rootPane.children += contentPane; spinner.isVisible = false }
it["list"] = list
}
}.subscribe(Schedulers.javafx()) { variables ->
parentTab.selectionModel.selectedItemProperty().onChangeAndOperateWeakly {
if (it?.userData == this) {
modPane.children.setAll(variables.get<List<ModItem>>("list"))
}
}
}
}
fun onAdd() {
val chooser = FileChooser()
chooser.title = i18n("mods.choose_mod")
chooser.extensionFilters.setAll(FileChooser.ExtensionFilter("Mod", "*.jar", "*.zip", "*.litemod"))
val res = chooser.showOpenDialog(Controllers.stage) ?: return
task { modManager.addMod(versionId, res) }
.subscribe(task(Schedulers.javafx()) { loadMods(modManager, versionId) })
}
}

View File

@@ -1,88 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXTextField
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.construct.FileItem
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.onChangeAndOperate
import java.io.File
/**
* @param profile null if creating a new profile.
*/
class ProfilePage(private val profile: Profile?): StackPane(), DecoratorPage {
private val titleProperty = SimpleStringProperty(this, "title",
if (profile == null) i18n("ui.newProfileWindow.title") else i18n("ui.label.profile") + " - " + profile.name)
override fun titleProperty() = titleProperty
private val locationProperty = SimpleStringProperty(this, "location",
profile?.gameDir?.absolutePath ?: "")
@FXML lateinit var txtProfileName: JFXTextField
@FXML lateinit var gameDir: FileItem
@FXML lateinit var btnSave: JFXButton
@FXML lateinit var btnDelete: JFXButton
init {
loadFXML("/assets/fxml/profile.fxml")
txtProfileName.text = profile?.name ?: ""
txtProfileName.textProperty().onChangeAndOperate {
btnSave.isDisable = !txtProfileName.validate() || locationProperty.get().isNullOrBlank()
}
gameDir.setProperty(locationProperty)
locationProperty.onChangeAndOperate {
btnSave.isDisable = !txtProfileName.validate() || locationProperty.get().isNullOrBlank()
}
if (profile == null)
btnDelete.isVisible = false
}
fun onDelete() {
if (profile != null) {
Settings.INSTANCE.deleteProfile(profile)
Controllers.navigate(null)
}
}
fun onSave() {
if (profile != null) { // editing a profile
profile.name = txtProfileName.text
if (locationProperty.get() != null)
profile.gameDir = File(locationProperty.get())
} else {
if (locationProperty.get().isNullOrBlank()) {
gameDir.onExplore()
}
Settings.INSTANCE.putProfile(Profile(txtProfileName.text, File(locationProperty.get())))
}
Settings.INSTANCE.onProfileLoading()
Controllers.navigate(null)
}
}

View File

@@ -1,65 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import javafx.fxml.FXMLLoader
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.shape.SVGPath
object SVG {
val svgNames = setOf("gear")
val svgs: Map<String, Group>
init {
val svgsImpl = HashMap<String, Group>()
for (svgName in svgNames) {
svgsImpl[svgName] = FXMLLoader(Controllers::class.java.getResource("/assets/svg/$svgName.fxml")).load()
}
svgs = svgsImpl
}
private fun createSVGPath(d: String, fill: String = "black", width: Double = 20.0, height: Double = 20.0): Node {
val path = SVGPath()
path.styleClass += "svg"
path.content = d
path.style = "-fx-fill: $fill;"
val svg = Group(path)
val scale = minOf(width / svg.boundsInParent.width, height / svg.boundsInParent.height)
svg.scaleX = scale
svg.scaleY = scale
svg.maxWidth(width)
return svg
}
@JvmStatic fun gear(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height)
@JvmStatic fun back(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height)
@JvmStatic fun close(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height)
@JvmStatic fun dotsVertical(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height)
@JvmStatic fun delete(fill: String = "white", width: Double = 20.0, height: Double = 20.0): Node = createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height)
@JvmStatic fun accountEdit(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height)
@JvmStatic fun expand(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height)
@JvmStatic fun collapse(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height)
@JvmStatic fun navigate(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height)
@JvmStatic fun launch(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height)
@JvmStatic fun pencil(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height)
@JvmStatic fun refresh(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height)
@JvmStatic fun folderOpen(fill: String = "white", width: Double = 20.0, height: Double = 20.0) = createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height)
}

View File

@@ -1,117 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXComboBox
import com.jfoenix.controls.JFXTextField
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.collections.FXCollections
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.scene.text.Font
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.DownloadProviders
import org.jackhuang.hmcl.setting.Locales
import org.jackhuang.hmcl.setting.Proxies
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.construct.FileItem
import org.jackhuang.hmcl.ui.construct.FontComboBox
import org.jackhuang.hmcl.ui.construct.Validator
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.onChange
class SettingsPage : StackPane(), DecoratorPage {
private val titleProperty: StringProperty = SimpleStringProperty(this, "title", i18n("launcher.title.launcher"))
override fun titleProperty() = titleProperty
@FXML lateinit var txtProxyHost: JFXTextField
@FXML lateinit var txtProxyPort: JFXTextField
@FXML lateinit var txtProxyUsername: JFXTextField
@FXML lateinit var txtProxyPassword: JFXTextField
@FXML lateinit var cboProxyType: JFXComboBox<*>
@FXML lateinit var cboFont: FontComboBox
@FXML lateinit var cboLanguage: JFXComboBox<*>
@FXML lateinit var cboDownloadSource: JFXComboBox<*>
@FXML lateinit var fileCommonLocation: FileItem
@FXML lateinit var fileBackgroundLocation: FileItem
@FXML lateinit var lblDisplay: Label
@FXML lateinit var txtFontSize: JFXTextField
init {
loadFXML("/assets/fxml/setting.fxml")
cboLanguage.limitWidth(400.0)
cboDownloadSource.limitWidth(400.0)
txtProxyHost.text = Settings.INSTANCE.proxyHost
txtProxyHost.textProperty().onChange { Settings.INSTANCE.proxyHost = it }
txtProxyPort.text = Settings.INSTANCE.proxyPort
txtProxyPort.textProperty().onChange { Settings.INSTANCE.proxyPort = it }
txtProxyUsername.text = Settings.INSTANCE.proxyUser
txtProxyUsername.textProperty().onChange { Settings.INSTANCE.proxyUser = it }
txtProxyPassword.text = Settings.INSTANCE.proxyPass
txtProxyPassword.textProperty().onChange { Settings.INSTANCE.proxyPass = it }
cboDownloadSource.selectionModel.select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.INSTANCE.downloadProvider))
cboDownloadSource.selectionModel.selectedIndexProperty().onChange {
Settings.INSTANCE.downloadProvider = DownloadProviders.getDownloadProvider(it)
}
cboFont.selectionModel.select(Settings.INSTANCE.font.family)
cboFont.valueProperty().onChange {
val font = Font.font(it, Settings.INSTANCE.font.size)
Settings.INSTANCE.font = font
lblDisplay.style = "-fx-font: ${Settings.INSTANCE.font.size} \"${font.family}\";"
}
txtFontSize.text = Settings.INSTANCE.font.size.toString()
txtFontSize.validators += Validator { it.toDoubleOrNull() != null }
txtFontSize.textProperty().onChange {
if (txtFontSize.validate()) {
val font = Font.font(Settings.INSTANCE.font.family, it!!.toDouble())
Settings.INSTANCE.font = font
lblDisplay.style = "-fx-font: ${font.size} \"${Settings.INSTANCE.font.family}\";"
}
}
lblDisplay.style = "-fx-font: ${Settings.INSTANCE.font.size} \"${Settings.INSTANCE.font.family}\";"
val list = FXCollections.observableArrayList<Label>()
for (locale in Locales.LOCALES) {
list += Label(locale.getName(Settings.INSTANCE.locale.resourceBundle))
}
cboLanguage.items = list
cboLanguage.selectionModel.select(Locales.LOCALES.indexOf(Settings.INSTANCE.locale))
cboLanguage.selectionModel.selectedIndexProperty().onChange {
Settings.INSTANCE.locale = Locales.getLocale(it)
}
cboProxyType.selectionModel.select(Proxies.PROXIES.indexOf(Settings.INSTANCE.proxyType))
cboProxyType.selectionModel.selectedIndexProperty().onChange {
Settings.INSTANCE.proxyType = Proxies.getProxyType(it)
}
fileCommonLocation.setProperty(Settings.INSTANCE.commonPathProperty())
}
}

View File

@@ -1,139 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXListView
import com.jfoenix.controls.JFXPopup
import com.jfoenix.controls.JFXTabPane
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML
import javafx.scene.control.Alert
import javafx.scene.control.Tab
import javafx.scene.control.Tooltip
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.ui.export.ExportWizardProvider
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
class VersionPage : StackPane(), DecoratorPage {
private val titleProperty: StringProperty = SimpleStringProperty(this, "title", null)
override fun titleProperty() = titleProperty
@FXML lateinit var versionSettingsController: VersionSettingsController
@FXML lateinit var modTab: Tab
@FXML lateinit var modController: ModController
@FXML lateinit var installerController: InstallerController
@FXML lateinit var browseList: JFXListView<*>
@FXML lateinit var managementList: JFXListView<*>
@FXML lateinit var btnBrowseMenu: JFXButton
@FXML lateinit var btnManagementMenu: JFXButton
@FXML lateinit var btnExport: JFXButton
@FXML lateinit var rootPane: StackPane
@FXML lateinit var contentPane: StackPane
@FXML lateinit var tabPane: JFXTabPane
val browsePopup: JFXPopup
val managementPopup: JFXPopup
lateinit var profile: Profile
lateinit var version: String
init {
loadFXML("/assets/fxml/version/version.fxml")
children -= browseList
children -= managementList
browsePopup = JFXPopup(browseList)
managementPopup = JFXPopup(managementList)
btnBrowseMenu.installTooltip(openDelay = 0.0, closeDelay = 0.0, tooltip = Tooltip(i18n("settings.explore")))
btnManagementMenu.installTooltip(openDelay = 0.0, closeDelay = 0.0, tooltip = Tooltip(i18n("settings.manage")))
btnExport.installTooltip(openDelay = 0.0, closeDelay = 0.0, tooltip = Tooltip(i18n("modpack.task.save")))
}
fun load(id: String, profile: Profile) {
this.version = id
this.profile = profile
titleProperty.set(i18n("launcher.title.game") + " - " + id)
versionSettingsController.loadVersionSetting(profile, id, profile.getVersionSetting(id))
modController.parentTab = tabPane
modTab.userData = modController
modController.loadMods(profile.modManager, id)
installerController.loadVersion(profile, id)
}
fun onBrowseMenu() {
browseList.selectionModel.select(-1)
browsePopup.show(btnBrowseMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12.0, 15.0)
}
fun onManagementMenu() {
managementList.selectionModel.select(-1)
managementPopup.show(btnManagementMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12.0, 15.0)
}
fun onExport() {
Controllers.decorator.startWizard(ExportWizardProvider(profile, version), i18n("modpack.wizard"))
}
fun onBrowse() {
openFolder(profile.repository.getRunDirectory(version).resolve(when (browseList.selectionModel.selectedIndex) {
0 -> ""
1 -> "mods"
2 -> "coremods"
3 -> "config"
4 -> "resourcepacks"
5 -> "screenshots"
6 -> "saves"
else -> throw Error()
}))
}
fun onManagement() {
when(managementList.selectionModel.selectedIndex) {
0 -> { // rename a version
val res = inputDialog(title = "Input", contentText = i18n("versions.manage.rename.message"), defaultValue = version)
if (res.isPresent)
if (profile.repository.renameVersion(version, res.get())) {
profile.repository.refreshVersions()
Controllers.navigate(null)
}
}
1 -> { // remove a version
if (alert(Alert.AlertType.CONFIRMATION, "Confirm", i18n("versions.manage.remove.confirm") + version))
if (profile.repository.removeVersionFromDisk(version)) {
profile.repository.refreshVersions()
Controllers.navigate(null)
}
}
2 -> { // redownload asset index
GameAssetIndexDownloadTask(profile.dependency, profile.repository.getVersion(version).resolve(profile.repository)).start()
}
3 -> { // delete libraries
profile.repository.baseDirectory.resolve("libraries").deleteRecursively()
}
else -> throw Error()
}
}
}

View File

@@ -1,252 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.*
import javafx.beans.value.ChangeListener
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.control.Toggle
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import javafx.scene.layout.VBox
import javafx.stage.FileChooser
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.EnumGameDirectory
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.task.Schedulers
import org.jackhuang.hmcl.ui.construct.ComponentList
import org.jackhuang.hmcl.ui.construct.MultiFileItem
import org.jackhuang.hmcl.ui.construct.NumberValidator
import org.jackhuang.hmcl.util.JavaVersion
import org.jackhuang.hmcl.util.OperatingSystem
import org.jackhuang.hmcl.util.task
class VersionSettingsController {
var lastVersionSetting: VersionSetting? = null
@FXML lateinit var rootPane: VBox
@FXML lateinit var scroll: ScrollPane
@FXML lateinit var txtWidth: JFXTextField
@FXML lateinit var txtHeight: JFXTextField
@FXML lateinit var txtMaxMemory: JFXTextField
@FXML lateinit var txtJVMArgs: JFXTextField
@FXML lateinit var txtGameArgs: JFXTextField
@FXML lateinit var txtMetaspace: JFXTextField
@FXML lateinit var txtWrapper: JFXTextField
@FXML lateinit var txtPrecallingCommand: JFXTextField
@FXML lateinit var txtServerIP: JFXTextField
@FXML lateinit var advancedSettingsPane: ComponentList
@FXML lateinit var cboLauncherVisibility: JFXComboBox<*>
@FXML lateinit var chkFullscreen: JFXCheckBox
@FXML lateinit var lblPhysicalMemory: Label
@FXML lateinit var chkNoJVMArgs: JFXToggleButton
@FXML lateinit var chkNoCommon: JFXToggleButton
@FXML lateinit var chkNoGameCheck: JFXToggleButton
@FXML lateinit var javaItem: MultiFileItem
@FXML lateinit var gameDirItem: MultiFileItem
@FXML lateinit var chkShowLogs: JFXToggleButton
@FXML lateinit var btnIconSelection: JFXButton
@FXML lateinit var iconView: ImageView
lateinit var profile: Profile
lateinit var versionId: String
fun initialize() {
lblPhysicalMemory.text = i18n("settings.physical_memory") + ": ${OperatingSystem.TOTAL_MEMORY}MB"
scroll.smoothScrolling()
val limit = 300.0
//txtJavaDir.limitWidth(limit)
txtMaxMemory.limitWidth(limit)
cboLauncherVisibility.limitWidth(limit)
val limitHeight = 10.0
chkNoJVMArgs.limitHeight(limitHeight)
chkNoCommon.limitHeight(limitHeight)
chkNoGameCheck.limitHeight(limitHeight)
chkShowLogs.limitHeight(limitHeight)
fun validator(nullable: Boolean = false) = NumberValidator(nullable).apply { message = "Must be a number." }
txtWidth.setValidators(validator())
txtWidth.setValidateWhileTextChanged()
txtHeight.setValidators(validator())
txtHeight.setValidateWhileTextChanged()
txtMaxMemory.setValidators(validator())
txtMaxMemory.setValidateWhileTextChanged()
txtMetaspace.setValidators(validator(true))
txtMetaspace.setValidateWhileTextChanged()
task {
it["list"] = JavaVersion.getJREs().values.map { javaVersion ->
javaItem.createChildren(javaVersion.version, javaVersion.binary.absolutePath, javaVersion)
}
}.subscribe(Schedulers.javafx()) {
javaItem.loadChildren(it.get<Collection<Node>>("list"))
}
gameDirItem.loadChildren(listOf(
gameDirItem.createChildren(i18n("advancedsettings.game_dir.default"), userData = EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(i18n("advancedsettings.game_dir.independent"), userData = EnumGameDirectory.VERSION_FOLDER)
))
}
fun loadVersionSetting(profile: Profile, versionId: String, version: VersionSetting) {
rootPane.children -= advancedSettingsPane
this.profile = profile
this.versionId = versionId
lastVersionSetting?.apply {
widthProperty().unbind()
heightProperty().unbind()
maxMemoryProperty().unbind()
javaArgsProperty().unbind()
minecraftArgsProperty().unbind()
permSizeProperty().unbind()
wrapperProperty().unbind()
preLaunchCommandProperty().unbind()
serverIpProperty().unbind()
fullscreenProperty().unbind()
notCheckGameProperty().unbind()
noCommonProperty().unbind()
javaDirProperty().unbind()
showLogsProperty().unbind()
unbindEnum(cboLauncherVisibility)
}
bindInt(txtWidth, version.widthProperty())
bindInt(txtHeight, version.heightProperty())
bindInt(txtMaxMemory, version.maxMemoryProperty())
bindString(javaItem.txtCustom, version.javaDirProperty())
bindString(gameDirItem.txtCustom, version.gameDirProperty())
bindString(txtJVMArgs, version.javaArgsProperty())
bindString(txtGameArgs, version.minecraftArgsProperty())
bindString(txtMetaspace, version.permSizeProperty())
bindString(txtWrapper, version.wrapperProperty())
bindString(txtPrecallingCommand, version.preLaunchCommandProperty())
bindString(txtServerIP, version.serverIpProperty())
bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty())
bindBoolean(chkFullscreen, version.fullscreenProperty())
bindBoolean(chkNoGameCheck, version.notCheckGameProperty())
bindBoolean(chkNoCommon, version.noCommonProperty())
bindBoolean(chkShowLogs, version.showLogsProperty())
val javaGroupKey = "java_group.listener"
@Suppress("UNCHECKED_CAST")
(javaItem.group.properties[javaGroupKey] as? ChangeListener<in Toggle>?)
?.run(javaItem.group.selectedToggleProperty()::removeListener)
var flag = false
var defaultToggle: JFXRadioButton? = null
javaItem.group.toggles.filter { it is JFXRadioButton }.forEach { toggle ->
if (toggle.userData == version.javaVersion) {
toggle.isSelected = true
flag = true
} else if (toggle.userData == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle as JFXRadioButton
}
}
val listener = ChangeListener<Toggle> { _, _, newValue ->
if (newValue == javaItem.radioCustom) { // Custom
version.java = "Custom"
} else {
version.java = ((newValue as JFXRadioButton).userData as JavaVersion).version
}
}
javaItem.group.properties[javaGroupKey] = listener
javaItem.group.selectedToggleProperty().addListener(listener)
if (!flag) {
defaultToggle?.isSelected = true
}
version.javaDirProperty().setChangedListener { initJavaSubtitle(version) }
version.javaProperty().setChangedListener { initJavaSubtitle(version) }
initJavaSubtitle(version)
val gameDirKey = "game_dir.listener"
@Suppress("UNCHECKED_CAST")
(gameDirItem.group.properties[gameDirKey] as? ChangeListener<in Toggle>?)
?.run(gameDirItem.group.selectedToggleProperty()::removeListener)
gameDirItem.group.toggles.filter { it is JFXRadioButton }.forEach { toggle ->
if (toggle.userData == version.gameDirType) {
toggle.isSelected = true
flag = true
}
}
gameDirItem.radioCustom.userData = EnumGameDirectory.CUSTOM
val gameDirListener = ChangeListener<Toggle> { _, _, newValue ->
version.gameDirType = (newValue as JFXRadioButton).userData as EnumGameDirectory
}
gameDirItem.group.properties[gameDirKey] = gameDirListener
gameDirItem.group.selectedToggleProperty().addListener(gameDirListener)
version.gameDirProperty().setChangedListener { initGameDirSubtitle(version) }
version.gameDirTypeProperty().setChangedListener { initGameDirSubtitle(version) }
initGameDirSubtitle(version)
lastVersionSetting = version
loadIcon()
}
private fun initJavaSubtitle(version: VersionSetting) {
task { it["java"] = version.javaVersion }
.subscribe(task(Schedulers.javafx()) { javaItem.subtitle = it.get<JavaVersion?>("java")?.binary?.absolutePath ?: "Invalid Java Directory" })
}
private fun initGameDirSubtitle(version: VersionSetting) {
gameDirItem.subtitle = profile.repository.getRunDirectory(versionId).absolutePath
}
fun onShowAdvanced() {
if (!rootPane.children.contains(advancedSettingsPane))
rootPane.children += advancedSettingsPane
else
rootPane.children.remove(advancedSettingsPane)
}
fun onExploreIcon() {
val chooser = FileChooser()
chooser.extensionFilters += FileChooser.ExtensionFilter("Image", "*.png")
val selectedFile = chooser.showOpenDialog(Controllers.stage)
if (selectedFile != null) {
val iconFile = profile.repository.getVersionIcon(versionId)
selectedFile.copyTo(iconFile, overwrite = true)
loadIcon()
}
}
private fun loadIcon() {
val iconFile = profile.repository.getVersionIcon(versionId)
if (iconFile.exists())
iconView.image = Image("file:" + iconFile.absolutePath)
else
iconView.image = DEFAULT_ICON
iconView.limitSize(32.0, 32.0)
}
}

View File

@@ -1,82 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXDialog
import com.jfoenix.controls.JFXPasswordField
import com.jfoenix.controls.JFXProgressBar
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Schedulers
import org.jackhuang.hmcl.util.taskResult
class YggdrasilAccountLoginPane(private val oldAccount: YggdrasilAccount, private val success: (AuthInfo) -> Unit, private val failed: () -> Unit) : StackPane() {
@FXML lateinit var lblUsername: Label
@FXML lateinit var txtPassword: JFXPasswordField
@FXML lateinit var lblCreationWarning: Label
@FXML lateinit var progressBar: JFXProgressBar
lateinit var dialog: JFXDialog
init {
loadFXML("/assets/fxml/yggdrasil-account-login.fxml")
lblUsername.text = oldAccount.username
txtPassword.setOnAction {
onAccept()
}
}
fun onAccept() {
val username = oldAccount.username
val password = txtPassword.text
progressBar.isVisible = true
lblCreationWarning.text = ""
taskResult("login") {
try {
val account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password)
account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.proxy)
} catch (e: Exception) {
e
}
}.subscribe(Schedulers.javafx()) {
val account: Any = it["login"]
if (account is AuthInfo) {
success(account)
dialog.close()
} else if (account is InvalidCredentialsException) {
lblCreationWarning.text = i18n("login.wrong_password")
} else if (account is Exception) {
lblCreationWarning.text = account.javaClass.toString() + ": " + account.localizedMessage
}
progressBar.isVisible = false
}
}
fun onCancel() {
failed()
dialog.close()
}
}

View File

@@ -1,111 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXRadioButton
import com.jfoenix.controls.JFXTextField
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.control.ToggleGroup
import javafx.scene.layout.BorderPane
import javafx.scene.layout.HBox
import javafx.scene.layout.VBox
import javafx.stage.DirectoryChooser
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.SVG
import org.jackhuang.hmcl.ui.limitHeight
import org.jackhuang.hmcl.util.*
class MultiFileItem : ComponentList() {
val customTextProperty = SimpleStringProperty(this, "customText", "Custom")
var customText by customTextProperty
val chooserTitleProperty = SimpleStringProperty(this, "chooserTitle", "Select a file")
var chooserTitle by chooserTitleProperty
val group = ToggleGroup()
val txtCustom = JFXTextField().apply {
BorderPane.setAlignment(this, Pos.CENTER_RIGHT)
}
val btnSelect = JFXButton().apply {
graphic = SVG.folderOpen("black", 15.0, 15.0)
setOnMouseClicked {
// TODO
}
}
val radioCustom = JFXRadioButton().apply {
textProperty().bind(customTextProperty)
toggleGroup = group
}
val custom = BorderPane().apply {
left = radioCustom
style = "-fx-padding: 3;"
right = HBox().apply {
spacing = 3.0
children += txtCustom
children += btnSelect
}
limitHeight(20.0)
}
val pane = VBox().apply {
style = "-fx-padding: 0 0 10 0;"
spacing = 8.0
children += custom
}
init {
addChildren(pane)
txtCustom.disableProperty().bind(radioCustom.selectedProperty().not())
btnSelect.disableProperty().bind(radioCustom.selectedProperty().not())
}
@JvmOverloads
fun createChildren(title: String, subtitle: String = "", userData: Any? = null): Node {
return BorderPane().apply {
style = "-fx-padding: 3;"
limitHeight(20.0)
left = JFXRadioButton(title).apply {
toggleGroup = group
this.userData = userData
}
right = Label(subtitle).apply {
styleClass += "subtitle-label"
style += "-fx-font-size: 10;"
}
}
}
fun loadChildren(list: Collection<Node>) {
pane.children.setAll(list)
pane.children += custom
}
fun onExploreJavaDir() {
val chooser = DirectoryChooser()
chooser.title = i18n(chooserTitle)
val selectedDir = chooser.showDialog(Controllers.stage)
if (selectedDir != null)
txtCustom.text = selectedDir.absolutePath
}
}

View File

@@ -1,179 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct
import com.jfoenix.controls.JFXRippler
import javafx.animation.Transition
import javafx.beans.DefaultProperty
import javafx.beans.NamedArg
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.geometry.Insets
import javafx.scene.Node
import javafx.scene.layout.Background
import javafx.scene.layout.BackgroundFill
import javafx.scene.layout.CornerRadii
import javafx.scene.layout.StackPane
import javafx.scene.paint.Color
import javafx.scene.paint.Paint
import javafx.scene.shape.Rectangle
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.onChange
import org.jackhuang.hmcl.util.onInvalidated
import org.jackhuang.hmcl.util.setValue
import java.util.concurrent.Callable
@DefaultProperty("container")
open class RipplerContainer(@NamedArg("container") container: Node): StackPane() {
val containerProperty = SimpleObjectProperty<Node>(this, "container", null)
@JvmName("containerProperty") get
var container: Node by containerProperty
val ripplerFillProperty = SimpleObjectProperty<Paint>(this, "ripplerFill", null)
@JvmName("ripplerFillProperty") get
var ripplerFill: Paint? by ripplerFillProperty
val selectedProperty = SimpleBooleanProperty(this, "selected", false)
@JvmName("selectedProperty") get
var selected: Boolean by selectedProperty
private val buttonContainer = StackPane()
private val buttonRippler = object : JFXRippler(StackPane()) {
override fun getMask(): Node {
val mask = StackPane()
mask.shapeProperty().bind(buttonContainer.shapeProperty())
mask.backgroundProperty().bind(Bindings.createObjectBinding<Background>(Callable<Background> { Background(BackgroundFill(Color.WHITE, if (buttonContainer.backgroundProperty().get() != null && buttonContainer.getBackground().getFills().size > 0) buttonContainer.background.fills[0].radii else defaultRadii, if (buttonContainer.backgroundProperty().get() != null && buttonContainer.background.fills.size > 0) buttonContainer.background.fills[0].insets else Insets.EMPTY)) }, buttonContainer.backgroundProperty()))
mask.resize(buttonContainer.width - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.height - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset())
return mask
}
override fun initListeners() {
this.ripplerPane.setOnMousePressed { event ->
if (releaseManualRippler != null) {
releaseManualRippler!!.run()
}
releaseManualRippler = null
this.createRipple(event.x, event.y)
}
}
}
private var clickedAnimation: Transition? = null
private val defaultRadii = CornerRadii(3.0)
private var invalid = true
private var releaseManualRippler: Runnable? = null
init {
styleClass += "rippler-container"
this.container = container
this.buttonContainer.children.add(this.buttonRippler)
setOnMousePressed {
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = 1.0
this.clickedAnimation!!.play()
}
}
setOnMouseReleased {
if (this.clickedAnimation != null) {
this.clickedAnimation!!.rate = -1.0
this.clickedAnimation!!.play()
}
}
focusedProperty().onChange {
if (it) {
if (!isPressed) {
this.buttonRippler.showOverlay()
}
} else {
this.buttonRippler.hideOverlay()
}
}
pressedProperty().onInvalidated(this.buttonRippler::hideOverlay)
isPickOnBounds = false
this.buttonContainer.isPickOnBounds = false
this.buttonContainer.shapeProperty().bind(shapeProperty())
this.buttonContainer.borderProperty().bind(borderProperty())
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding<Background>(Callable<Background> {
if (background == null || this.isJavaDefaultBackground(background) || this.isJavaDefaultClickedBackground(background)) {
background = Background(BackgroundFill(Color.TRANSPARENT, defaultRadii, null))
}
try {
return@Callable(
if (background != null && (background.fills[0] as BackgroundFill).insets == Insets(-0.2, -0.2, -0.2, -0.2))
Background(BackgroundFill((if (background != null) (background.fills[0] as BackgroundFill).fill else Color.TRANSPARENT) as Paint,
if (backgroundProperty().get() != null) background.fills[0].radii else defaultRadii, Insets.EMPTY))
else
Background(BackgroundFill((if (background != null) background.fills[0].fill else Color.TRANSPARENT) as Paint,
if (background != null) background.fills[0].radii else defaultRadii, Insets.EMPTY))
)
} catch (var3: Exception) {
return@Callable background
}
}, backgroundProperty()))
ripplerFillProperty.onChange { this.buttonRippler.ripplerFill = it }
if (background == null || this.isJavaDefaultBackground(background)) {
background = Background(BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null))
}
this.updateChildren()
containerProperty.onInvalidated(this::updateChildren)
selectedProperty.onInvalidated {
if (selected) background = Background(BackgroundFill(ripplerFill, defaultRadii, null))
else background = Background(BackgroundFill(Color.TRANSPARENT, defaultRadii, null))
}
shape = Rectangle().apply {
widthProperty().bind(this@RipplerContainer.widthProperty())
heightProperty().bind(this@RipplerContainer.heightProperty())
}
}
protected fun updateChildren() {
children.addAll(buttonContainer, container)
for (i in 1..this.children.size - 1) {
this.children[i].isPickOnBounds = false
}
}
private fun isJavaDefaultBackground(background: Background): Boolean {
try {
val firstFill = (background.fills[0] as BackgroundFill).fill.toString()
return "0xffffffba" == firstFill || "0xffffffbf" == firstFill || "0xffffffbd" == firstFill
} catch (var3: Exception) {
return false
}
}
private fun isJavaDefaultClickedBackground(background: Background): Boolean {
try {
return "0x039ed3ff" == (background.fills[0] as BackgroundFill).fill.toString()
} catch (var3: Exception) {
return false
}
}
}

View File

@@ -1,117 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download
import javafx.scene.Node
import org.jackhuang.hmcl.game.HMCLModpackInstallTask
import org.jackhuang.hmcl.game.HMCLModpackManifest
import org.jackhuang.hmcl.game.MultiMCInstallVersionSettingTask
import org.jackhuang.hmcl.mod.*
import org.jackhuang.hmcl.setting.EnumGameDirectory
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardProvider
import org.jackhuang.hmcl.util.task
import java.io.File
class DownloadWizardProvider(): WizardProvider {
lateinit var profile: Profile
override fun start(settings: MutableMap<String, Any>) {
profile = Settings.INSTANCE.selectedProfile
settings[PROFILE] = profile
}
private fun finishVersionDownloading(settings: MutableMap<String, Any>): Task {
val builder = profile.dependency.gameBuilder()
builder.name(settings["name"] as String)
builder.gameVersion(settings["game"] as String)
if (settings.containsKey("forge"))
builder.version("forge", settings["forge"] as String)
if (settings.containsKey("liteloader"))
builder.version("liteloader", settings["liteloader"] as String)
if (settings.containsKey("optifine"))
builder.version("optifine", settings["optifine"] as String)
return builder.buildAsync()
}
private fun finishModpackInstalling(settings: MutableMap<String, Any>): Task? {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null
val selectedFile = settings[ModpackPage.MODPACK_FILE] as? File? ?: return null
val modpack = settings[ModpackPage.MODPACK_CURSEFORGE_MANIFEST] as? Modpack? ?: return null
val name = settings[ModpackPage.MODPACK_NAME] as? String? ?: return null
profile.repository.markVersionAsModpack(name)
val finalizeTask = task {
profile.repository.refreshVersions()
val vs = profile.specializeVersionSetting(name)
profile.repository.undoMark(name)
if (vs != null) {
vs.gameDirType = EnumGameDirectory.VERSION_FOLDER
}
}
return when (modpack.manifest) {
is CurseManifest -> CurseInstallTask(profile.dependency, selectedFile, modpack.manifest as CurseManifest, name)
is HMCLModpackManifest -> HMCLModpackInstallTask(profile, selectedFile, modpack, name)
is MultiMCInstanceConfiguration -> MultiMCModpackInstallTask(profile.dependency, selectedFile, modpack.manifest as MultiMCInstanceConfiguration, name).with(MultiMCInstallVersionSettingTask(profile, modpack.manifest as MultiMCInstanceConfiguration, name))
else -> throw Error()
}.with(finalizeTask)
}
override fun finish(settings: MutableMap<String, Any>): Any? {
return when (settings[InstallTypePage.INSTALL_TYPE]) {
0 -> finishVersionDownloading(settings)
1 -> finishModpackInstalling(settings)
else -> null
}
}
override fun createPage(controller: WizardController, step: Int, settings: MutableMap<String, Any>): Node {
val provider = profile.dependency.downloadProvider
return when (step) {
0 -> InstallTypePage(controller)
1 -> when (settings[InstallTypePage.INSTALL_TYPE]) {
0 -> VersionsPage(controller, "", provider, "game") { controller.onNext(InstallersPage(controller, profile.repository, provider)) }
1 -> ModpackPage(controller)
else -> throw Error()
}
else -> throw IllegalStateException()
}
}
override fun cancel(): Boolean {
return true
}
companion object {
const val PROFILE = "PROFILE"
}
}

View File

@@ -1,49 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download
import com.jfoenix.controls.JFXListView
import javafx.fxml.FXML
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.ui.loadFXML
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardPage
class InstallTypePage(private val controller: WizardController): StackPane(), WizardPage {
@FXML lateinit var list: JFXListView<Any>
init {
loadFXML("/assets/fxml/download/dltype.fxml")
list.setOnMouseClicked {
controller.settings[INSTALL_TYPE] = list.selectionModel.selectedIndex
controller.onNext()
}
}
override fun cleanup(settings: MutableMap<String, Any>) {
settings.remove(INSTALL_TYPE)
}
override fun getTitle() = "Select an operation"
companion object {
const val INSTALL_TYPE: String = "INSTALL_TYPE"
}
}

View File

@@ -1,105 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXListView
import com.jfoenix.controls.JFXTextField
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.ui.construct.Validator
import org.jackhuang.hmcl.ui.loadFXML
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardPage
import org.jackhuang.hmcl.util.onInvalidated
class InstallersPage(private val controller: WizardController, private val repository: GameRepository, private val downloadProvider: DownloadProvider): StackPane(), WizardPage {
@FXML lateinit var list: VBox
@FXML lateinit var btnForge: JFXButton
@FXML lateinit var btnLiteLoader: JFXButton
@FXML lateinit var btnOptiFine: JFXButton
@FXML lateinit var lblGameVersion: Label
@FXML lateinit var lblForge: Label
@FXML lateinit var lblLiteLoader: Label
@FXML lateinit var lblOptiFine: Label
@FXML lateinit var txtName: JFXTextField
@FXML lateinit var btnInstall: JFXButton
init {
loadFXML("/assets/fxml/download/installers.fxml")
val gameVersion = controller.settings["game"] as String
txtName.validators += Validator { !repository.hasVersion(it) && it.isNotBlank() }.apply { message = i18n("version.already_exists") }
txtName.textProperty().onInvalidated { btnInstall.isDisable = !txtName.validate() }
txtName.text = gameVersion
btnForge.setOnMouseClicked {
controller.settings[INSTALLER_TYPE] = 0
controller.onNext(VersionsPage(controller, gameVersion, downloadProvider, "forge") { controller.onPrev(false) })
}
btnLiteLoader.setOnMouseClicked {
controller.settings[INSTALLER_TYPE] = 1
controller.onNext(VersionsPage(controller, gameVersion, downloadProvider, "liteloader") { controller.onPrev(false) })
}
btnOptiFine.setOnMouseClicked {
controller.settings[INSTALLER_TYPE] = 2
controller.onNext(VersionsPage(controller, gameVersion, downloadProvider, "optifine") { controller.onPrev(false) })
}
}
override fun getTitle() = "Choose a game version"
override fun onNavigate(settings: MutableMap<String, Any>) {
lblGameVersion.text = "Current Game Version: ${controller.settings["game"]}"
if (controller.settings.containsKey("forge"))
lblForge.text = "Forge Versoin: ${controller.settings["forge"]}"
else
lblForge.text = "Forge not installed"
if (controller.settings.containsKey("liteloader"))
lblLiteLoader.text = "LiteLoader Versoin: ${controller.settings["liteloader"]}"
else
lblLiteLoader.text = "LiteLoader not installed"
if (controller.settings.containsKey("optifine"))
lblOptiFine.text = "OptiFine Versoin: ${controller.settings["optifine"]}"
else
lblOptiFine.text = "OptiFine not installed"
}
override fun cleanup(settings: MutableMap<String, Any>) {
settings.remove(INSTALLER_TYPE)
}
fun onInstall() {
controller.settings["name"] = txtName.text
controller.onFinish()
}
companion object {
val INSTALLER_TYPE = "INSTALLER_TYPE"
}
}

View File

@@ -1,108 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.download
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXTextField
import javafx.application.Platform
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.Region
import javafx.scene.layout.StackPane
import javafx.stage.FileChooser
import org.jackhuang.hmcl.game.ModpackHelper
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.mod.Modpack
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.ui.*
import org.jackhuang.hmcl.ui.construct.Validator
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardPage
import org.jackhuang.hmcl.util.onInvalidated
class ModpackPage(private val controller: WizardController): StackPane(), WizardPage {
private val title: String = i18n("modpack.task.install")
override fun getTitle() = title
@FXML lateinit var borderPane: Region
@FXML lateinit var lblName: Label
@FXML lateinit var lblVersion: Label
@FXML lateinit var lblAuthor: Label
@FXML lateinit var lblModpackLocation: Label
@FXML lateinit var txtModpackName: JFXTextField
@FXML lateinit var btnInstall: JFXButton
var manifest: Modpack? = null
init {
loadFXML("/assets/fxml/download/modpack.fxml")
val profile = controller.settings["PROFILE"] as Profile
val chooser = FileChooser()
chooser.title = i18n("modpack.choose")
chooser.extensionFilters += FileChooser.ExtensionFilter(i18n("modpack"), "*.zip")
val selectedFile = chooser.showOpenDialog(Controllers.stage)
if (selectedFile == null) Platform.runLater { controller.onFinish() }
else {
// TODO: original HMCL modpack support.
controller.settings[MODPACK_FILE] = selectedFile
lblModpackLocation.text = selectedFile.absolutePath
txtModpackName.validators += Validator { !profile.repository.hasVersion(it) && it.isNotBlank() }.apply { message = i18n("version.already_exists") }
txtModpackName.textProperty().onInvalidated { btnInstall.isDisable = !txtModpackName.validate() }
try {
manifest = ModpackHelper.readModpackManifest(selectedFile)
controller.settings[MODPACK_CURSEFORGE_MANIFEST] = manifest!!
lblName.text = manifest!!.name
lblVersion.text = manifest!!.version
lblAuthor.text = manifest!!.author
txtModpackName.text = manifest!!.name + (if (manifest!!.version.isNullOrBlank()) "" else ("-" + manifest!!.version))
} catch (e: Exception) {
// TODO
txtModpackName.text = i18n("modpack.task.install.error")
}
}
//borderPane.limitHeight(100.0)
borderPane.limitWidth(500.0)
}
override fun cleanup(settings: MutableMap<String, Any>) {
settings.remove(MODPACK_FILE)
}
fun onInstall() {
if (!txtModpackName.validate()) return
controller.settings[MODPACK_NAME] = txtModpackName.text
controller.onFinish()
}
fun onDescribe() {
if (manifest != null)
WebStage().apply {
webView.engine.loadContent(manifest!!.description)
title = i18n("modpack.wizard.step.3")
}.showAndWait()
}
companion object {
val MODPACK_FILE = "MODPACK_FILE"
val MODPACK_NAME = "MODPACK_NAME"
val MODPACK_CURSEFORGE_MANIFEST = "CURSEFORGE_MANIFEST"
}
}

View File

@@ -1,137 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.wizard
import com.jfoenix.concurrency.JFXUtilities
import com.jfoenix.controls.JFXProgressBar
import javafx.application.Platform
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import org.jackhuang.hmcl.task.*
import java.util.*
import kotlin.concurrent.thread
interface AbstractWizardDisplayer : WizardDisplayer {
val wizardController: WizardController
val cancelQueue: Queue<Any>
override fun handleDeferredWizardResult(settings: Map<String, Any>, deferredResult: DeferredWizardResult) {
val vbox = VBox()
val progressBar = JFXProgressBar()
val label = Label()
progressBar.maxHeight = 10.0
vbox.children += progressBar
vbox.children += label
navigateTo(StackPane().apply { children += vbox }, Navigation.NavigationDirection.FINISH)
cancelQueue.add(thread {
deferredResult.start(settings, object : ResultProgressHandle {
private var running = true
override fun setProgress(currentStep: Int, totalSteps: Int) {
progressBar.progress = 1.0 * currentStep / totalSteps
}
override fun setProgress(description: String, currentStep: Int, totalSteps: Int) {
label.text = description
progressBar.progress = 1.0 * currentStep / totalSteps
}
override fun setBusy(description: String) {
progressBar.progress = JFXProgressBar.INDETERMINATE_PROGRESS
}
override fun finished(result: Any) {
running = false
}
override fun failed(message: String, canNavigateBack: Boolean) {
label.text = message
running = false
}
override fun isRunning() = running
})
if (!Thread.currentThread().isInterrupted)
JFXUtilities.runInFX {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
}
})
}
override fun handleTask(settings: Map<String, Any>, task: Task) {
val vbox = VBox()
val tasksBar = JFXProgressBar()
val label = Label()
tasksBar.maxHeight = 10.0
vbox.children += tasksBar
vbox.children += label
var finishedTasks = 0
navigateTo(StackPane().apply { children += vbox }, Navigation.NavigationDirection.FINISH)
task.with(org.jackhuang.hmcl.util.task(Schedulers.javafx()) {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
}).executor().apply {
@Suppress("NAME_SHADOWING")
taskListener = object : TaskListener() {
override fun onReady(task: Task) {
Platform.runLater { tasksBar.progressProperty().set(finishedTasks * 1.0 / runningTasks) }
}
override fun onFinished(task: Task) {
Platform.runLater {
label.text = task.name
++finishedTasks
tasksBar.progressProperty().set(finishedTasks * 1.0 / runningTasks)
}
}
override fun onFailed(task: Task, throwable: Throwable) {
Platform.runLater {
label.text = task.name
++finishedTasks
tasksBar.progressProperty().set(finishedTasks * 1.0 / runningTasks)
}
}
override fun onTerminate() {
Platform.runLater { navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH) }
}
}
cancelQueue.add(this)
}.start()
}
override fun onCancel() {
while (cancelQueue.isNotEmpty()) {
val x = cancelQueue.poll()
when (x) {
is TaskExecutor -> x.cancel()
is Thread -> x.interrupt()
}
}
}
}

View File

@@ -1,93 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.wizard
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXToolbar
import javafx.fxml.FXML
import javafx.scene.Node
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.animation.TransitionHandler
import org.jackhuang.hmcl.ui.loadFXML
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
internal class DefaultWizardDisplayer(private val prefix: String, wizardProvider: WizardProvider) : StackPane(), AbstractWizardDisplayer {
override val wizardController = WizardController(this).apply { provider = wizardProvider }
override val cancelQueue: Queue<Any> = ConcurrentLinkedQueue<Any>()
lateinit var transitionHandler: TransitionHandler
@FXML lateinit var root: StackPane
@FXML lateinit var backButton: JFXButton
@FXML lateinit var toolbar: JFXToolbar
/**
* Only shown if it is needed in now step.
*/
@FXML lateinit var refreshButton: JFXButton
@FXML lateinit var titleLabel: Label
lateinit var nowPage: Node
init {
loadFXML("/assets/fxml/wizard.fxml")
toolbar.effect = null
}
fun initialize() {
transitionHandler = TransitionHandler(root)
wizardController.onStart()
}
override fun onStart() {
}
override fun onEnd() {
}
override fun onCancel() {
}
fun back() {
wizardController.onPrev(true)
}
fun close() {
wizardController.onCancel()
Controllers.navigate(null)
}
fun refresh() {
(nowPage as Refreshable).refresh()
}
override fun navigateTo(page: Node, nav: Navigation.NavigationDirection) {
backButton.isDisable = !wizardController.canPrev()
transitionHandler.setContent(page, nav.animation.animationProducer)
val title = if (prefix.isEmpty()) "" else "$prefix - "
if (page is WizardPage)
titleLabel.text = title + page.title
refreshButton.isVisible = page is Refreshable
nowPage = page
}
}

View File

@@ -1,95 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.wizard
import javafx.scene.Node
import org.jackhuang.hmcl.task.Task
import java.util.*
class WizardController(protected val displayer: WizardDisplayer) : Navigation {
lateinit var provider: WizardProvider
val settings = mutableMapOf<String, Any>()
val pages = Stack<Node>()
override fun onStart() {
settings.clear()
provider.start(settings)
pages.clear()
val page = navigatingTo(0)
pages.push(page)
if (page is WizardPage)
page.onNavigate(settings)
displayer.onStart()
displayer.navigateTo(page, Navigation.NavigationDirection.START)
}
override fun onNext() {
onNext(navigatingTo(pages.size))
}
fun onNext(page: Node) {
pages.push(page)
if (page is WizardPage)
page.onNavigate(settings)
displayer.navigateTo(page, Navigation.NavigationDirection.NEXT)
}
override fun onPrev(cleanUp: Boolean) {
val page = pages.pop()
if (cleanUp && page is WizardPage)
page.cleanup(settings)
val prevPage = pages.peek()
if (prevPage is WizardPage)
prevPage.onNavigate(settings)
displayer.navigateTo(prevPage, Navigation.NavigationDirection.PREVIOUS)
}
override fun canPrev() = pages.size > 1
override fun onFinish() {
val result = provider.finish(settings)
when (result) {
is DeferredWizardResult -> displayer.handleDeferredWizardResult(settings, result)
is Summary -> displayer.navigateTo(result.component, Navigation.NavigationDirection.NEXT)
is Task -> displayer.handleTask(settings, result)
}
}
override fun onEnd() {
settings.clear()
pages.clear()
displayer.onEnd()
}
override fun onCancel() {
displayer.onCancel()
onEnd()
}
fun navigatingTo(step: Int): Node {
return provider.createPage(this, step, settings)
}
}

View File

@@ -1,123 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util
import com.google.gson.Gson
import com.google.gson.JsonParseException
import com.google.gson.reflect.TypeToken
import javafx.beans.property.Property
import javafx.event.Event.fireEvent
import org.jackhuang.hmcl.event.Event
import org.jackhuang.hmcl.event.EventBus
import org.jackhuang.hmcl.event.EventManager
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Schedulers
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.util.Constants.UI_THREAD_SCHEDULER
import java.io.InputStream
import java.lang.reflect.Type
import java.net.URL
import java.rmi.activation.Activatable.unregister
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicReference
inline fun ignoreException(func: () -> Unit) {
try {
func()
} catch(ignore: Exception) {}
}
inline fun ignoreThrowable(func: () -> Unit) {
try {
func()
} catch (ignore: Throwable) {}
}
fun isBlank(str: String?) = str?.isBlank() ?: true
fun isNotBlank(str: String?) = !isBlank(str)
fun String.tokenize(delim: String = " \t\n\r"): List<String> {
val list = mutableListOf<String>()
val tokenizer = StringTokenizer(this, delim)
while (tokenizer.hasMoreTokens())
list.add(tokenizer.nextToken())
return list
}
fun String.asVersion(): String? {
if (count { it != '.' && (it < '0' || it > '9') } > 0 || isBlank())
return null
val s = split(".")
for (i in s) if (i.isBlank()) return null
val builder = StringBuilder()
var last = s.size - 1
for (i in s.size - 1 downTo 0)
if (s[i].toInt() == 0)
last = i
for (i in 0 .. last)
builder.append(s[i]).append('.')
return builder.deleteCharAt(builder.length - 1).toString()
}
fun Any?.toStringOrEmpty() = this?.toString().orEmpty()
fun String.toURL() = URL(this)
fun Collection<String>.containsOne(vararg matcher: String): Boolean {
for (a in this)
for (b in matcher)
if (a.toLowerCase().contains(b.toLowerCase()))
return true
return false
}
fun <T> Property<in T>.updateAsync(newValue: T, update: AtomicReference<T>) {
if (update.getAndSet(newValue) == null) {
UI_THREAD_SCHEDULER.accept(Runnable {
val current = update.getAndSet(null)
this.value = current
})
}
}
inline fun <reified T> typeOf(): Type = object : TypeToken<T>() {}.type
inline fun <reified T> Gson.fromJson(json: String): T? = fromJson<T>(json, T::class.java)
inline fun <reified T> Gson.fromJsonQuietly(json: String): T? {
try {
return fromJson<T>(json)
} catch (json: JsonParseException) {
return null
}
}
fun task(scheduler: Scheduler = Schedulers.defaultScheduler(), closure: (AutoTypingMap<String>) -> Unit): Task = Task.of(closure, scheduler)
fun <V> taskResult(id: String, callable: Callable<V>): TaskResult<V> = Task.ofResult(id, callable)
fun <V> taskResult(id: String, callable: (AutoTypingMap<String>) -> V): TaskResult<V> = Task.ofResult(id, callable)
fun InputStream.readFullyAsString() = IOUtils.readFullyAsString(this)
inline fun <reified T : Event> EventBus.channel() = channel(T::class.java)
operator fun <T : Event> EventManager<T>.plusAssign(func: (T) -> Unit) = register(func)
operator fun <T : Event> EventManager<T>.plusAssign(func: () -> Unit) = register(func)
operator fun <T : Event> EventManager<T>.minusAssign(func: (T) -> Unit) = unregister(func)
operator fun <T : Event> EventManager<T>.minusAssign(func: () -> Unit) = unregister(func)
operator fun <T : Event> EventManager<T>.invoke(event: T) = fireEvent(event)

View File

@@ -1,44 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util
import javafx.beans.value.*
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
fun <T> ObservableValue<T>.onChange(op: (T?) -> Unit) = apply { addListener { _, _, new -> op(new) } }
fun <T> ObservableValue<T>.onChangeAndOperate(op: (T?) -> Unit) = apply { addListener { _, _, new -> op(new) }; op(value) }
fun <T> ObservableValue<T>.onChangeAndOperateWeakly(op: (T?) -> Unit) = apply { addListener(WeakChangeListener { _, _, new -> op(new) }); op(value) }
fun ObservableBooleanValue.onChange(op: (Boolean) -> Unit) = apply { addListener { _, _, new -> op(new ?: false) } }
fun ObservableIntegerValue.onChange(op: (Int) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0).toInt()) } }
fun ObservableLongValue.onChange(op: (Long) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0L).toLong()) } }
fun ObservableFloatValue.onChange(op: (Float) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0f).toFloat()) } }
fun ObservableDoubleValue.onChange(op: (Double) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0.0).toDouble()) } }
fun <T> ObservableList<T>.onChange(op: (ListChangeListener.Change<out T>) -> Unit) = apply {
addListener(ListChangeListener { op(it) })
}
fun <T> ObservableValue<*>.onInvalidated(op: () -> T) = apply { addListener { _ -> op() } }
fun <T> ObservableValue<T>.setOnChangeListener(properties: MutableMap<Any, Any>, key: String = "changeListener", changeListener: ChangeListener<in T>) {
@Suppress("UNCHECKED_CAST")
if (properties.containsKey(key))
removeListener(properties[key] as ChangeListener<in T>)
properties[key] = changeListener
addListener(changeListener)
}

View File

@@ -1,767 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util
import javafx.beans.Observable
import javafx.beans.binding.*
import javafx.beans.property.*
import javafx.beans.value.*
import javafx.collections.ObservableList
import java.lang.ref.WeakReference
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.concurrent.Callable
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
fun <T> property(value: T? = null) = PropertyDelegate(SimpleObjectProperty<T>(value))
fun <T> property(block: () -> Property<T>) = PropertyDelegate(block())
class PropertyDelegate<T>(val fxProperty: Property<T>) : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return fxProperty.value
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
fxProperty.value = value
}
}
fun <T> Any.getProperty(prop: KMutableProperty1<*, T>): ObjectProperty<T> {
// avoid kotlin-reflect dependency
val field = javaClass.findFieldByName("${prop.name}\$delegate")
?: throw IllegalArgumentException("No delegate field then name '${prop.name}' found")
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
val delegate = field.get(this) as PropertyDelegate<T>
return delegate.fxProperty as ObjectProperty<T>
}
fun Class<*>.findFieldByName(name: String): Field? {
val field = (declaredFields + fields).find { it.name == name }
if (field != null) return field
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
if (superclass == java.lang.Object::class.java) return null
return superclass.findFieldByName(name)
}
fun Class<*>.findMethodByName(name: String): Method? {
val method = (declaredMethods + methods).find { it.name == name }
if (method != null) return method
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
if (superclass == java.lang.Object::class.java) return null
return superclass.findMethodByName(name)
}
/**
* Convert an owner instance and a corresponding property reference into an observable
*/
fun <S, T> S.observable(prop: KMutableProperty1<S, T>) = observable(this, prop)
/**
* Convert an owner instance and a corresponding property reference into an observable
*/
@JvmName("observableFromMutableProperty")
fun <S, T> observable(owner: S, prop: KMutableProperty1<S, T>): ObjectProperty<T> {
return object : SimpleObjectProperty<T>(owner, prop.name) {
override fun get() = prop.get(owner)
override fun set(v: T) = prop.set(owner, v)
}
}
/**
* Convert an owner instance and a corresponding property reference into a readonly observable
*/
fun <S, T> observable(owner: S, prop: KProperty1<S, T>): ReadOnlyObjectProperty<T> {
return object : ReadOnlyObjectWrapper<T>(owner, prop.name) {
override fun get() = prop.get(owner)
}
}
open class PojoProperty<T>(bean: Any, propName: String) : SimpleObjectProperty<T>(bean, propName) {
fun refresh() {
fireValueChangedEvent()
}
}
enum class SingleAssignThreadSafetyMode {
SYNCHRONIZED,
NONE
}
fun <T> singleAssign(threadSafeyMode: SingleAssignThreadSafetyMode = SingleAssignThreadSafetyMode.SYNCHRONIZED): SingleAssign<T> =
if (threadSafeyMode.equals(SingleAssignThreadSafetyMode.SYNCHRONIZED)) SynchronizedSingleAssign<T>() else UnsynchronizedSingleAssign<T>()
private object UNINITIALIZED_VALUE
interface SingleAssign<T> {
fun isInitialized(): Boolean
operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
}
private class SynchronizedSingleAssign<T> : SingleAssign<T> {
@Volatile
private var initialized = false
@Volatile
private var _value: Any? = UNINITIALIZED_VALUE
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (!initialized)
throw Exception("Value has not been assigned yet!")
@Suppress("UNCHECKED_CAST")
return _value as T
}
override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
synchronized(this) {
if (initialized) {
throw Exception("Value has already been assigned!")
}
_value = value
initialized = true
}
}
override fun isInitialized() = initialized
}
private class UnsynchronizedSingleAssign<T> : SingleAssign<T> {
private var initialized = false
private var _value: Any? = UNINITIALIZED_VALUE
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (!initialized)
throw Exception("Value has not been assigned yet!")
@Suppress("UNCHECKED_CAST")
return _value as T
}
override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (initialized) {
throw Exception("Value has already been assigned!")
}
_value = value
initialized = true
}
override fun isInitialized() = initialized
}
/**
* Binds this property to an observable, automatically unbinding it before if already bound.
*/
fun <T> Property<T>.cleanBind(observable: ObservableValue<T>) {
unbind()
bind(observable)
}
operator fun <T> ObservableValue<T>.getValue(thisRef: Any, property: KProperty<*>) = value
operator fun <T> Property<T?>.setValue(thisRef: Any, property: KProperty<*>, value: T?) = setValue(value)
operator fun ObservableDoubleValue.getValue(thisRef: Any, property: KProperty<*>) = get()
operator fun DoubleProperty.setValue(thisRef: Any, property: KProperty<*>, value: Double) = set(value)
operator fun ObservableFloatValue.getValue(thisRef: Any, property: KProperty<*>) = get()
operator fun FloatProperty.setValue(thisRef: Any, property: KProperty<*>, value: Float) = set(value)
operator fun ObservableLongValue.getValue(thisRef: Any, property: KProperty<*>) = get()
operator fun LongProperty.setValue(thisRef: Any, property: KProperty<*>, value: Long) = set(value)
operator fun ObservableIntegerValue.getValue(thisRef: Any, property: KProperty<*>) = get()
operator fun IntegerProperty.setValue(thisRef: Any, property: KProperty<*>, value: Int) = set(value)
operator fun ObservableBooleanValue.getValue(thisRef: Any, property: KProperty<*>) = get()
operator fun BooleanProperty.setValue(thisRef: Any, property: KProperty<*>, value: Boolean) = set(value)
operator fun DoubleExpression.plus(other: Number): DoubleBinding = add(other.toDouble())
operator fun DoubleExpression.plus(other: ObservableNumberValue): DoubleBinding = add(other)
operator fun DoubleProperty.plusAssign(other: Number) { value += other.toDouble() }
operator fun DoubleProperty.plusAssign(other: ObservableNumberValue) { value += other.doubleValue() }
operator fun DoubleProperty.inc(): DoubleProperty {
value++
return this
}
operator fun DoubleExpression.minus(other: Number): DoubleBinding = subtract(other.toDouble())
operator fun DoubleExpression.minus(other: ObservableNumberValue): DoubleBinding = subtract(other)
operator fun DoubleProperty.minusAssign(other: Number) { value -= other.toDouble() }
operator fun DoubleProperty.minusAssign(other: ObservableNumberValue) { value -= other.doubleValue() }
operator fun DoubleExpression.unaryMinus(): DoubleBinding = negate()
operator fun DoubleProperty.dec(): DoubleProperty {
value--
return this
}
operator fun DoubleExpression.times(other: Number): DoubleBinding = multiply(other.toDouble())
operator fun DoubleExpression.times(other: ObservableNumberValue): DoubleBinding = multiply(other)
operator fun DoubleProperty.timesAssign(other: Number) { value *= other.toDouble() }
operator fun DoubleProperty.timesAssign(other: ObservableNumberValue) { value *= other.doubleValue() }
operator fun DoubleExpression.div(other: Number): DoubleBinding = divide(other.toDouble())
operator fun DoubleExpression.div(other: ObservableNumberValue): DoubleBinding = divide(other)
operator fun DoubleProperty.divAssign(other: Number) { value /= other.toDouble() }
operator fun DoubleProperty.divAssign(other: ObservableNumberValue) { value /= other.doubleValue() }
operator fun DoubleExpression.rem(other: Number): DoubleBinding = doubleBinding(this) { get() % other.toDouble() }
operator fun DoubleExpression.rem(other: ObservableNumberValue): DoubleBinding = doubleBinding(this, other) { get() % other.doubleValue() }
operator fun DoubleProperty.remAssign(other: Number) { value %= other.toDouble() }
operator fun DoubleProperty.remAssign(other: ObservableNumberValue) { value %= other.doubleValue() }
operator fun ObservableDoubleValue.compareTo(other: Number): Int {
if (get() > other.toDouble())
return 1
else if (get() < other.toDouble())
return -1
else
return 0
}
operator fun ObservableDoubleValue.compareTo(other: ObservableNumberValue): Int {
if (get() > other.doubleValue())
return 1
else if (get() < other.doubleValue())
return -1
else
return 0
}
operator fun FloatExpression.plus(other: Number): FloatBinding = add(other.toFloat())
operator fun FloatExpression.plus(other: Double): DoubleBinding = add(other)
operator fun FloatExpression.plus(other: ObservableNumberValue): FloatBinding = add(other) as FloatBinding
operator fun FloatExpression.plus(other: ObservableDoubleValue): DoubleBinding = add(other) as DoubleBinding
operator fun FloatProperty.plusAssign(other: Number) { value += other.toFloat() }
operator fun FloatProperty.plusAssign(other: ObservableNumberValue) { value += other.floatValue() }
operator fun FloatProperty.inc(): FloatProperty {
value++
return this
}
operator fun FloatExpression.minus(other: Number): FloatBinding = subtract(other.toFloat())
operator fun FloatExpression.minus(other: Double): DoubleBinding = subtract(other)
operator fun FloatExpression.minus(other: ObservableNumberValue): FloatBinding = subtract(other) as FloatBinding
operator fun FloatExpression.minus(other: ObservableDoubleValue): DoubleBinding = subtract(other) as DoubleBinding
operator fun FloatProperty.minusAssign(other: Number) { value -= other.toFloat() }
operator fun FloatProperty.minusAssign(other: ObservableNumberValue) { value -= other.floatValue() }
operator fun FloatExpression.unaryMinus(): FloatBinding = negate()
operator fun FloatProperty.dec(): FloatProperty {
value--
return this
}
operator fun FloatExpression.times(other: Number): FloatBinding = multiply(other.toFloat())
operator fun FloatExpression.times(other: Double): DoubleBinding = multiply(other)
operator fun FloatExpression.times(other: ObservableNumberValue): FloatBinding = multiply(other) as FloatBinding
operator fun FloatExpression.times(other: ObservableDoubleValue): DoubleBinding = multiply(other) as DoubleBinding
operator fun FloatProperty.timesAssign(other: Number) { value *= other.toFloat() }
operator fun FloatProperty.timesAssign(other: ObservableNumberValue) { value *= other.floatValue() }
operator fun FloatExpression.div(other: Number): FloatBinding = divide(other.toFloat())
operator fun FloatExpression.div(other: Double): DoubleBinding = divide(other)
operator fun FloatExpression.div(other: ObservableNumberValue): FloatBinding = divide(other) as FloatBinding
operator fun FloatExpression.div(other: ObservableDoubleValue): DoubleBinding = divide(other) as DoubleBinding
operator fun FloatProperty.divAssign(other: Number) { value /= other.toFloat() }
operator fun FloatProperty.divAssign(other: ObservableNumberValue) { value /= other.floatValue() }
operator fun FloatExpression.rem(other: Number): FloatBinding = floatBinding(this) { get() % other.toFloat() }
operator fun FloatExpression.rem(other: Double): DoubleBinding = doubleBinding(this) { get() % other }
operator fun FloatExpression.rem(other: ObservableNumberValue): FloatBinding = floatBinding(this, other) { get() % other.floatValue() }
operator fun FloatExpression.rem(other: ObservableDoubleValue): DoubleBinding = doubleBinding(this, other) { get() % other.get() }
operator fun FloatProperty.remAssign(other: Number) { value %= other.toFloat() }
operator fun FloatProperty.remAssign(other: ObservableNumberValue) { value %= other.floatValue() }
operator fun ObservableFloatValue.compareTo(other: Number): Int {
if (get() > other.toFloat())
return 1
else if (get() < other.toFloat())
return -1
else
return 0
}
operator fun ObservableFloatValue.compareTo(other: ObservableNumberValue): Int {
if (get() > other.floatValue())
return 1
else if (get() < other.floatValue())
return -1
else
return 0
}
operator fun IntegerExpression.plus(other: Int): IntegerBinding = add(other)
operator fun IntegerExpression.plus(other: Long): LongBinding = add(other)
operator fun IntegerExpression.plus(other: Float): FloatBinding = add(other)
operator fun IntegerExpression.plus(other: Double): DoubleBinding = add(other)
operator fun IntegerExpression.plus(other: ObservableIntegerValue): IntegerBinding = add(other) as IntegerBinding
operator fun IntegerExpression.plus(other: ObservableLongValue): LongBinding = add(other) as LongBinding
operator fun IntegerExpression.plus(other: ObservableFloatValue): FloatBinding = add(other) as FloatBinding
operator fun IntegerExpression.plus(other: ObservableDoubleValue): DoubleBinding = add(other) as DoubleBinding
operator fun IntegerProperty.plusAssign(other: Number) { value += other.toInt() }
operator fun IntegerProperty.plusAssign(other: ObservableNumberValue) { value += other.intValue() }
operator fun IntegerProperty.inc(): IntegerProperty {
value++
return this
}
operator fun IntegerExpression.minus(other: Int): IntegerBinding = subtract(other)
operator fun IntegerExpression.minus(other: Long): LongBinding = subtract(other)
operator fun IntegerExpression.minus(other: Float): FloatBinding = subtract(other)
operator fun IntegerExpression.minus(other: Double): DoubleBinding = subtract(other)
operator fun IntegerExpression.minus(other: ObservableIntegerValue): IntegerBinding = subtract(other) as IntegerBinding
operator fun IntegerExpression.minus(other: ObservableLongValue): LongBinding = subtract(other) as LongBinding
operator fun IntegerExpression.minus(other: ObservableFloatValue): FloatBinding = subtract(other) as FloatBinding
operator fun IntegerExpression.minus(other: ObservableDoubleValue): DoubleBinding = subtract(other) as DoubleBinding
operator fun IntegerProperty.minusAssign(other: Number) { value -= other.toInt() }
operator fun IntegerProperty.minusAssign(other: ObservableNumberValue) { value -= other.intValue() }
operator fun IntegerExpression.unaryMinus(): IntegerBinding = negate()
operator fun IntegerProperty.dec(): IntegerProperty {
value--
return this
}
operator fun IntegerExpression.times(other: Int): IntegerBinding = multiply(other)
operator fun IntegerExpression.times(other: Long): LongBinding = multiply(other)
operator fun IntegerExpression.times(other: Float): FloatBinding = multiply(other)
operator fun IntegerExpression.times(other: Double): DoubleBinding = multiply(other)
operator fun IntegerExpression.times(other: ObservableIntegerValue): IntegerBinding = multiply(other) as IntegerBinding
operator fun IntegerExpression.times(other: ObservableLongValue): LongBinding = multiply(other) as LongBinding
operator fun IntegerExpression.times(other: ObservableFloatValue): FloatBinding = multiply(other) as FloatBinding
operator fun IntegerExpression.times(other: ObservableDoubleValue): DoubleBinding = multiply(other) as DoubleBinding
operator fun IntegerProperty.timesAssign(other: Number) { value *= other.toInt() }
operator fun IntegerProperty.timesAssign(other: ObservableNumberValue) { value *= other.intValue() }
operator fun IntegerExpression.div(other: Int): IntegerBinding = divide(other)
operator fun IntegerExpression.div(other: Long): LongBinding = divide(other)
operator fun IntegerExpression.div(other: Float): FloatBinding = divide(other)
operator fun IntegerExpression.div(other: Double): DoubleBinding = divide(other)
operator fun IntegerExpression.div(other: ObservableIntegerValue): IntegerBinding = divide(other) as IntegerBinding
operator fun IntegerExpression.div(other: ObservableLongValue): LongBinding = divide(other) as LongBinding
operator fun IntegerExpression.div(other: ObservableFloatValue): FloatBinding = divide(other) as FloatBinding
operator fun IntegerExpression.div(other: ObservableDoubleValue): DoubleBinding = divide(other) as DoubleBinding
operator fun IntegerProperty.divAssign(other: Number) { value /= other.toInt() }
operator fun IntegerProperty.divAssign(other: ObservableNumberValue) { value /= other.intValue() }
operator fun IntegerExpression.rem(other: Int): IntegerBinding = integerBinding(this) { get() % other }
operator fun IntegerExpression.rem(other: Long): LongBinding = longBinding(this) { get() % other }
operator fun IntegerExpression.rem(other: Float): FloatBinding = floatBinding(this) { get() % other }
operator fun IntegerExpression.rem(other: Double): DoubleBinding = doubleBinding(this) { get() % other }
operator fun IntegerExpression.rem(other: ObservableIntegerValue): IntegerBinding = integerBinding(this, other) { get() % other.get() }
operator fun IntegerExpression.rem(other: ObservableLongValue): LongBinding = longBinding(this, other) { get() % other.get() }
operator fun IntegerExpression.rem(other: ObservableFloatValue): FloatBinding = floatBinding(this, other) { get() % other.get() }
operator fun IntegerExpression.rem(other: ObservableDoubleValue): DoubleBinding = doubleBinding(this, other) { get() % other.get() }
operator fun IntegerProperty.remAssign(other: Number) { value %= other.toInt() }
operator fun IntegerProperty.remAssign(other: ObservableNumberValue) { value %= other.intValue() }
operator fun ObservableIntegerValue.rangeTo(other: ObservableIntegerValue): Sequence<IntegerProperty> {
val sequence = mutableListOf<IntegerProperty>()
for (i in get()..other.get()) {
sequence += SimpleIntegerProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableIntegerValue.rangeTo(other: Int): Sequence<IntegerProperty> {
val sequence = mutableListOf<IntegerProperty>()
for (i in get()..other) {
sequence += SimpleIntegerProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableIntegerValue.rangeTo(other: ObservableLongValue): Sequence<LongProperty> {
val sequence = mutableListOf<LongProperty>()
for (i in get()..other.get()) {
sequence += SimpleLongProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableIntegerValue.rangeTo(other: Long): Sequence<LongProperty> {
val sequence = mutableListOf<LongProperty>()
for (i in get()..other) {
sequence += SimpleLongProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableIntegerValue.compareTo(other: Number): Int {
if (get() > other.toDouble())
return 1
else if (get() < other.toDouble())
return -1
else
return 0
}
operator fun ObservableIntegerValue.compareTo(other: ObservableNumberValue): Int {
if (get() > other.doubleValue())
return 1
else if (get() < other.doubleValue())
return -1
else
return 0
}
operator fun LongExpression.plus(other: Number): LongBinding = add(other.toLong())
operator fun LongExpression.plus(other: Float): FloatBinding = add(other)
operator fun LongExpression.plus(other: Double): DoubleBinding = add(other)
operator fun LongExpression.plus(other: ObservableNumberValue): LongBinding = add(other) as LongBinding
operator fun LongExpression.plus(other: ObservableFloatValue): FloatBinding = add(other) as FloatBinding
operator fun LongExpression.plus(other: ObservableDoubleValue): DoubleBinding = add(other) as DoubleBinding
operator fun LongProperty.plusAssign(other: Number) { value += other.toLong() }
operator fun LongProperty.plusAssign(other: ObservableNumberValue) { value += other.longValue() }
operator fun LongProperty.inc(): LongProperty {
value++
return this
}
operator fun LongExpression.minus(other: Number): LongBinding = subtract(other.toLong())
operator fun LongExpression.minus(other: Float): FloatBinding = subtract(other)
operator fun LongExpression.minus(other: Double): DoubleBinding = subtract(other)
operator fun LongExpression.minus(other: ObservableNumberValue): LongBinding = subtract(other) as LongBinding
operator fun LongExpression.minus(other: ObservableFloatValue): FloatBinding = subtract(other) as FloatBinding
operator fun LongExpression.minus(other: ObservableDoubleValue): DoubleBinding = subtract(other) as DoubleBinding
operator fun LongProperty.minusAssign(other: Number) { value -= other.toLong() }
operator fun LongProperty.minusAssign(other: ObservableNumberValue) { value -= other.longValue() }
operator fun LongExpression.unaryMinus(): LongBinding = negate()
operator fun LongProperty.dec(): LongProperty {
value--
return this
}
operator fun LongExpression.times(other: Number): LongBinding = multiply(other.toLong())
operator fun LongExpression.times(other: Float): FloatBinding = multiply(other)
operator fun LongExpression.times(other: Double): DoubleBinding = multiply(other)
operator fun LongExpression.times(other: ObservableNumberValue): LongBinding = multiply(other) as LongBinding
operator fun LongExpression.times(other: ObservableFloatValue): FloatBinding = multiply(other) as FloatBinding
operator fun LongExpression.times(other: ObservableDoubleValue): DoubleBinding = multiply(other) as DoubleBinding
operator fun LongProperty.timesAssign(other: Number) { value *= other.toLong() }
operator fun LongProperty.timesAssign(other: ObservableNumberValue) { value *= other.longValue() }
operator fun LongExpression.div(other: Number): LongBinding = divide(other.toLong())
operator fun LongExpression.div(other: Float): FloatBinding = divide(other)
operator fun LongExpression.div(other: Double): DoubleBinding = divide(other)
operator fun LongExpression.div(other: ObservableNumberValue): LongBinding = divide(other) as LongBinding
operator fun LongExpression.div(other: ObservableFloatValue): FloatBinding = divide(other) as FloatBinding
operator fun LongExpression.div(other: ObservableDoubleValue): DoubleBinding = divide(other) as DoubleBinding
operator fun LongProperty.divAssign(other: Number) { value /= other.toLong() }
operator fun LongProperty.divAssign(other: ObservableNumberValue) { value /= other.longValue() }
operator fun LongExpression.rem(other: Number): LongBinding = longBinding(this) { get() % other.toLong() }
operator fun LongExpression.rem(other: Float): FloatBinding = floatBinding(this) { get() % other }
operator fun LongExpression.rem(other: Double): DoubleBinding = doubleBinding(this) { get() % other }
operator fun LongExpression.rem(other: ObservableNumberValue): LongBinding = longBinding(this, other) { this.get() % other.longValue() }
operator fun LongExpression.rem(other: ObservableFloatValue): FloatBinding = floatBinding(this, other) { this.get() % other.get() }
operator fun LongExpression.rem(other: ObservableDoubleValue): DoubleBinding = doubleBinding(this, other) { this.get() % other.get() }
operator fun LongProperty.remAssign(other: Number) { value %= other.toLong() }
operator fun LongProperty.remAssign(other: ObservableNumberValue) { value %= other.longValue() }
operator fun ObservableLongValue.rangeTo(other: ObservableLongValue): Sequence<LongProperty> {
val sequence = mutableListOf<LongProperty>()
for (i in get()..other.get()) {
sequence += SimpleLongProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableLongValue.rangeTo(other: Long): Sequence<LongProperty> {
val sequence = mutableListOf<LongProperty>()
for (i in get()..other) {
sequence += SimpleLongProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableLongValue.rangeTo(other: ObservableIntegerValue): Sequence<LongProperty> {
val sequence = mutableListOf<LongProperty>()
for (i in get()..other.get()) {
sequence += SimpleLongProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableLongValue.rangeTo(other: Int): Sequence<LongProperty> {
val sequence = mutableListOf<LongProperty>()
for (i in get()..other) {
sequence += SimpleLongProperty(i)
}
return sequence.asSequence()
}
operator fun ObservableLongValue.compareTo(other: Number): Int {
if (get() > other.toDouble())
return 1
else if (get() < other.toDouble())
return -1
else
return 0
}
operator fun ObservableLongValue.compareTo(other: ObservableNumberValue): Int {
if (get() > other.doubleValue())
return 1
else if (get() < other.doubleValue())
return -1
else
return 0
}
infix fun NumberExpression.gt(other: Int): BooleanBinding = greaterThan(other)
infix fun NumberExpression.gt(other: Long): BooleanBinding = greaterThan(other)
infix fun NumberExpression.gt(other: Float): BooleanBinding = greaterThan(other)
infix fun NumberExpression.gt(other: Double): BooleanBinding = greaterThan(other)
infix fun NumberExpression.gt(other: ObservableNumberValue): BooleanBinding = greaterThan(other)
infix fun NumberExpression.ge(other: Int): BooleanBinding = greaterThanOrEqualTo(other)
infix fun NumberExpression.ge(other: Long): BooleanBinding = greaterThanOrEqualTo(other)
infix fun NumberExpression.ge(other: Float): BooleanBinding = greaterThanOrEqualTo(other)
infix fun NumberExpression.ge(other: Double): BooleanBinding = greaterThanOrEqualTo(other)
infix fun NumberExpression.ge(other: ObservableNumberValue): BooleanBinding = greaterThanOrEqualTo(other)
infix fun NumberExpression.eq(other: Int): BooleanBinding = isEqualTo(other)
infix fun NumberExpression.eq(other: Long): BooleanBinding = isEqualTo(other)
infix fun NumberExpression.eq(other: ObservableNumberValue): BooleanBinding = isEqualTo(other)
infix fun NumberExpression.le(other: Int): BooleanBinding = lessThanOrEqualTo(other)
infix fun NumberExpression.le(other: Long): BooleanBinding = lessThanOrEqualTo(other)
infix fun NumberExpression.le(other: Float): BooleanBinding = lessThanOrEqualTo(other)
infix fun NumberExpression.le(other: Double): BooleanBinding = lessThanOrEqualTo(other)
infix fun NumberExpression.le(other: ObservableNumberValue): BooleanBinding = lessThanOrEqualTo(other)
infix fun NumberExpression.lt(other: Int): BooleanBinding = lessThan(other)
infix fun NumberExpression.lt(other: Long): BooleanBinding = lessThan(other)
infix fun NumberExpression.lt(other: Float): BooleanBinding = lessThan(other)
infix fun NumberExpression.lt(other: Double): BooleanBinding = lessThan(other)
infix fun NumberExpression.lt(other: ObservableNumberValue): BooleanBinding = lessThan(other)
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
operator fun BooleanExpression.not(): BooleanBinding = not()
infix fun BooleanExpression.and(other: Boolean): BooleanBinding = and(SimpleBooleanProperty(other))
infix fun BooleanExpression.and(other: ObservableBooleanValue): BooleanBinding = and(other)
infix fun BooleanExpression.or(other: Boolean): BooleanBinding = or(SimpleBooleanProperty(other))
infix fun BooleanExpression.or(other: ObservableBooleanValue): BooleanBinding = or(other)
infix fun BooleanExpression.xor(other: Boolean): BooleanBinding = booleanBinding(this) { get() xor other }
infix fun BooleanExpression.xor(other: ObservableBooleanValue): BooleanBinding = booleanBinding(this, other) { get() xor other.get() }
infix fun BooleanExpression.eq(other: Boolean): BooleanBinding = isEqualTo(SimpleBooleanProperty(other))
infix fun BooleanExpression.eq(other: ObservableBooleanValue): BooleanBinding = isEqualTo(other)
operator fun StringExpression.plus(other: Any): StringExpression = concat(other)
operator fun StringProperty.plusAssign(other: Any) { value += other }
operator fun StringExpression.get(index: Int): Binding<Char?> = objectBinding(this) {
if (index < get().length)
get()[index]
else
null
}
operator fun StringExpression.get(index: ObservableIntegerValue): Binding<Char?> = objectBinding(this, index) {
if (index < get().length)
get()[index.get()]
else
null
}
operator fun StringExpression.get(start: Int, end: Int): StringBinding = stringBinding(this) { get().subSequence(start, end).toString() }
operator fun StringExpression.get(start: ObservableIntegerValue, end: Int): StringBinding = stringBinding(this, start) { get().subSequence(start.get(), end).toString() }
operator fun StringExpression.get(start: Int, end: ObservableIntegerValue): StringBinding = stringBinding(this, end) { get().subSequence(start, end.get()).toString() }
operator fun StringExpression.get(start: ObservableIntegerValue, end: ObservableIntegerValue): StringBinding = stringBinding(this, start, end) { get().subSequence(start.get(), end.get()).toString() }
operator fun StringExpression.unaryMinus(): StringBinding = stringBinding(this) { get().reversed() }
operator fun StringExpression.compareTo(other: String): Int = get().compareTo(other)
operator fun StringExpression.compareTo(other: ObservableStringValue): Int = get().compareTo(other.get())
infix fun StringExpression.gt(other: String): BooleanBinding = greaterThan(other)
infix fun StringExpression.gt(other: ObservableStringValue): BooleanBinding = greaterThan(other)
infix fun StringExpression.ge(other: String): BooleanBinding = greaterThanOrEqualTo(other)
infix fun StringExpression.ge(other: ObservableStringValue): BooleanBinding = greaterThanOrEqualTo(other)
infix fun StringExpression.eq(other: String): BooleanBinding = isEqualTo(other)
infix fun StringExpression.eq(other: ObservableStringValue): BooleanBinding = isEqualTo(other)
infix fun StringExpression.le(other: String): BooleanBinding = lessThanOrEqualTo(other)
infix fun StringExpression.le(other: ObservableStringValue): BooleanBinding = lessThanOrEqualTo(other)
infix fun StringExpression.lt(other: String): BooleanBinding = lessThan(other)
infix fun StringExpression.lt(other: ObservableStringValue): BooleanBinding = lessThan(other)
infix fun StringExpression.eqIgnoreCase(other: String): BooleanBinding = isEqualToIgnoreCase(other)
infix fun StringExpression.eqIgnoreCase(other: ObservableStringValue): BooleanBinding = isEqualToIgnoreCase(other)
fun <T> ObservableValue<T>.integerBinding(vararg dependencies: Observable, op: (T?) -> Int): IntegerBinding
= Bindings.createIntegerBinding(Callable { op(value) }, this, *dependencies)
fun <T : Any> integerBinding(receiver: T, vararg dependencies: Observable, op: T.() -> Int): IntegerBinding
= Bindings.createIntegerBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
fun <T> ObservableValue<T>.longBinding(vararg dependencies: Observable, op: (T?) -> Long): LongBinding
= Bindings.createLongBinding(Callable { op(value) }, this, *dependencies)
fun <T : Any> longBinding(receiver: T, vararg dependencies: Observable, op: T.() -> Long): LongBinding
= Bindings.createLongBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
fun <T> ObservableValue<T>.doubleBinding(vararg dependencies: Observable, op: (T?) -> Double): DoubleBinding
= Bindings.createDoubleBinding(Callable { op(value) }, this, *dependencies)
fun <T : Any> doubleBinding(receiver: T, vararg dependencies: Observable, op: T.() -> Double): DoubleBinding
= Bindings.createDoubleBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
fun <T> ObservableValue<T>.floatBinding(vararg dependencies: Observable, op: (T?) -> Float): FloatBinding
= Bindings.createFloatBinding(Callable { op(value) }, this, *dependencies)
fun <T : Any> floatBinding(receiver: T, vararg dependencies: Observable, op: T.() -> Float): FloatBinding
= Bindings.createFloatBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
fun <T> ObservableValue<T>.booleanBinding(vararg dependencies: Observable, op: (T?) -> Boolean): BooleanBinding =
Bindings.createBooleanBinding(Callable { op(value) }, this, *dependencies)
fun <T : Any> booleanBinding(receiver: T, vararg dependencies: Observable, op: T.() -> Boolean): BooleanBinding
= Bindings.createBooleanBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
/**
* A Boolean binding that tracks all items in an observable list and create an observable boolean
* value by anding together an observable boolean representing each element in the observable list.
* Whenever the list changes, the binding is updated as well
*/
fun <T : Any> booleanListBinding(list: ObservableList<T>, itemToBooleanExpr: T.() -> BooleanExpression): BooleanExpression {
val facade = SimpleBooleanProperty()
fun rebind() {
if (list.isEmpty()) {
facade.unbind()
facade.value = false
} else {
facade.cleanBind(list.map(itemToBooleanExpr).reduce { a, b -> a.and(b) })
}
}
list.onChange { rebind() }
rebind()
return facade
}
fun <T> ObservableValue<T>.stringBinding(vararg dependencies: Observable, op: (T?) -> String?): StringBinding
= Bindings.createStringBinding(Callable { op(value) }, this, *dependencies)
fun <T : Any> stringBinding(receiver: T, vararg dependencies: Observable, op: T.() -> String?): StringBinding =
Bindings.createStringBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
fun <T, R> ObservableValue<T>.objectBinding(vararg dependencies: Observable, op: (T?) -> R?): Binding<R?>
= Bindings.createObjectBinding(Callable { op(value) }, this, *dependencies)
fun <T : Any, R> objectBinding(receiver: T, vararg dependencies: Observable, op: T.() -> R?): ObjectBinding<R?>
= Bindings.createObjectBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
fun <T : Any, R> nonNullObjectBinding(receiver: T, vararg dependencies: Observable, op: T.() -> R): ObjectBinding<R>
= Bindings.createObjectBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))
private fun <T> createObservableArray(receiver: T, vararg dependencies: Observable): Array<out Observable> =
if (receiver is Observable) arrayOf(receiver, *dependencies) else dependencies
/**
* Assign the value from the creator to this WritableValue if and only if it is currently null
*/
fun <T> WritableValue<T>.assignIfNull(creator: () -> T) {
if (value == null) value = creator()
}
fun Double.toProperty(): DoubleProperty = SimpleDoubleProperty(this)
fun Float.toProperty(): FloatProperty = SimpleFloatProperty(this)
fun Long.toProperty(): LongProperty = SimpleLongProperty(this)
fun Int.toProperty(): IntegerProperty = SimpleIntegerProperty(this)
fun Boolean.toProperty(): BooleanProperty = SimpleBooleanProperty(this)
fun String.toProperty(): StringProperty = SimpleStringProperty(this)
fun String?.toProperty() = SimpleStringProperty(this ?: "")
fun Double?.toProperty() = SimpleDoubleProperty(this ?: 0.0)
fun Float?.toProperty() = SimpleFloatProperty(this ?: 0.0F)
fun Long?.toProperty() = SimpleLongProperty(this ?: 0L)
fun Boolean?.toProperty() = SimpleBooleanProperty(this ?: false)
fun <T : Any> T?.toProperty() = SimpleObjectProperty<T>(this)
class WeakReferenceDelegate<T>(val creator: () -> T) {
var weakReference : WeakReference<T>? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
var ret = weakReference?.get()
if (ret == null) {
ret = creator()
weakReference = WeakReference(ret)
}
return ret!!
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
}
}

View File

@@ -183,9 +183,9 @@ public abstract class Task {
return new TaskExecutor(this);
}
public final TaskExecutor executor(TaskListener taskListener) {
public final TaskExecutor executor(Function<TaskExecutor, TaskListener> taskListener) {
TaskExecutor executor = new TaskExecutor(this);
executor.setTaskListener(taskListener);
executor.setTaskListener(taskListener.apply(executor));
return executor;
}
@@ -204,13 +204,21 @@ public abstract class Task {
}
public final TaskExecutor subscribe(Scheduler scheduler, ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
return subscribe(of(closure, scheduler));
return subscribe(of(scheduler, closure));
}
public final TaskExecutor subscribe(Scheduler scheduler, ExceptionalRunnable<?> closure) {
return subscribe(of(scheduler, i -> closure.run()));
}
public final TaskExecutor subscribe(ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
return subscribe(of(closure));
}
public final TaskExecutor subscribe(ExceptionalRunnable<?> closure) {
return subscribe(of(closure));
}
public final Task then(Task b) {
return then(s -> b);
}
@@ -237,13 +245,17 @@ public abstract class Task {
}
public static Task of(ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
return of(closure, Schedulers.defaultScheduler());
return of(Schedulers.defaultScheduler(), closure);
}
public static Task of(ExceptionalConsumer<AutoTypingMap<String>, ?> closure, Scheduler scheduler) {
public static Task of(Scheduler scheduler, ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
return new SimpleTask(closure, scheduler);
}
public static Task of(Scheduler scheduler, ExceptionalRunnable<?> closure) {
return new SimpleTask(i -> closure.run(), scheduler);
}
public static <V> TaskResult<V> ofResult(String id, Callable<V> callable) {
return new TaskCallable<>(id, callable);
}

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
@@ -38,6 +39,10 @@ public final class AutoTypingMap<K> {
return (V) impl.get(key);
}
public <V> Optional<V> getOptional(K key) {
return Optional.ofNullable(get(key));
}
public void set(K key, Object value) {
if (value != null)
impl.put(key, value);

View File

@@ -71,12 +71,12 @@ public final class Lang {
}
}
public static <T, R, E extends Exception> Function<T, R> hideException(ExceptionalFunction<T, R, E> function) {
public static <T, R, E extends Exception> Function<T, R> hideFunction(ExceptionalFunction<T, R, E> function) {
return r -> invoke(function, r);
}
public static <T, R, E extends Exception> Function<T, R> liftException(ExceptionalFunction<T, R, E> function) throws E {
return hideException(function);
public static <T, R, E extends Exception> Function<T, R> liftFunction(ExceptionalFunction<T, R, E> function) throws E {
return hideFunction(function);
}
/**
@@ -107,6 +107,33 @@ public final class Lang {
return hideException(supplier);
}
/**
* This method will call a method without checked exceptions
* by treating the compiler.
*
* If this method throws a checked exception,
* it will still abort the application because of the exception.
*
* @param <T> type of result.
* @param consumer your method.
* @return the result of the method to invoke.
*/
public static <T, E extends Exception> void invokeConsumer(ExceptionalConsumer<T, E> consumer, T t) {
try {
consumer.accept(t);
} catch (Exception e) {
throwable(e);
}
}
public static <T, E extends Exception> Consumer<T> hideConsumer(ExceptionalConsumer<T, E> consumer) {
return it -> invokeConsumer(consumer, it);
}
public static <T, E extends Exception> Consumer<T> liftConsumer(ExceptionalConsumer<T, E> consumer) throws E {
return hideConsumer(consumer);
}
public static <E extends Exception> boolean test(ExceptionalSupplier<Boolean, E> r) {
try {
return r.get();
@@ -248,17 +275,25 @@ public final class Lang {
return () -> asIterator(enumeration);
}
public static int parseInt(String string, int defaultValue) {
public static int parseInt(Object string, int defaultValue) {
try {
return Integer.parseInt(string);
return Integer.parseInt(string.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
public static Integer toIntOrNull(String string) {
public static Integer toIntOrNull(Object string) {
try {
return Integer.parseInt(string);
return Integer.parseInt(string.toString());
} catch (NumberFormatException e) {
return null;
}
}
public static Double toDoubleOrNull(Object string) {
try {
return Double.parseDouble(string.toString());
} catch (NumberFormatException e) {
return null;
}
@@ -268,4 +303,9 @@ public final class Lang {
for (T a : t) if (a != null) return a;
return null;
}
public static <T> T apply(T t, Consumer<T> consumer) {
consumer.accept(t);
return t;
}
}

View File

@@ -1,152 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.download.MojangDownloadProvider
import org.jackhuang.hmcl.game.DefaultGameRepository
import org.jackhuang.hmcl.launch.DefaultLauncher
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.game.minecraftVersion
import org.jackhuang.hmcl.launch.ProcessListener
import org.jackhuang.hmcl.util.makeCommand
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskListener
import org.jackhuang.hmcl.util.Log4jLevel
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
class Test {
val ss = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 1080))
val repository = DefaultGameRepository(File(".minecraft").absoluteFile)
val dependency = DefaultDependencyManager(
repository = repository,
downloadProvider = MojangDownloadProvider,
proxy = ss)
init {
repository.refreshVersions()
}
fun launch() {
val launcher = DefaultLauncher(
repository = repository,
versionId = "test",
account = OfflineAccount.fromUsername("player007").logIn(),
options = LaunchOptions(gameDir = repository.baseDirectory),
listener = object : ProcessListener {
override fun onLog(log: String, level: Log4jLevel) {
println(log)
}
override fun onExit(exitCode: Int, exitType: ProcessListener.ExitType) {
println("Process exited then exit code $exitCode")
}
},
isDaemon = false
)
println(makeCommand(launcher.rawCommandLine))
launcher.launch()
try {
Thread.sleep(Long.MAX_VALUE)
} catch (e: InterruptedException) {
return
}
}
fun downloadNewVersion() {
val thread = Thread.currentThread()
dependency.gameBuilder()
.name("test")
.gameVersion("1.12")
.version("forge", "14.21.1.2426")
.version("liteloader", "1.12-SNAPSHOT-4")
.version("optifine", "HD_U_C4")
.buildAsync().executor().apply {
taskListener = taskListener(thread)
}.start()
try {
Thread.sleep(Long.MAX_VALUE)
} catch (e: InterruptedException) {
return
}
}
fun completeGame() {
val thread = Thread.currentThread()
val version = repository.getVersion("test").resolve(repository)
dependency.checkGameCompletionAsync(version).executor().apply {
taskListener = taskListener(thread)
}.start()
try {
Thread.sleep(Long.MAX_VALUE)
} catch (e: InterruptedException) {
return
}
}
fun installForge() {
val thread = Thread.currentThread()
val version = repository.getVersion("test").resolve(repository)
val minecraftVersion = minecraftVersion(repository.getVersionJar(version)) ?: ""
// optifine HD_U_C4
// forge 14.21.1.2426
// liteloader 1.12-SNAPSHOT-4
dependency.installLibraryAsync(minecraftVersion, version, "liteloader", "1.12-SNAPSHOT-4").executor().apply {
taskListener = taskListener(thread)
}.start()
try {
Thread.sleep(Long.MAX_VALUE)
} catch (e: InterruptedException) {
return
}
}
fun refreshAsync() {
val thread = Thread.currentThread()
LiteLoaderVersionList.refreshAsync(BMCLAPIDownloadProvider).executor().apply {
taskListener = taskListener(thread)
}.start()
try {
Thread.sleep(Long.MAX_VALUE)
} catch (e: InterruptedException) {
return
}
}
fun taskListener(thread: Thread) = object : TaskListener {
override fun onReady(task: Task) {
}
override fun onFinished(task: Task) {
}
override fun onFailed(task: Task, throwable: Throwable) {
}
override fun onTerminate() {
thread.interrupt()
}
}
}

108
README.md
View File

@@ -7,50 +7,76 @@ HMCL is a Minecraft launcher which supports Mod management, game customizing, au
## Contribution
If you want to submit a pull request, there're some requirements:
* IDE: Netbeans 8.1
* Compiler: Java 1.8 and libraries only supports Java 1.7(because of retrolambda).
* IDE: Intellij IDEA.
* Compiler: Java 1.8.
* Do NOT modify `gradle` files.
## Code
* package `HMCLCore/org.jackhuang.hmcl.util`: HMCL development utilities.
* package `HMCL/org.jackhuang.hmcl`: HMCL UI core.
* package `HMCLCore/org.jackhuang.hmcl.core`: HMCL game launcher core.
* package `HMCLAPI(HMCL)/org.jackhuang.hmcl.api`: HMCL API, see API section.
* Folder `HMCLCore/src/main/resources/org/jackhuang/hmcl/lang` contains language files.
## HMCLCore
Now HMCLCore is independent and you can use HMCLCore as a library to launch your game.
## Pay Attention
* When you do decide to modify this app, please and you MUST delete `HMCL/org.jackhuang.hmcl.util.CrashReporter`, or errors your code cause will be sent to my server.
* package `org.jackhuang.hmcl.util.logging`: repackaged Apache Log4j, Apache License 2.0.
* package `com.google.gson`: Apache License 2.0
* package `org.jackhuang.hmcl.laf.ui`: contains some NimbusLAF's code belonging to Sun Microsystems under LGPL.
### GameRepository
Create a game repository `repository` to manage a minecraft installation. Like this.
```java
DefaultGameRepository repository = new DefaultGameRepository(new File(".minecraft").getAbsoluteFile());
```
## API
HMCLAPI is based on Event bus. There are all events below.
* org.jackhuang.hmcl.api.event
- OutOfDateEvent - you can cancel checking new versions and upgrading by this event.
* org.jackhuang.hmcl.api.event.config
- AuthenticatorChangedEvent
- DownloadTypeChangedEvent
- ThemeChangedEvent
* org.jackhuang.hmcl.api.event.launch
- LaunchEvent
- LaunchSucceededEvent
- LaunchingStateChangedEvent
- ProcessingLaunchOptionsEvent
- ProcessingLoginResultEvent
* org.jackhuang.hmcl.api.event.process
- JVMLaunchFailedEvent
- JavaProcessExitedAbnormallyEvent
- JavaProcessStartingEvent
- JavaProcessStoppedEvent
* org.jackhuang.hmcl.api.event.version
- LoadedOneVersionEvent
- RefreshedVersionsEvent
- RefreshingVersionsEvent
You should put where your minecraft installation is to the only argument of the constructor of `DefaultGameRepository`.
You can also add tabs to root window or add authenticators through IPlugin.
### Launching
Now you can launch game by constructing a `DefaultLauncher`.
```java
DefaultLauncher launcher = new DefaultLauncher(
repository, // GameRepository
"test", // Your minecraft version name
OfflineAccountFactory.INSTANCE.fromUsername("player007").logIn(MultiCharacterSelector.DEFAULT), // account
// or YggdrasilAccountFactory.INSTANCE.fromUsername(username, password).logIn
new LaunchOptions.Builder()
.setGameDir(repository.getBaseDirectory())
.setMaxMemory(...)
.setJava(...)
.setJavaArgs(...)
.setMinecraftArgs(...)
.setHeight(...)
.setWidth(...)
...
.create(),
new ProcessListener() { // listening the process state.
@Override
public void onLog(String log, Log4jLevel level) { // new console log
System.out.println(log);
}
### Remember
* A valid plugin will have a main class that implements `org.jackhuang.hmcl.api.IPlugin`. HMCL will search all jar files in `plugins` folder and load classes that implements IPlugin.
* If you want to debug, use option: `--plugin=<Your IPlugin Class Name>` and add your jar to classpath.
* You'd better only access `org.jackhuang.hmcl.api.*`, and other classes may change in different versions.
@Override
public void onExit(int exitCode, ExitType exitType) { // process exited
System.out.println("Process exited then exit code " + exitCode);
}
},
false // true if launcher process exits, listening thread exit too.
);
```
Now you can simply call `launcher.launch()` to launch the game.
If you want the command line, just call `launcher.getRawCommandLine`. Also, `StringUtils.makeCommand` might be useful.
### Downloading
HMCLCore just owns a simple way to download a new game.
```java
DefaultDependencyManager dependency = new DefaultDependencyManager(repository, MojangDownloadProvider.INSTANCE, proxy);
```
`repository` is your `GameRepository`. `MojangDownloadProvider.INSTANCE` means that we download files from mojang servers. If you want BMCLAPI, `BMCLAPIDownloadProvider.INSTANCE` is just for you. `proxy` is `java.net.Proxy`, if you have a proxy, put it here, or `Proxy.NO_PROXY`.
Now `GameBuilder` can build a game.
```
Task gameBuildingTask = dependency.gameBuilder()
.name("test")
.gameVersion("1.12") // Minecraft version
.version("forge", "14.21.1.2426") // Forge version
.version("liteloader", "1.12-SNAPSHOT-4") // LiteLoader version
.version("optifine", "HD_U_C4") // OptiFine version
.buildAsync()
```
Nowadays HMCLCore only supports Forge, LiteLoader and OptiFine auto-installing.
`buildAsync` will return a `Task`, you can call `Task.executor()::start` or simply `Task::start` to start this task. If you want to monitor the execution of tasks, you should see `TaskExecutor` and `Task::executor`.
## HMCL
JavaFX version of HMCL does not support old APIs.

View File

@@ -26,7 +26,6 @@ buildscript {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'net.sf.proguard:proguard-gradle:5.3.3'
}
}
@@ -50,16 +49,7 @@ allprojects {
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "com.google.code.gson:gson:2.8.1"
compile "org.apache.commons:commons-compress:1.8.1"
compile "org.tukaani:xz:1.6"