diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index b9ce083ea..3018f51fa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -30,6 +30,6 @@ public final class Metadata { public static final String TITLE = NAME + " " + VERSION; public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link"); - public static final String CONTACT_URL = "https://www.huangyuhui.net/hmcl.php"; + public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact"; public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html"; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/event/ProfileChangedEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/event/ProfileChangedEvent.java deleted file mode 100644 index 2155d94cc..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/event/ProfileChangedEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.event; - -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.util.ToStringBuilder; - -/** - * This event gets fired when the selected profile changed. - *
- * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} - * @author huangyuhui - */ -public final class ProfileChangedEvent extends Event { - private final Profile profile; - - /** - * Constructor. - * - * @param source {@link org.jackhuang.hmcl.setting.Settings} - * @param profile the new profile. - */ - public ProfileChangedEvent(Object source, Profile profile) { - super(source); - - this.profile = profile; - } - - public Profile getProfile() { - return profile; - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("source", source) - .append("profile", profile) - .toString(); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/event/ProfileLoadingEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/event/ProfileLoadingEvent.java deleted file mode 100644 index 81ff05379..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/event/ProfileLoadingEvent.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.event; - -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.util.ToStringBuilder; - -import java.util.Collection; -import java.util.Collections; - -/** - * This event gets fired when loading profiles. - *
- * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} - * - * @author huangyuhui - */ -public class ProfileLoadingEvent extends Event { - - private final Collection profiles; - - /** - * Constructor. - * - * @param source {@link org.jackhuang.hmcl.setting.Settings} - */ - public ProfileLoadingEvent(Object source, Collection profiles) { - super(source); - - this.profiles = Collections.unmodifiableCollection(profiles); - } - - public Collection getProfiles() { - return profiles; - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("source", source) - .append("profiles", profiles) - .toString(); - } -} - diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java index faef60043..b576f0978 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java @@ -101,10 +101,6 @@ public final class AccountHelper { private final boolean refresh; private final List dependencies = new LinkedList<>(); - public SkinLoadTask(YggdrasilAccount account) { - this(account, false); - } - public SkinLoadTask(YggdrasilAccount account, boolean refresh) { this.account = account; this.refresh = refresh; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java index e1e15a72d..83ad03709 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java @@ -33,8 +33,6 @@ import java.util.List; import java.util.Optional; import java.util.logging.Level; -import static org.jackhuang.hmcl.setting.ConfigHolder.config; - /** * @author huangyuhui */ diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java index bf7bf4036..f586b2e29 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -21,9 +21,7 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.launch.DefaultLauncher; import org.jackhuang.hmcl.launch.ProcessListener; -import org.jackhuang.hmcl.util.CommandBuilder; -import java.util.List; import java.util.Map; /** diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 0033ae6bd..4d4ea3f04 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -34,8 +34,6 @@ import java.io.IOException; import java.util.*; import java.util.logging.Level; -import static org.jackhuang.hmcl.setting.ConfigHolder.config; - public class HMCLGameRepository extends DefaultGameRepository { private final Profile profile; private final Map versionSettings = new HashMap<>(); @@ -81,7 +79,6 @@ public class HMCLGameRepository extends DefaultGameRepository { @Override public File getLibraryFile(Version version, Library lib) { - VersionSetting vs = profile.getVersionSetting(version.getId()); File self = super.getLibraryFile(version, lib); if (Settings.instance().isCommonDirectoryDisabled() || self.exists()) return self; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index b90162f04..f3a223794 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -35,7 +35,6 @@ 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; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index 134b8cd0c..422e236ed 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -92,7 +92,7 @@ public final class Accounts { } private static ObservableList accounts = observableArrayList(account -> new Observable[] { account }); - private static ReadOnlyListProperty accountsWrapper = new ReadOnlyListWrapper<>(accounts); + private static ReadOnlyListWrapper accountsWrapper = new ReadOnlyListWrapper<>(accounts); private static ObjectProperty selectedAccount = new SimpleObjectProperty() { { @@ -194,7 +194,7 @@ public final class Accounts { } public static ReadOnlyListProperty accountsProperty() { - return accountsWrapper; + return accountsWrapper.getReadOnlyProperty(); } public static Account getSelectedAccount() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 355cd3b8f..69d12c3f3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -153,6 +153,9 @@ public final class Config implements Cloneable, Observable { @SerializedName("updateChannel") private ObjectProperty updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE); + @SerializedName("enableMainPageGameList") + private BooleanProperty enableMainPageGameList = new SimpleBooleanProperty(false); + @SerializedName("_version") private IntegerProperty configVersion = new SimpleIntegerProperty(0); @@ -450,4 +453,16 @@ public final class Config implements Cloneable, Observable { public void setUpdateChannel(UpdateChannel updateChannel) { this.updateChannel.set(updateChannel); } + + public boolean isEnableMainPageGameList() { + return enableMainPageGameList.get(); + } + + public BooleanProperty enableMainPageGameListProperty() { + return enableMainPageGameList; + } + + public void setEnableMainPageGameList(boolean enableMainPageGameList) { + this.enableMainPageGameList.set(enableMainPageGameList); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java index 6fa7c0651..78a6ca28a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java @@ -18,86 +18,99 @@ package org.jackhuang.hmcl.setting; import com.google.gson.*; +import com.jfoenix.concurrency.JFXUtilities; import javafx.beans.InvalidationListener; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.Observable; +import javafx.beans.property.*; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; import org.jackhuang.hmcl.game.HMCLDependencyManager; import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.mod.ModManager; -import org.jackhuang.hmcl.util.ImmediateObjectProperty; -import org.jackhuang.hmcl.util.ImmediateStringProperty; -import org.jackhuang.hmcl.util.ToStringBuilder; +import org.jackhuang.hmcl.ui.WeakListenerHelper; +import org.jackhuang.hmcl.util.*; import java.io.File; import java.lang.reflect.Type; import java.util.Optional; +import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; + /** * * @author huangyuhui */ -public final class Profile { - +public final class Profile implements Observable { + private final WeakListenerHelper helper = new WeakListenerHelper(); private final HMCLGameRepository repository; private final ModManager modManager; - private final ImmediateObjectProperty gameDirProperty; + private final StringProperty selectedVersion = new SimpleStringProperty(); - public ImmediateObjectProperty gameDirProperty() { - return gameDirProperty; + public StringProperty selectedVersionProperty() { + return selectedVersion; + } + + public String getSelectedVersion() { + return selectedVersion.get(); + } + + public void setSelectedVersion(String selectedVersion) { + this.selectedVersion.set(selectedVersion); + } + + private final ObjectProperty gameDir; + + public ObjectProperty gameDirProperty() { + return gameDir; } public File getGameDir() { - return gameDirProperty.get(); + return gameDir.get(); } public void setGameDir(File gameDir) { - gameDirProperty.set(gameDir); + this.gameDir.set(gameDir); } - private final ImmediateObjectProperty globalProperty = new ImmediateObjectProperty<>(this, "global", new VersionSetting()); + private final ReadOnlyObjectWrapper global = new ReadOnlyObjectWrapper<>(this, "global"); - public ImmediateObjectProperty globalProperty() { - return globalProperty; + public ReadOnlyObjectProperty globalProperty() { + return global.getReadOnlyProperty(); } public VersionSetting getGlobal() { - return globalProperty.get(); + return global.get(); } - private void setGlobal(VersionSetting global) { - if (global == null) - global = new VersionSetting(); - globalProperty.set(global); - } - - private final ImmediateStringProperty nameProperty; + private final ImmediateStringProperty name; public ImmediateStringProperty nameProperty() { - return nameProperty; + return name; } public String getName() { - return nameProperty.get(); + return name.get(); } public void setName(String name) { - nameProperty.set(name); + this.name.set(name); } - private BooleanProperty useRelativePathProperty = new SimpleBooleanProperty(this, "useRelativePath", false); + private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false); public BooleanProperty useRelativePathProperty() { - return useRelativePathProperty(); + return useRelativePath; } public boolean isUseRelativePath() { - return useRelativePathProperty.get(); + return useRelativePath.get(); } public void setUseRelativePath(boolean useRelativePath) { - useRelativePathProperty.set(useRelativePath); + this.useRelativePath.set(useRelativePath); } public Profile(String name) { @@ -105,12 +118,33 @@ public final class Profile { } public Profile(String name, File initialGameDir) { - nameProperty = new ImmediateStringProperty(this, "name", name); - gameDirProperty = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir); + this(name, initialGameDir, new VersionSetting()); + } + + public Profile(String name, File initialGameDir, VersionSetting global) { + this.name = new ImmediateStringProperty(this, "name", name); + gameDir = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir); repository = new HMCLGameRepository(this, initialGameDir); modManager = new ModManager(repository); + this.global.set(global == null ? new VersionSetting() : global); - gameDirProperty.addListener((a, b, newValue) -> repository.changeDirectory(newValue)); + gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue)); + selectedVersion.addListener(o -> checkSelectedVersion()); + helper.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion())); + + addPropertyChangedListener(onInvalidating(this::invalidate)); + } + + private void checkSelectedVersion() { + if (!repository.isLoaded()) return; + String newValue = selectedVersion.get(); + if (!repository.hasVersion(newValue)) { + Optional version = repository.getVersions().stream().findFirst().map(Version::getId); + if (version.isPresent()) + selectedVersion.setValue(version.get()); + else if (StringUtils.isNotBlank(newValue)) + selectedVersion.setValue(null); + } } public HMCLGameRepository getRepository() { @@ -170,12 +204,29 @@ public final class Profile { .toString(); } - public void addPropertyChangedListener(InvalidationListener listener) { - nameProperty.addListener(listener); - globalProperty.addListener(listener); - gameDirProperty.addListener(listener); - useRelativePathProperty.addListener(listener); - globalProperty.get().addPropertyChangedListener(listener); + private void addPropertyChangedListener(InvalidationListener listener) { + name.addListener(listener); + global.addListener(listener); + gameDir.addListener(listener); + useRelativePath.addListener(listener); + global.get().addPropertyChangedListener(listener); + selectedVersion.addListener(listener); + } + + private ObservableHelper observableHelper = new ObservableHelper(this); + + @Override + public void addListener(InvalidationListener listener) { + observableHelper.addListener(listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + observableHelper.removeListener(listener); + } + + protected void invalidate() { + observableHelper.invalidate(); } public static final class Serializer implements JsonSerializer, JsonDeserializer { @@ -193,6 +244,7 @@ public final class Profile { jsonObject.add("global", context.serialize(src.getGlobal())); jsonObject.addProperty("gameDir", src.getGameDir().getPath()); jsonObject.addProperty("useRelativePath", src.isUseRelativePath()); + jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion()); return jsonObject; } @@ -203,9 +255,8 @@ public final class Profile { JsonObject obj = (JsonObject) json; String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""); - Profile profile = new Profile("Default", new File(gameDir)); - profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class)); - + Profile profile = new Profile("Default", new File(gameDir), context.deserialize(obj.get("global"), VersionSetting.class)); + profile.setSelectedVersion(Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse("")); profile.setUseRelativePath(Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false)); return profile; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java index 5b6d69b7f..7bfe7496f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java @@ -17,6 +17,16 @@ */ package org.jackhuang.hmcl.setting; +import javafx.beans.Observable; +import javafx.beans.property.*; +import javafx.collections.ObservableList; +import org.jackhuang.hmcl.Launcher; + +import java.util.HashSet; + +import static javafx.collections.FXCollections.observableArrayList; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; +import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class Profiles { @@ -37,4 +47,104 @@ public final class Profiles { return profile.getName(); } } + + private static final ObservableList profiles = observableArrayList(profile -> new Observable[] { profile }); + private static final ReadOnlyListWrapper profilesWrapper = new ReadOnlyListWrapper<>(profiles); + + private static ObjectProperty selectedProfile = new SimpleObjectProperty() { + { + profiles.addListener(onInvalidating(this::invalidated)); + } + + @Override + protected void invalidated() { + Profile profile = get(); + if (profiles.isEmpty()) { + if (profile != null) { + set(null); + return; + } + } else { + if (!profiles.contains(profile)) { + set(profiles.get(0)); + return; + } + } + + if (!initialized) + return; + config().setSelectedProfile(profile == null ? "" : profile.getName()); + } + }; + + private static void checkProfiles() { + if (profiles.isEmpty()) { + Profile current = new Profile(Profiles.DEFAULT_PROFILE); + current.setUseRelativePath(true); + + Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY); + + profiles.addAll(current, home); + } + } + + /** + * True if {@link #init()} hasn't been called. + */ + private static boolean initialized = false; + + static { + profiles.addListener(onInvalidating(ConfigHolder::markConfigDirty)); + + profiles.addListener(onInvalidating(Profiles::checkProfiles)); + + selectedProfile.addListener((a, b, newValue) -> { + if (newValue != null) + newValue.getRepository().refreshVersionsAsync().start(); + }); + } + + /** + * Called when it's ready to load profiles from {@link ConfigHolder#config()}. + */ + static void init() { + if (initialized) + throw new IllegalStateException("Already initialized"); + + HashSet names = new HashSet<>(); + config().getConfigurations().forEach((name, profile) -> { + if (!names.add(name)) return; + profile.setName(name); + profiles.add(profile); + }); + checkProfiles(); + + initialized = true; + + selectedProfile.set( + profiles.stream() + .filter(it -> it.getName().equals(config().getSelectedProfile())) + .findFirst() + .get()); + } + + public static ObservableList getProfiles() { + return profiles; + } + + public static ReadOnlyListProperty profilesProperty() { + return profilesWrapper.getReadOnlyProperty(); + } + + public static Profile getSelectedProfile() { + return selectedProfile.get(); + } + + public static void setSelectedProfile(Profile profile) { + selectedProfile.set(profile); + } + + public static ObjectProperty selectedProfileProperty() { + return selectedProfile; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index 1d987604b..ca227baa0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -52,14 +52,7 @@ public class Settings { private Settings() { ProxyManager.init(); Accounts.init(); - - checkProfileMap(); - - for (Map.Entry profileEntry : getProfileMap().entrySet()) { - profileEntry.getValue().setName(profileEntry.getKey()); - profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged); - profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.markConfigDirty()); - } + Profiles.init(); } public Font getFont() { @@ -114,95 +107,4 @@ public class Settings { throw new IllegalArgumentException("Unknown download provider: " + downloadProvider); config().setDownloadType(index); } - - /**************************************** - * PROFILES * - ****************************************/ - - public Profile getSelectedProfile() { - checkProfileMap(); - - if (!hasProfile(config().getSelectedProfile())) { - getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> { - config().setSelectedProfile(selectedProfile); - }); - Schedulers.computation().schedule(this::onProfileChanged); - } - return getProfile(config().getSelectedProfile()); - } - - public void setSelectedProfile(Profile selectedProfile) { - if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), config().getSelectedProfile())) { - config().setSelectedProfile(selectedProfile.getName()); - Schedulers.computation().schedule(this::onProfileChanged); - } - } - - public Profile getProfile(String name) { - checkProfileMap(); - - Optional p = name == null ? getProfileMap().values().stream().findFirst() : Optional.ofNullable(getProfileMap().get(name)); - return p.orElse(null); - } - - public boolean hasProfile(String name) { - return getProfileMap().containsKey(name); - } - - public Map getProfileMap() { - return config().getConfigurations(); - } - - public Collection getProfiles() { - return getProfileMap().values().stream().filter(t -> StringUtils.isNotBlank(t.getName())).collect(Collectors.toList()); - } - - public void putProfile(Profile ver) { - if (StringUtils.isBlank(ver.getName())) - throw new IllegalArgumentException("Profile's name is empty"); - - getProfileMap().put(ver.getName(), ver); - Schedulers.computation().schedule(this::onProfileLoading); - - ver.nameProperty().setChangedListener(this::profileNameChanged); - } - - public void deleteProfile(Profile profile) { - deleteProfile(profile.getName()); - } - - public void deleteProfile(String profileName) { - getProfileMap().remove(profileName); - checkProfileMap(); - Schedulers.computation().schedule(this::onProfileLoading); - } - - private void checkProfileMap() { - if (getProfileMap().isEmpty()) { - Profile current = new Profile(Profiles.DEFAULT_PROFILE); - current.setUseRelativePath(true); - getProfileMap().put(Profiles.DEFAULT_PROFILE, current); - - Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY); - getProfileMap().put(Profiles.HOME_PROFILE, home); - } - } - - private void onProfileChanged() { - EventBus.EVENT_BUS.fireEvent(new ProfileChangedEvent(this, getSelectedProfile())); - getSelectedProfile().getRepository().refreshVersionsAsync().start(); - } - - private void profileNameChanged(ObservableValue observableValue, String oldValue, String newValue) { - getProfileMap().put(newValue, getProfileMap().remove(oldValue)); - } - - /** - * Start profiles loading process. - * Invoked by loading GUI phase. - */ - public void onProfileLoading() { - EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles())); - onProfileChanged(); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountPage.java deleted file mode 100644 index 9befda845..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountPage.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2017 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.ui; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXProgressBar; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.image.Image; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.StackPane; - -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; -import org.jackhuang.hmcl.auth.offline.OfflineAccount; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.game.AccountHelper; -import org.jackhuang.hmcl.setting.Accounts; -import org.jackhuang.hmcl.setting.Theme; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.ui.construct.AdvancedListItem; -import org.jackhuang.hmcl.ui.construct.ComponentList; -import org.jackhuang.hmcl.ui.wizard.DecoratorPage; - -import static org.jackhuang.hmcl.ui.FXUtils.installTooltip; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -import java.util.Optional; - -public class AccountPage extends StackPane implements DecoratorPage { - private final StringProperty title; - private final ObjectProperty onDelete = new SimpleObjectProperty<>(this, "onDelete"); - private final AdvancedListItem item; - private final Account account; - - @FXML - private Label lblType; - @FXML - private Label lblServer; - @FXML - private Label lblCharacter; - @FXML - private Label lblEmail; - @FXML - private BorderPane paneServer; - @FXML - private BorderPane paneEmail; - @FXML - private ComponentList componentList; - @FXML - private JFXButton btnDelete; - @FXML - private JFXButton btnRefresh; - @FXML - private JFXProgressBar progressBar; - - public AccountPage(Account account, AdvancedListItem item) { - this.account = account; - this.item = item; - - title = new SimpleStringProperty(this, "title", i18n("account") + " - " + account.getCharacter()); - - FXUtils.loadFXML(this, "/assets/fxml/account.fxml"); - - if (account instanceof AuthlibInjectorAccount) { - AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer(); - lblServer.setText(server.getName()); - installTooltip(lblServer, server.getUrl()); - } else { - componentList.removeChildren(paneServer); - - if (account instanceof OfflineAccount) { - componentList.removeChildren(paneEmail); - } - } - - btnDelete.setGraphic(SVG.delete(Theme.blackFillBinding(), 15, 15)); - btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15)); - - lblCharacter.setText(account.getCharacter()); - lblType.setText(Accounts.getAccountTypeName(account)); - lblEmail.setText(account.getUsername()); - - btnRefresh.setVisible(account instanceof YggdrasilAccount); - } - - @FXML - private void onDelete() { - Accounts.getAccounts().remove(account); - Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run); - } - - @FXML - private void onRefresh() { - if (account instanceof YggdrasilAccount) { - progressBar.setVisible(true); - AccountHelper.refreshSkinAsync((YggdrasilAccount) account) - .finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> { - progressBar.setVisible(false); - - if (isDependentsSucceeded) { - Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); - item.setImage(image, AccountHelper.getViewport(4)); - } - }).start(); - } - } - - public String getTitle() { - return title.get(); - } - - @Override - public StringProperty titleProperty() { - return title; - } - - public void setTitle(String title) { - this.title.set(title); - } - - public Runnable getOnDelete() { - return onDelete.get(); - } - - public ObjectProperty onDeleteProperty() { - return onDelete; - } - - public void setOnDelete(Runnable onDelete) { - this.onDelete.set(onDelete); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 272921f0f..a12b7ce7d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -24,13 +24,18 @@ import javafx.scene.layout.Region; import javafx.stage.Stage; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Metadata; -import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.ui.account.AccountList; +import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage; import org.jackhuang.hmcl.ui.construct.InputDialogPane; import org.jackhuang.hmcl.ui.construct.MessageBox; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; +import org.jackhuang.hmcl.ui.decorator.DecoratorController; +import org.jackhuang.hmcl.ui.profile.ProfileList; +import org.jackhuang.hmcl.ui.versions.GameList; +import org.jackhuang.hmcl.ui.versions.VersionPage; import org.jackhuang.hmcl.util.FutureCallback; import org.jackhuang.hmcl.util.JavaVersion; @@ -45,9 +50,12 @@ public final class Controllers { private static MainPage mainPage = null; private static SettingsPage settingsPage = null; private static VersionPage versionPage = null; + private static GameList gameListPage = null; + private static AccountList accountListPage = null; + private static ProfileList profileListPage = null; private static AuthlibInjectorServersPage serversPage = null; private static LeftPaneController leftPaneController; - private static Decorator decorator; + private static DecoratorController decorator; public static Scene getScene() { return scene; @@ -64,6 +72,27 @@ public final class Controllers { return settingsPage; } + // FXThread + public static GameList getGameListPage() { + if (gameListPage == null) + gameListPage = new GameList(); + return gameListPage; + } + + // FXThread + public static AccountList getAccountListPage() { + if (accountListPage == null) + accountListPage = new AccountList(); + return accountListPage; + } + + // FXThread + public static ProfileList getProfileListPage() { + if (profileListPage == null) + profileListPage = new ProfileList(); + return profileListPage; + } + // FXThread public static VersionPage getVersionPage() { if (versionPage == null) @@ -79,7 +108,7 @@ public final class Controllers { } // FXThread - public static Decorator getDecorator() { + public static DecoratorController getDecorator() { return decorator; } @@ -98,21 +127,14 @@ public final class Controllers { stage.setOnCloseRequest(e -> Launcher.stopApplication()); - decorator = new Decorator(stage, getMainPage(), Metadata.TITLE, false, true); - decorator.showPage(null); - leftPaneController = new LeftPaneController(decorator.getLeftPane()); + decorator = new DecoratorController(stage, getMainPage()); + leftPaneController = new LeftPaneController(); + decorator.getDecorator().drawerProperty().setAll(leftPaneController); - Settings.instance().onProfileLoading(); Task.of(JavaVersion::initialize).start(); - decorator.setCustomMaximize(false); - - scene = new Scene(decorator, 804, 521); + scene = new Scene(decorator.getDecorator(), 800, 519); scene.getStylesheets().setAll(config().getTheme().getStylesheets()); - stage.setMinWidth(804); - stage.setMaxWidth(804); - stage.setMinHeight(521); - stage.setMaxHeight(521); stage.getIcons().add(new Image("/assets/img/icon.png")); stage.setTitle(Metadata.TITLE); @@ -163,10 +185,7 @@ public final class Controllers { } public static void navigate(Node node) { - if (decorator.getNowPage() == node) - decorator.showPage(null); - else - decorator.showPage(node); + decorator.getNavigator().navigate(node); } public static boolean isStopped() { @@ -181,5 +200,8 @@ public final class Controllers { decorator = null; stage = null; scene = null; + gameListPage = null; + accountListPage = null; + profileListPage = null; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java deleted file mode 100644 index ddb959e32..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java +++ /dev/null @@ -1,732 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.ui; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXDialog; -import com.jfoenix.controls.JFXDrawer; -import com.jfoenix.controls.JFXHamburger; -import com.jfoenix.svg.SVGGlyph; -import javafx.animation.Interpolator; -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.animation.Timeline; -import javafx.application.Platform; -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.event.EventHandler; -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.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.DragEvent; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.*; -import javafx.scene.paint.Color; -import javafx.scene.shape.Rectangle; -import javafx.stage.Screen; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.util.Duration; -import org.jackhuang.hmcl.Launcher; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD; -import org.jackhuang.hmcl.setting.ConfigHolder; -import org.jackhuang.hmcl.setting.EnumBackgroundImage; -import org.jackhuang.hmcl.setting.Theme; -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.construct.AdvancedListBox; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.StackContainerPane; -import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer; -import org.jackhuang.hmcl.ui.wizard.*; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.OperatingSystem; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.Queue; -import java.util.Random; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.logging.Level; - -import static java.util.stream.Collectors.toList; -import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.util.Logging.LOG; - -public final class Decorator extends StackPane implements TaskExecutorDialogWizardDisplayer { - 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 static final String PROPERTY_DIALOG_CLOSE_HANDLER = Decorator.class.getName() + ".dialog.closeListener"; - - private final ObjectProperty onCloseButtonAction; - private final BooleanProperty customMaximize = new SimpleBooleanProperty(false); - - private final Stage primaryStage; - private final Node mainPage; - private final boolean max, min; - private final WizardController wizardController = new WizardController(this); - private final Queue cancelQueue = new ConcurrentLinkedQueue<>(); - - private double xOffset, yOffset, newX, newY, initX, initY; - private boolean allowMove, isDragging, maximized; - private BoundingBox originalBox, maximizedBox; - private final TransitionHandler animationHandler; - - private JFXDialog dialog; - private StackContainerPane dialogPane; - - @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 Label titleLabel; - @FXML - private Label lblTitle; - @FXML - private AdvancedListBox leftPane; - @FXML - private JFXDrawer drawer; - @FXML - private StackPane titleBurgerContainer; - @FXML - private JFXHamburger titleBurger; - @FXML - private JFXButton btnMin; - @FXML - private JFXButton btnMax; - @FXML - private JFXButton btnClose; - @FXML - private HBox navLeft; - @FXML - private ImageView welcomeView; - @FXML - private Rectangle separator; - - 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"); - - onCloseButtonAction = new SimpleObjectProperty<>(this, "onCloseButtonAction", Launcher::stopApplication); - - primaryStage.initStyle(StageStyle.UNDECORATED); - btnClose.setGraphic(close); - btnMin.setGraphic(minus); - btnMax.setGraphic(resizeMax); - - close.fillProperty().bind(Theme.foregroundFillBinding()); - minus.fillProperty().bind(Theme.foregroundFillBinding()); - resizeMax.fillProperty().bind(Theme.foregroundFillBinding()); - resizeMin.fillProperty().bind(Theme.foregroundFillBinding()); - - refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), 15, 15)); - closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), 15, 15)); - backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), 15, 15)); - - separator.visibleProperty().bind(refreshNavButton.visibleProperty()); - - 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(); - }); - - welcomeView.setCursor(Cursor.HAND); - welcomeView.setOnMouseClicked(e -> { - Timeline nowAnimation = new Timeline(); - nowAnimation.getKeyFrames().addAll( - new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)), - new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)), - new KeyFrame(new Duration(300), e2 -> drawerWrapper.getChildren().remove(welcomeView)) - ); - nowAnimation.play(); - }); - if (!ConfigHolder.isNewlyCreated() || config().getLocalization().getLocale() != Locale.CHINA) - drawerWrapper.getChildren().remove(welcomeView); - - 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; - }); - Rectangle rectangle = new Rectangle(0, 0, 0, 0); - rectangle.widthProperty().bind(titleContainer.widthProperty()); - rectangle.heightProperty().bind(Bindings.createDoubleBinding(() -> titleContainer.getHeight() + 100, titleContainer.heightProperty())); - titleContainer.setClip(rectangle); - - animationHandler = new TransitionHandler(contentPlaceHolder); - - setupBackground(); - setupAuthlibInjectorDnD(); - } - - // ==== Background ==== - private void setupBackground() { - drawerWrapper.backgroundProperty().bind( - Bindings.createObjectBinding( - () -> { - Image image = null; - if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) { - image = tryLoadImage(Paths.get(config().getBackgroundImage())) - .orElse(null); - } - if (image == null) { - image = loadDefaultBackgroundImage(); - } - return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true))); - }, - config().backgroundImageTypeProperty(), - config().backgroundImageProperty())); - } - - private Image defaultBackground = new Image("/assets/img/background.jpg"); - - /** - * Load background image from bg/, background.png, background.jpg - */ - private Image loadDefaultBackgroundImage() { - Optional image = randomImageIn(Paths.get("bg")); - if (!image.isPresent()) { - image = tryLoadImage(Paths.get("background.png")); - } - if (!image.isPresent()) { - image = tryLoadImage(Paths.get("background.jpg")); - } - return image.orElse(defaultBackground); - } - - private Optional randomImageIn(Path imageDir) { - if (!Files.isDirectory(imageDir)) { - return Optional.empty(); - } - - List candidates; - try { - candidates = Files.list(imageDir) - .filter(Files::isRegularFile) - .filter(it -> { - String filename = it.getFileName().toString(); - return filename.endsWith(".png") || filename.endsWith(".jpg"); - }) - .collect(toList()); - } catch (IOException e) { - LOG.log(Level.WARNING, "Failed to list files in ./bg", e); - return Optional.empty(); - } - - Random rnd = new Random(); - while (candidates.size() > 0) { - int selected = rnd.nextInt(candidates.size()); - Optional loaded = tryLoadImage(candidates.get(selected)); - if (loaded.isPresent()) { - return Optional.of(loaded.get()); - } else { - candidates.remove(selected); - } - } - return Optional.empty(); - } - - private Optional tryLoadImage(Path path) { - if (Files.isRegularFile(path)) { - try { - return Optional.of(new Image(path.toAbsolutePath().toUri().toString())); - } catch (IllegalArgumentException ignored) { - } - } - return Optional.empty(); - } - - private boolean isMaximized() { - switch (OperatingSystem.CURRENT_OS) { - case OSX: - Rectangle2D bounds = Screen.getPrimary().getVisualBounds(); - return primaryStage.getWidth() >= bounds.getWidth() && primaryStage.getHeight() >= bounds.getHeight(); - default: - return primaryStage.isMaximized(); - } - } - - // ==== - - @FXML - private void onMouseMoved(MouseEvent mouseEvent) { - if (!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); - } - } - - @FXML - private void onMouseReleased() { - isDragging = false; - } - - @FXML - private void onMouseDragged(MouseEvent mouseEvent) { - this.isDragging = true; - if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) { - if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !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(); - } - } - } - } - - @FXML - private void onMin() { - primaryStage.setIconified(true); - } - - @FXML - private 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; - } - } - - @FXML - private 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 showCloseNavButton() { - navLeft.getChildren().add(closeNavButton); - } - - private void hideCloseNavButton() { - navLeft.getChildren().remove(closeNavButton); - } - - private void setContent(Node content, AnimationProducer animation) { - isWizardPageNow = false; - animationHandler.setContent(content, animation); - - if (content instanceof Region) { - ((Region) content).setMinSize(0, 0); - FXUtils.setOverflowHidden((Region) content, true); - } - - refreshNavButton.setVisible(content instanceof Refreshable); - backNavButton.setVisible(content != mainPage); - - 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; - private boolean isWizardPageNow; - - public Node getNowPage() { - return nowPage; - } - - public void showPage(Node content) { - FXUtils.checkFxUserThread(); - - contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background"); - if (content != null) - contentPlaceHolder.getStyleClass().add("gray-background"); - - 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 void showDialog(Node node) { - FXUtils.checkFxUserThread(); - - if (dialog == null) { - dialog = new JFXDialog(); - dialogPane = new StackContainerPane(); - - dialog.setContent(dialogPane); - dialog.setDialogContainer(drawerWrapper); - dialog.setOverlayClose(false); - dialog.show(); - } - - dialogPane.push(node); - - EventHandler handler = event -> closeDialog(node); - node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler); - node.addEventHandler(DialogCloseEvent.CLOSE, handler); - } - - @SuppressWarnings("unchecked") - private void closeDialog(Node node) { - FXUtils.checkFxUserThread(); - - Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER)) - .ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler) handler)); - - if (dialog != null) { - dialogPane.pop(node); - - if (dialogPane.getChildren().isEmpty()) { - dialog.close(); - dialog = null; - dialogPane = null; - } - } - } - - public void startWizard(WizardProvider wizardProvider) { - startWizard(wizardProvider, null); - } - - public void startWizard(WizardProvider wizardProvider, String category) { - FXUtils.checkFxUserThread(); - - this.category = category; - wizardController.setProvider(wizardProvider); - wizardController.onStart(); - } - - @Override - public void onStart() { - backNavButton.setVisible(true); - backNavButton.setDisable(false); - showCloseNavButton(); - refreshNavButton.setVisible(false); - } - - @Override - public void onEnd() { - backNavButton.setVisible(false); - hideCloseNavButton(); - refreshNavButton.setVisible(false); - } - - @Override - public void navigateTo(Node page, Navigation.NavigationDirection nav) { - contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background"); - contentPlaceHolder.getStyleClass().add("white-background"); - setContent(page, nav.getAnimation().getAnimationProducer()); - isWizardPageNow = true; - } - - @FXML - private void onRefresh() { - ((Refreshable) contentPlaceHolder.getChildren().get(0)).refresh(); - } - - @FXML - private void onCloseNav() { - wizardController.onCancel(); - showPage(null); - } - - @FXML - private void onBack() { - if (isWizardPageNow && wizardController.canPrev()) - wizardController.onPrev(true); - else - onCloseNav(); - } - - @Override - public Queue getCancelQueue() { - return cancelQueue; - } - - public Runnable getOnCloseButtonAction() { - return onCloseButtonAction.get(); - } - - public ObjectProperty onCloseButtonActionProperty() { - return onCloseButtonAction; - } - - public void setOnCloseButtonAction(Runnable onCloseButtonAction) { - this.onCloseButtonAction.set(onCloseButtonAction); - } - - public boolean isCustomMaximize() { - return customMaximize.get(); - } - - public BooleanProperty customMaximizeProperty() { - return customMaximize; - } - - public void setCustomMaximize(boolean customMaximize) { - this.customMaximize.set(customMaximize); - } - - @Override - public WizardController getWizardController() { - return wizardController; - } - - public AdvancedListBox getLeftPane() { - return leftPane; - } - - private void setupAuthlibInjectorDnD() { - addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler()); - addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler( - url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url)))); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java index b0d4d5862..c47790a1a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.task.SilentException; +import org.jackhuang.hmcl.ui.account.AccountLoginPane; import java.util.Optional; import java.util.concurrent.CountDownLatch; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameVersionListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameVersionListPage.java new file mode 100644 index 000000000..0b2df4cc1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameVersionListPage.java @@ -0,0 +1,241 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import com.jfoenix.concurrency.JFXUtilities; +import com.jfoenix.controls.*; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.image.Image; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; +import org.jackhuang.hmcl.event.RefreshingVersionsEvent; +import org.jackhuang.hmcl.game.GameVersion; +import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.ModpackHelper; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.mod.MismatchedModpackTypeException; +import org.jackhuang.hmcl.mod.UnsupportedModpackException; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.MessageBox; +import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.VersionNumber; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.util.StringUtils.removePrefix; +import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class GameVersionListPage extends StackPane { + private final JFXSpinner spinner; + private final StackPane contentPane; + private final JFXMasonryPane masonryPane; + + private Profile profile; + + public GameVersionListPage() { + spinner = new JFXSpinner(); + spinner.getStyleClass().setAll("first-spinner"); + + contentPane = new StackPane(); + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setFitToWidth(true); + + masonryPane = new JFXMasonryPane(); + masonryPane.setHSpacing(3); + masonryPane.setVSpacing(3); + masonryPane.setCellWidth(182); + masonryPane.setCellHeight(153); + + scrollPane.setContent(masonryPane); + + VBox vBox = new VBox(); + vBox.setPadding(new Insets(15)); + vBox.setPickOnBounds(false); + vBox.setAlignment(Pos.BOTTOM_RIGHT); + vBox.setSpacing(15); + + JFXButton btnRefresh = new JFXButton(); + btnRefresh.setPrefWidth(40); + btnRefresh.setPrefHeight(40); + btnRefresh.setButtonType(JFXButton.ButtonType.RAISED); + btnRefresh.getStyleClass().setAll("jfx-button-raised-round"); + btnRefresh.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)); + btnRefresh.setOnMouseClicked(e -> profile.getRepository().refreshVersionsAsync().start()); + FXUtils.installTooltip(btnRefresh, i18n("button.refresh")); + + vBox.getChildren().setAll(btnRefresh); + + contentPane.getChildren().setAll(scrollPane, vBox); + + getChildren().setAll(spinner); + + Profiles.selectedProfileProperty().addListener((o, a, b) -> this.profile = b); + profile = Profiles.getSelectedProfile(); + + EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> { + if (event.getSource() == profile.getRepository()) + loadVersions((HMCLGameRepository) event.getSource()); + }); + EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> { + if (event.getSource() == profile.getRepository()) + // This will occupy 0.5s. Too slow! + JFXUtilities.runInFX(this::loadingVersions); + }); + if (profile.getRepository().isLoaded()) + loadVersions(profile.getRepository()); + else + profile.getRepository().refreshVersionsAsync().start(); + } + + private String modifyVersion(String gameVersion, String version) { + return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_"); + } + + private Node buildNode(HMCLGameRepository repository, Version version, Callable gameCallable) { + Profile profile = repository.getProfile(); + String id = version.getId(); + VersionItem item = new VersionItem(); + item.setUpdate(repository.isModpack(id)); + Task.ofResult("game", gameCallable).subscribe(Schedulers.javafx(), vars -> { + String game = vars.get("game"); + item.setGameVersion(game); + + StringBuilder libraries = new StringBuilder(); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version); + analyzer.getForge().ifPresent(library -> libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n")); + analyzer.getLiteLoader().ifPresent(library -> libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n")); + analyzer.getOptiFine().ifPresent(library -> libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n")); + + item.setLibraries(libraries.toString()); + }); + item.setVersionName(id); + item.setOnLaunchButtonClicked(e -> Versions.launch(profile, id)); + item.setOnScriptButtonClicked(e -> Versions.generateLaunchScript(profile, id)); + item.setOnSettingsButtonClicked(e -> { + Controllers.getVersionPage().load(id, profile); + Controllers.navigate(Controllers.getVersionPage()); + }); + item.setOnUpdateButtonClicked(event -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle(i18n("modpack.choose")); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip")); + File selectedFile = chooser.showOpenDialog(Controllers.getStage()); + if (selectedFile != null) { + AtomicReference region = new AtomicReference<>(); + try { + TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, id, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(id))) + .then(Task.of(Schedulers.javafx(), () -> region.get().fireEvent(new DialogCloseEvent()))).executor(); + region.set(Controllers.taskDialog(executor, i18n("modpack.update"), "")); + executor.start(); + } catch (UnsupportedModpackException e) { + region.get().fireEvent(new DialogCloseEvent()); + Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageBox.ERROR_MESSAGE); + } catch (MismatchedModpackTypeException e) { + region.get().fireEvent(new DialogCloseEvent()); + Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageBox.ERROR_MESSAGE); + } catch (IOException e) { + region.get().fireEvent(new DialogCloseEvent()); + Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE); + } + } + }); + item.setOnMouseClicked(event -> { + if (event.getButton() == MouseButton.SECONDARY) { + JFXListView versionList = new JFXListView<>(); + JFXPopup versionPopup = new JFXPopup(versionList); + versionList.getStyleClass().add("option-list-view"); + FXUtils.setLimitWidth(versionList, 150); + versionList.getItems().setAll(Lang.immutableListOf( + i18n("version.manage.rename"), + i18n("version.manage.remove"), + i18n("modpack.export"), + i18n("folder.game") + )); + versionList.setOnMouseClicked(e -> { + versionPopup.hide(); + switch (versionList.getSelectionModel().getSelectedIndex()) { + case 0: + Versions.renameVersion(profile, id); + break; + case 1: + Versions.deleteVersion(profile, id); + break; + case 2: + Versions.exportVersion(profile, id); + break; + case 3: + FXUtils.openFolder(repository.getRunDirectory(id)); + break; + default: + break; + } + }); + versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); + } else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { + Versions.launch(profile, id); + } + }); + File iconFile = repository.getVersionIcon(id); + if (iconFile.exists()) + item.setImage(new Image("file:" + iconFile.getAbsolutePath())); + return item; + } + + private void loadingVersions() { + getChildren().setAll(spinner); + masonryPane.getChildren().clear(); + } + + private void loadVersions(HMCLGameRepository repository) { + List children = repository.getVersions().parallelStream() + .filter(version -> !version.isHidden()) + .sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId()))) + .map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown"))) + .collect(Collectors.toList()); + JFXUtilities.runInFX(() -> { + if (profile == repository.getProfile()) { + masonryPane.getChildren().setAll(children); + getChildren().setAll(contentPane); + } + }); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java index cf7cbbadd..07c3acfae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java @@ -18,29 +18,11 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.concurrency.JFXUtilities; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXPopup; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.beans.binding.When; -import javafx.beans.property.ListProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleListProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.scene.Node; -import javafx.scene.control.Tooltip; -import javafx.scene.image.Image; -import javafx.scene.input.MouseButton; import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; import javafx.scene.paint.Color; - -import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; -import org.jackhuang.hmcl.auth.offline.OfflineAccount; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.event.*; -import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.mod.Modpack; @@ -49,47 +31,32 @@ import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem; +import org.jackhuang.hmcl.ui.account.AddAccountPane; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem; +import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; import org.jackhuang.hmcl.upgrade.UpdateChecker; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.MappedObservableList; import java.io.File; -import java.util.LinkedList; -import java.util.Objects; -import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; -import static javafx.collections.FXCollections.singletonObservableList; -import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public final class LeftPaneController { - private final AdvancedListBox leftPane; - private final VBox profilePane = new VBox(); - private final VBox accountPane = new VBox(); - private final IconedItem launcherSettingsItem; +public final class LeftPaneController extends AdvancedListBox { - private ListProperty accountItems = new SimpleListProperty<>(); - private ObjectProperty selectedAccount = new SimpleObjectProperty() { - { - accountItems.addListener(onInvalidating(this::invalidated)); - } + public LeftPaneController() { - @Override - protected void invalidated() { - Account selected = get(); - accountItems.forEach(item -> item.setSelected( - getAccountFromItem(item) - .map(it -> it == selected) - .orElse(false))); - } - }; + AccountAdvancedListItem accountListItem = new AccountAdvancedListItem(); + accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage())); + accountListItem.accountProperty().bind(Accounts.selectedAccountProperty()); + GameAdvancedListItem gameListItem = new GameAdvancedListItem(); + gameListItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage())); + ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem(); + profileListItem.setOnAction(e -> Controllers.navigate(Controllers.getProfileListPage())); + profileListItem.profileProperty().bind(Profiles.selectedProfileProperty()); - public LeftPaneController(AdvancedListBox leftPane) { - this.leftPane = leftPane; - - launcherSettingsItem = new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20)); + IconedItem launcherSettingsItem = new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20)); launcherSettingsItem.getLabel().textProperty().bind( new When(UpdateChecker.outdatedProperty()) @@ -101,102 +68,25 @@ public final class LeftPaneController { .then(Color.RED) .otherwise(Color.BLACK)); - launcherSettingsItem.prefWidthProperty().bind(leftPane.widthProperty()); + launcherSettingsItem.maxWidthProperty().bind(widthProperty()); launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage())); - leftPane - .add(new ClassTitle(i18n("account").toUpperCase(), Lang.apply(new JFXButton(), button -> { - button.setGraphic(SVG.plus(Theme.blackFillBinding(), 10, 10)); - button.getStyleClass().add("toggle-icon-tiny"); - button.setOnMouseClicked(e -> addNewAccount()); - }))) - .add(accountPane) + this + .startCategory(i18n("account").toUpperCase()) + .add(accountListItem) + .startCategory(i18n("version").toUpperCase()) + .add(gameListItem) + .startCategory(i18n("profile.title").toUpperCase()) + .add(profileListItem) .startCategory(i18n("launcher").toUpperCase()) - .add(launcherSettingsItem) - .add(new ClassTitle(i18n("profile.title").toUpperCase(), Lang.apply(new JFXButton(), button -> { - button.setGraphic(SVG.plus(Theme.blackFillBinding(), 10, 10)); - button.getStyleClass().add("toggle-icon-tiny"); - button.setOnMouseClicked(e -> - Controllers.getDecorator().showPage(new ProfilePage(null))); - }))) - .add(profilePane); + .add(launcherSettingsItem); - // ==== Accounts ==== - // Missing account item - AdvancedListItem missingAccountItem = new AdvancedListItem(i18n("account.missing"), i18n("message.unknown")); - RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem); - missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount()); - missingAccountRippler.setOnMouseClicked(e -> addNewAccount()); - - accountItems.bind( - new When(Accounts.accountsProperty().emptyProperty()) - .then(singletonObservableList(missingAccountRippler)) - .otherwise(MappedObservableList.create(Accounts.getAccounts(), this::createAccountItem))); - Bindings.bindContent(accountPane.getChildren(), accountItems); - - selectedAccount.bindBidirectional(Accounts.selectedAccountProperty()); - // ==== - - EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading); - EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged); - EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions); + EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource())); + if (Profiles.selectedProfileProperty().get().getRepository().isLoaded()) + onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository()); } // ==== Accounts ==== - private Optional getAccountFromItem(RipplerContainer accountItem) { - return Optional.ofNullable(accountItem.getProperties().get("account")) - .map(Account.class::cast); - } - - private static String accountSubtitle(Account account) { - if (account instanceof OfflineAccount) - return i18n("account.methods.offline"); - else if (account instanceof YggdrasilAccount) - return account.getUsername(); - else - return ""; - } - - private RipplerContainer createAccountItem(Account account) { - AdvancedListItem item = new AdvancedListItem(account.getCharacter(), accountSubtitle(account)); - RipplerContainer rippler = new RipplerContainer(item); - item.setOnSettingsButtonClicked(e -> { - AccountPage accountPage = new AccountPage(account, item); - JFXPopup popup = new JFXPopup(accountPage); - accountPage.setOnDelete(popup::hide); - popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY()); - }); - rippler.setOnMouseClicked(e -> { - if (e.getButton() == MouseButton.PRIMARY) { - selectedAccount.set(account); - } - }); - rippler.getProperties().put("account", account); - rippler.maxWidthProperty().bind(leftPane.widthProperty()); - - if (account instanceof YggdrasilAccount) { - Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); - item.setImage(image, AccountHelper.getViewport(4)); - } else { - item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4)); - } - - if (account instanceof AuthlibInjectorAccount) { - FXUtils.installTooltip(rippler, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName())); - } - - // update skin - if (account instanceof YggdrasilAccount) { - AccountHelper.refreshSkinAsync((YggdrasilAccount) account) - .subscribe(Schedulers.javafx(), () -> { - Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); - item.setImage(image, AccountHelper.getViewport(4)); - }); - } - - return rippler; - } - public void checkAccount() { if (Accounts.getAccounts().isEmpty()) addNewAccount(); @@ -207,40 +97,11 @@ public final class LeftPaneController { } // ==== - private void onProfileChanged(ProfileChangedEvent event) { - Profile profile = event.getProfile(); - - Platform.runLater(() -> { - for (Node node : profilePane.getChildren()) { - if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof String) { - boolean current = Objects.equals(node.getProperties().get("profile"), profile.getName()); - ((RipplerContainer) node).setSelected(current); - ((AdvancedListItem) ((RipplerContainer) node).getContainer()).setSubtitle(current ? i18n("profile.selected") : ""); - } - } - }); - } - - private void onProfilesLoading() { - LinkedList list = new LinkedList<>(); - for (Profile profile : Settings.instance().getProfiles()) { - AdvancedListItem item = new AdvancedListItem(Profiles.getProfileDisplayName(profile)); - RipplerContainer ripplerContainer = new RipplerContainer(item); - item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile))); - ripplerContainer.setOnMouseClicked(e -> Settings.instance().setSelectedProfile(profile)); - ripplerContainer.getProperties().put("profile", profile.getName()); - ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty()); - list.add(ripplerContainer); - } - Platform.runLater(() -> profilePane.getChildren().setAll(list)); - } - private boolean checkedModpack = false; private static boolean showNewAccount = true; - private void onRefreshedVersions(RefreshedVersionsEvent event) { + private void onRefreshedVersions(HMCLGameRepository repository) { JFXUtilities.runInFX(() -> { - HMCLGameRepository repository = (HMCLGameRepository) event.getSource(); if (!checkedModpack) { checkedModpack = true; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java index dea720a8f..263ca1af5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java @@ -17,229 +17,41 @@ */ package org.jackhuang.hmcl.ui; -import com.jfoenix.concurrency.JFXUtilities; -import com.jfoenix.controls.*; +import com.jfoenix.controls.JFXButton; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.fxml.FXML; -import javafx.scene.Node; -import javafx.scene.control.ScrollPane; -import javafx.scene.image.Image; -import javafx.scene.input.MouseButton; -import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; -import javafx.stage.FileChooser; - -import org.jackhuang.hmcl.download.LibraryAnalyzer; -import org.jackhuang.hmcl.event.EventBus; -import org.jackhuang.hmcl.event.ProfileChangedEvent; -import org.jackhuang.hmcl.event.RefreshedVersionsEvent; -import org.jackhuang.hmcl.event.RefreshingVersionsEvent; -import org.jackhuang.hmcl.game.*; -import org.jackhuang.hmcl.mod.MismatchedModpackTypeException; -import org.jackhuang.hmcl.mod.UnsupportedModpackException; -import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.setting.ConfigHolder; import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.setting.Settings; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.task.TaskExecutor; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.MessageBox; -import org.jackhuang.hmcl.ui.download.DownloadWizardProvider; -import org.jackhuang.hmcl.ui.wizard.DecoratorPage; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.OperatingSystem; -import org.jackhuang.hmcl.util.VersionNumber; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.ui.versions.Versions; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import static org.jackhuang.hmcl.util.StringUtils.removePrefix; -import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class MainPage extends StackPane implements DecoratorPage { private final StringProperty title = new SimpleStringProperty(this, "title", i18n("main_page")); - private Profile profile; - @FXML - private JFXButton btnRefresh; - @FXML - private StackPane contentPane; - @FXML - private JFXButton btnAdd; - @FXML - private JFXSpinner spinner; - @FXML - private JFXMasonryPane masonryPane; - @FXML - private ScrollPane scrollPane; + private StackPane main; { FXUtils.loadFXML(this, "/assets/fxml/main.fxml"); - loadingVersions(); - - EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> { - if (event.getSource() == profile.getRepository()) - loadVersions((HMCLGameRepository) event.getSource()); - }); - EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> { - if (event.getSource() == profile.getRepository()) - // This will occupy 0.5s. Too slow! - JFXUtilities.runInFX(this::loadingVersions); - }); - EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(event -> { - this.profile = event.getProfile(); - }); - - btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), i18n("install"))); - FXUtils.installTooltip(btnAdd, i18n("install")); - btnRefresh.setOnMouseClicked(e -> Settings.instance().getSelectedProfile().getRepository().refreshVersionsAsync().start()); - FXUtils.installTooltip(btnRefresh, i18n("button.refresh")); - } - - private String modifyVersion(String gameVersion, String version) { - return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_"); - } - - private Node buildNode(HMCLGameRepository repository, Version version, Callable gameCallable) { - Profile profile = repository.getProfile(); - String id = version.getId(); - VersionItem item = new VersionItem(); - item.setUpdate(repository.isModpack(id)); - Task.ofResult("game", gameCallable).subscribe(Schedulers.javafx(), vars -> { - String game = vars.get("game"); - item.setGameVersion(game); - - StringBuilder libraries = new StringBuilder(); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version); - analyzer.getForge().ifPresent(library -> libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n")); - analyzer.getLiteLoader().ifPresent(library -> libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n")); - analyzer.getOptiFine().ifPresent(library -> libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n")); - - item.setLibraries(libraries.toString()); - }); - item.setVersionName(id); - item.setOnLaunchButtonClicked(e -> { - if (Accounts.getSelectedAccount() == null) - Controllers.getLeftPaneController().checkAccount(); + FXUtils.onChangeAndOperate(ConfigHolder.config().enableMainPageGameListProperty(), newValue -> { + if (newValue) + getChildren().setAll(new GameVersionListPage()); else - LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null); + getChildren().setAll(main); }); - item.setOnScriptButtonClicked(e -> { - if (Accounts.getSelectedAccount() == null) - Controllers.dialog(i18n("login.empty_username")); - else { - FileChooser chooser = new FileChooser(); - if (repository.getRunDirectory(id).isDirectory()) - chooser.setInitialDirectory(repository.getRunDirectory(id)); - chooser.setTitle(i18n("version.launch_script.save")); - chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS - ? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat") - : new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh")); - File file = chooser.showSaveDialog(Controllers.getStage()); - if (file != null) - LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file); - } - }); - item.setOnSettingsButtonClicked(e -> { - Controllers.getDecorator().showPage(Controllers.getVersionPage()); - Controllers.getVersionPage().load(id, profile); - }); - item.setOnUpdateButtonClicked(event -> { - FileChooser chooser = new FileChooser(); - chooser.setTitle(i18n("modpack.choose")); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip")); - File selectedFile = chooser.showOpenDialog(Controllers.getStage()); - if (selectedFile != null) { - AtomicReference region = new AtomicReference<>(); - try { - TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, id, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(id))) - .then(Task.of(Schedulers.javafx(), () -> region.get().fireEvent(new DialogCloseEvent()))).executor(); - region.set(Controllers.taskDialog(executor, i18n("modpack.update"), "")); - executor.start(); - } catch (UnsupportedModpackException e) { - region.get().fireEvent(new DialogCloseEvent()); - Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageBox.ERROR_MESSAGE); - } catch (MismatchedModpackTypeException e) { - region.get().fireEvent(new DialogCloseEvent()); - Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageBox.ERROR_MESSAGE); - } catch (IOException e) { - region.get().fireEvent(new DialogCloseEvent()); - Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE); - } - } - }); - item.setOnMouseClicked(event -> { - if (event.getButton() == MouseButton.SECONDARY) { - JFXListView versionList = new JFXListView<>(); - JFXPopup versionPopup = new JFXPopup(versionList); - versionList.getStyleClass().add("option-list-view"); - FXUtils.setLimitWidth(versionList, 150); - versionList.getItems().setAll(Lang.immutableListOf( - i18n("version.manage.rename"), - i18n("version.manage.remove"), - i18n("modpack.export"), - i18n("folder.game") - )); - versionList.setOnMouseClicked(e -> { - versionPopup.hide(); - switch (versionList.getSelectionModel().getSelectedIndex()) { - case 0: - VersionPage.renameVersion(profile, id); - break; - case 1: - VersionPage.deleteVersion(profile, id); - break; - case 2: - VersionPage.exportVersion(profile, id); - break; - case 3: - FXUtils.openFolder(repository.getRunDirectory(id)); - break; - default: - break; - } - }); - versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); - } else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { - if (Accounts.getSelectedAccount() == null) - Controllers.dialog(i18n("login.empty_username")); - else - LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null); - } - }); - File iconFile = repository.getVersionIcon(id); - if (iconFile.exists()) - item.setImage(new Image("file:" + iconFile.getAbsolutePath())); - return item; } - private void loadingVersions() { - getChildren().setAll(spinner); - masonryPane.getChildren().clear(); - } - - private void loadVersions(HMCLGameRepository repository) { - List children = repository.getVersions().parallelStream() - .filter(version -> !version.isHidden()) - .sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId()))) - .map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown"))) - .collect(Collectors.toList()); - JFXUtilities.runInFX(() -> { - if (profile == repository.getProfile()) { - masonryPane.getChildren().setAll(children); - getChildren().setAll(contentPane); - } - }); + @FXML + private void launch() { + Profile profile = Profiles.getSelectedProfile(); + Versions.launch(profile, profile.getSelectedVersion()); } public String getTitle() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.java index ec7362e44..27f131b00 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModItem.java @@ -26,6 +26,7 @@ import javafx.scene.layout.BorderPane; import org.jackhuang.hmcl.mod.ModInfo; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.StringUtils; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 65f15b6f3..8925dd194 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -18,8 +18,10 @@ package org.jackhuang.hmcl.ui; import javafx.beans.binding.ObjectBinding; +import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; +import javafx.scene.layout.StackPane; import javafx.scene.paint.Paint; import javafx.scene.shape.SVGPath; @@ -33,6 +35,12 @@ public final class SVG { path.setContent(d); path.fillProperty().bind(fill); + if (width < 0 || height < 0) { + StackPane pane = new StackPane(path); + pane.setAlignment(Pos.CENTER); + return pane; + } + Group svg = new Group(path); double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight()); svg.setScaleX(scale); @@ -127,4 +135,7 @@ public final class SVG { return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height); } + public static Node importIcon(ObjectBinding fill, double width, double height) { + return createSVGPath("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z", fill, width, height); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java index 8b35d9eb7..803984150 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsPage.java @@ -36,7 +36,7 @@ import javafx.scene.paint.Color; import javafx.scene.text.Font; import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.ui.construct.Validator; -import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.upgrade.UpdateChannel; import org.jackhuang.hmcl.upgrade.RemoteVersion; import org.jackhuang.hmcl.upgrade.UpdateChecker; @@ -63,6 +63,8 @@ public final class SettingsPage extends SettingsView implements DecoratorPage { public SettingsPage() { FXUtils.smoothScrolling(scroll); + chkEnableGameList.selectedProperty().bindBidirectional(config().enableMainPageGameListProperty()); + cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.instance().getDownloadProvider())); cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.instance().setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue()))); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsView.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsView.java index 9149bc294..aa4d913ca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsView.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SettingsView.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.*; -import javafx.fxml.FXML; import javafx.geometry.HPos; import javafx.geometry.Pos; import javafx.geometry.VPos; @@ -31,7 +30,6 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage; import org.jackhuang.hmcl.setting.EnumCommonDirectory; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.i18n.I18n; public abstract class SettingsView extends StackPane { @@ -60,17 +58,17 @@ public abstract class SettingsView extends StackPane { protected final JFXCheckBox chkProxyAuthentication; protected final GridPane authPane; protected final Pane proxyPane; + protected final JFXToggleButton chkEnableGameList; public SettingsView() { scroll = new ScrollPane(); getChildren().setAll(scroll); - scroll.setStyle("-fx-font-size: 14; -fx-pref-width: 100%;"); - scroll.setFitToHeight(true); + scroll.setStyle("-fx-font-size: 14;"); scroll.setFitToWidth(true); { VBox rootPane = new VBox(); - rootPane.setStyle("-fx-padding: 20;"); + rootPane.setStyle("-fx-padding: 18;"); { ComponentList settingsPane = new ComponentList(); { @@ -137,6 +135,21 @@ public abstract class SettingsView extends StackPane { settingsPane.addChildren(backgroundItem); } + { + BorderPane borderPane = new BorderPane(); + + Label left = new Label(I18n.i18n("settings.launcher.enable_game_list")); + BorderPane.setAlignment(left, Pos.CENTER_LEFT); + borderPane.setLeft(left); + + chkEnableGameList = new JFXToggleButton(); + chkEnableGameList.setSize(8); + FXUtils.setLimitHeight(chkEnableGameList, 20); + borderPane.setRight(chkEnableGameList); + + settingsPane.addChildren(borderPane); + } + { BorderPane downloadSourcePane = new BorderPane(); { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WeakListenerHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WeakListenerHelper.java new file mode 100644 index 000000000..3d8f0dde8 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WeakListenerHelper.java @@ -0,0 +1,58 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui; + +import javafx.beans.InvalidationListener; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.WeakChangeListener; +import javafx.collections.ListChangeListener; +import javafx.collections.WeakListChangeListener; + +import java.util.LinkedList; +import java.util.List; + +public class WeakListenerHelper { + List refs = new LinkedList<>(); + + public WeakListenerHelper() { + } + + public WeakInvalidationListener weak(InvalidationListener listener) { + refs.add(listener); + return new WeakInvalidationListener(listener); + } + + public WeakChangeListener weak(ChangeListener listener) { + refs.add(listener); + return new WeakChangeListener<>(listener); + } + + public WeakListChangeListener weak(ListChangeListener listener) { + refs.add(listener); + return new WeakListChangeListener<>(listener); + } + + public void add(Object obj) { + refs.add(obj); + } + + public boolean remove(Object obj) { + return refs.remove(obj); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java new file mode 100644 index 000000000..ce968e09b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java @@ -0,0 +1,74 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.account; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.offline.OfflineAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.ui.construct.AdvancedListItem; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class AccountAdvancedListItem extends AdvancedListItem { + private ObjectProperty account = new SimpleObjectProperty() { + + @Override + protected void invalidated() { + Account account = get(); + if (account == null) { + titleProperty().set(i18n("account.missing")); + subtitleProperty().set(i18n("account.missing.add")); + imageProperty().set(new Image("/assets/img/craft_table.png")); + } else { + titleProperty().set(account.getCharacter()); + subtitleProperty().set(accountSubtitle(account)); + + imageProperty().set(AccountHelper.getDefaultSkin(account.getUUID(), 4)); + + if (account instanceof YggdrasilAccount) { + AccountHelper.loadSkinAsync((YggdrasilAccount) account).subscribe(Schedulers.javafx(), () -> { + Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); + imageProperty().set(image); + }); + } + } + } + }; + + public AccountAdvancedListItem() { + viewportProperty().set(AccountHelper.getViewport(4)); + } + + public ObjectProperty accountProperty() { + return account; + } + + private static String accountSubtitle(Account account) { + if (account instanceof OfflineAccount) + return i18n("account.methods.offline"); + else if (account instanceof YggdrasilAccount) + return account.getUsername(); + else + return ""; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountList.java new file mode 100644 index 000000000..9164e908c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountList.java @@ -0,0 +1,82 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.account; + +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.ToggleGroup; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.util.MappedObservableList; + +import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class AccountList extends Control implements DecoratorPage { + private final StringProperty title = new SimpleStringProperty(i18n("account.manage")); + private final ListProperty items = new SimpleListProperty<>(FXCollections.observableArrayList()); + private ObjectProperty selectedAccount = new SimpleObjectProperty() { + { + items.addListener(onInvalidating(this::invalidated)); + } + + @Override + protected void invalidated() { + Account selected = get(); + items.forEach(item -> item.selectedProperty().set(item.getAccount() == selected)); + } + }; + + private ToggleGroup toggleGroup; + + public AccountList() { + toggleGroup = new ToggleGroup(); + + items.bindContent(MappedObservableList.create( + Accounts.accountsProperty(), + account -> new AccountListItem(toggleGroup, account))); + + selectedAccount.bindBidirectional(Accounts.selectedAccountProperty()); + toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { + if (toggle == null || toggle.getUserData() == null) return; + selectedAccount.set(((AccountListItem) toggle.getUserData()).getAccount()); + }); + } + + @Override + protected Skin createDefaultSkin() { + return new AccountListSkin(this); + } + + public void addNewAccount() { + Controllers.dialog(new AddAccountPane()); + } + + public ListProperty itemsProperty() { + return items; + } + + @Override + public StringProperty titleProperty() { + return title; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java new file mode 100644 index 000000000..fe46ed324 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -0,0 +1,123 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.account; + +import javafx.beans.property.*; +import javafx.geometry.Rectangle2D; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; +import org.jackhuang.hmcl.auth.offline.OfflineAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.task.Schedulers; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class AccountListItem extends Control { + private final Account account; + private final ToggleGroup toggleGroup; + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty subtitle = new SimpleStringProperty(); + private final BooleanProperty selected = new SimpleBooleanProperty(); + private final ObjectProperty image = new SimpleObjectProperty<>(); + private final ObjectProperty viewport = new SimpleObjectProperty<>(); + + public AccountListItem(ToggleGroup toggleGroup, Account account) { + this.account = account; + this.toggleGroup = toggleGroup; + + StringBuilder subtitleString = new StringBuilder(Accounts.getAccountTypeName(account)); + if (account instanceof AuthlibInjectorAccount) { + AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer(); + subtitleString.append(", ").append(i18n("account.injector.server")).append(": ").append(server.getName()); + } + + if (account instanceof OfflineAccount) + title.set(account.getCharacter()); + else + title.set(account.getUsername() + " - " + account.getCharacter()); + subtitle.set(subtitleString.toString()); + selected.set(Accounts.selectedAccountProperty().get() == account); + + viewport.set(AccountHelper.getViewport(4)); + if (account instanceof YggdrasilAccount) { + Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); + this.image.set(image); + } else { + this.image.set(AccountHelper.getDefaultSkin(account.getUUID(), 4)); + } + } + + @Override + protected Skin createDefaultSkin() { + return new AccountListItemSkin(this); + } + + public ToggleGroup getToggleGroup() { + return toggleGroup; + } + + public Account getAccount() { + return account; + } + + public StringProperty titleProperty() { + return title; + } + + public StringProperty subtitleProperty() { + return subtitle; + } + + public BooleanProperty selectedProperty() { + return selected; + } + + public ObjectProperty imageProperty() { + return image; + } + + public ObjectProperty viewportProperty() { + return viewport; + } + + public void refresh() { + if (account instanceof YggdrasilAccount) { + // progressBar.setVisible(true); + AccountHelper.refreshSkinAsync((YggdrasilAccount) account) + .finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> { + // progressBar.setVisible(false); + + if (isDependentsSucceeded) { + Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4); + this.image.set(image); + } + }).start(); + } + } + + public void remove() { + Accounts.getAccounts().remove(account); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java new file mode 100644 index 000000000..26911fce3 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -0,0 +1,95 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.account; + +import com.jfoenix.concurrency.JFXUtilities; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXRadioButton; +import com.jfoenix.effects.JFXDepthManager; +import javafx.geometry.Pos; +import javafx.scene.control.SkinBase; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class AccountListItemSkin extends SkinBase { + + public AccountListItemSkin(AccountListItem skinnable) { + super(skinnable); + + BorderPane root = new BorderPane(); + + JFXRadioButton chkSelected = new JFXRadioButton(); + BorderPane.setAlignment(chkSelected, Pos.CENTER); + chkSelected.setUserData(skinnable); + chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty()); + chkSelected.setToggleGroup(skinnable.getToggleGroup()); + root.setLeft(chkSelected); + + HBox center = new HBox(); + center.setSpacing(8); + center.setAlignment(Pos.CENTER_LEFT); + + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + ImageView imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + imageView.imageProperty().bind(skinnable.imageProperty()); + imageView.viewportProperty().bind(skinnable.viewportProperty()); + imageViewContainer.getChildren().setAll(imageView); + + TwoLineListItem item = new TwoLineListItem(); + BorderPane.setAlignment(item, Pos.CENTER); + center.getChildren().setAll(imageView, item); + root.setCenter(center); + + HBox right = new HBox(); + right.setAlignment(Pos.CENTER_RIGHT); + JFXButton btnRefresh = new JFXButton(); + btnRefresh.setOnMouseClicked(e -> skinnable.refresh()); + btnRefresh.getStyleClass().add("toggle-icon4"); + btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1)); + JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRefresh, i18n("button.refresh"))); + right.getChildren().add(btnRefresh); + + JFXButton btnRemove = new JFXButton(); + btnRemove.setOnMouseClicked(e -> skinnable.remove()); + btnRemove.getStyleClass().add("toggle-icon4"); + BorderPane.setAlignment(btnRemove, Pos.CENTER); + btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1)); + JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRemove, i18n("button.delete"))); + right.getChildren().add(btnRemove); + root.setRight(right); + + root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;"); + JFXDepthManager.setDepth(root, 1); + item.titleProperty().bind(skinnable.titleProperty()); + item.subtitleProperty().bind(skinnable.subtitleProperty()); + + getChildren().setAll(root); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListSkin.java new file mode 100644 index 000000000..d4a2fd206 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListSkin.java @@ -0,0 +1,77 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.account; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXScrollPane; +import javafx.beans.binding.Bindings; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; + +public class AccountListSkin extends SkinBase { + + public AccountListSkin(AccountList skinnable) { + super(skinnable); + + StackPane root = new StackPane(); + + ScrollPane scrollPane = new ScrollPane(); + { + scrollPane.setFitToWidth(true); + + VBox accountList = new VBox(); + accountList.maxWidthProperty().bind(scrollPane.widthProperty()); + accountList.setSpacing(10); + accountList.setStyle("-fx-padding: 10 10 10 10;"); + + Bindings.bindContent(accountList.getChildren(), skinnable.itemsProperty()); + + scrollPane.setContent(accountList); + JFXScrollPane.smoothScrolling(scrollPane); + } + + VBox vBox = new VBox(); + { + vBox.setAlignment(Pos.BOTTOM_RIGHT); + vBox.setPickOnBounds(false); + vBox.setPadding(new Insets(15)); + vBox.setSpacing(15); + + JFXButton btnAdd = new JFXButton(); + FXUtils.setLimitWidth(btnAdd, 40); + FXUtils.setLimitHeight(btnAdd, 40); + btnAdd.getStyleClass().setAll("jfx-button-raised-round"); + btnAdd.setButtonType(JFXButton.ButtonType.RAISED); + btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1)); + btnAdd.setOnMouseClicked(e -> skinnable.addNewAccount()); + + vBox.getChildren().setAll(btnAdd); + } + + root.getChildren().setAll(scrollPane, vBox); + + getChildren().setAll(root); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountLoginPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginPane.java similarity index 97% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountLoginPane.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginPane.java index 89bd4c6bd..340cb8489 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountLoginPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginPane.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.account; import com.jfoenix.controls.JFXPasswordField; import com.jfoenix.controls.JFXProgressBar; @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.NoSelectedCharacterException; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import java.util.function.Consumer; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java similarity index 95% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java index ab2bbf599..92436bbca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.account; import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.controls.*; @@ -41,6 +41,9 @@ import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.IconedItem; @@ -87,6 +90,9 @@ public class AddAccountPane extends StackPane { cboType.setConverter(stringConverter(Accounts::getAccountTypeName)); cboType.getSelectionModel().select(0); + cboServers.getItems().addListener(onInvalidating(this::checkIfNoServer)); + checkIfNoServer(); + ReadOnlyObjectProperty> loginType = cboType.getSelectionModel().selectedItemProperty(); txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE)); @@ -119,6 +125,13 @@ public class AddAccountPane extends StackPane { } } + private void checkIfNoServer() { + if (cboServers.getItems().isEmpty()) + cboServers.getStyleClass().setAll("jfx-combo-box-warning"); + else + cboServers.getStyleClass().setAll("jfx-combo-box"); + } + /** * Gets the additional data that needs to be passed into {@link AccountFactory#create(CharacterSelector, String, String, Object)}. */ diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAuthlibInjectorServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAuthlibInjectorServerPane.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAuthlibInjectorServerPane.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAuthlibInjectorServerPane.java index 61dd9d52d..c875e1c01 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAuthlibInjectorServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAuthlibInjectorServerPane.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.account; import static org.jackhuang.hmcl.ui.FXUtils.loadFXML; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServerItem.java similarity index 97% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServerItem.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServerItem.java index f5c874dfc..570988b42 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServerItem.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.account; import com.jfoenix.controls.JFXButton; import com.jfoenix.effects.JFXDepthManager; @@ -25,6 +25,7 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.SVG; import java.util.function.Consumer; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java similarity index 95% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java index 02e02ad30..70c20d4c4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AuthlibInjectorServersPage.java @@ -15,14 +15,15 @@ * 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; +package org.jackhuang.hmcl.ui.account; import static org.jackhuang.hmcl.ui.FXUtils.loadFXML; import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; -import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.MappedObservableList; import javafx.beans.binding.Bindings; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java index 5611ba16a..770f87810 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.ui.animation; import javafx.animation.KeyFrame; -import javafx.util.Duration; import java.util.List; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java index 7a358f10e..8eaf67baa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java @@ -20,14 +20,11 @@ package org.jackhuang.hmcl.ui.animation; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; -import javafx.scene.Node; import javafx.util.Duration; -import org.jackhuang.hmcl.util.Lang; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java index 2cad4d23c..0695bc013 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java @@ -42,7 +42,7 @@ public class AdvancedListBox extends ScrollPane { } public AdvancedListBox add(Node child) { - if (child instanceof Pane) + if (child instanceof Pane || child instanceof AdvancedListItem) container.getChildren().add(child); else { StackPane pane = new StackPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java index e740fbce9..356d7d7cd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java @@ -1,7 +1,7 @@ /* * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * + * Copyright (C) 2017 huangyuhui + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -17,55 +17,57 @@ */ package org.jackhuang.hmcl.ui.construct; -import com.jfoenix.controls.JFXButton; +import javafx.beans.property.*; +import javafx.event.ActionEvent; import javafx.event.EventHandler; -import javafx.fxml.FXML; import javafx.geometry.Rectangle2D; -import javafx.scene.control.Label; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.StackPane; -import org.jackhuang.hmcl.ui.FXUtils; -public final class AdvancedListItem extends StackPane { - @FXML - private StackPane imageViewContainer; - @FXML - private Label lblTitle; - @FXML - private Label lblSubtitle; - @FXML - private ImageView imageView; - @FXML private JFXButton btnSettings; +public class AdvancedListItem extends Control { + private final ObjectProperty image = new SimpleObjectProperty<>(); + private final ObjectProperty viewport = new SimpleObjectProperty<>(); + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty subtitle = new SimpleStringProperty(); - public AdvancedListItem(String title) { - this(title, ""); + public ObjectProperty imageProperty() { + return image; } - public AdvancedListItem(String title, String subtitle) { - FXUtils.loadFXML(this, "/assets/fxml/advanced-list-item.fxml"); - - lblTitle.setText(title); - lblSubtitle.setText(subtitle); - - FXUtils.limitSize(imageView, 32, 32); + public ObjectProperty viewportProperty() { + return viewport; } - public void setOnSettingsButtonClicked(EventHandler handler) { - btnSettings.setOnMouseClicked(handler); + public StringProperty titleProperty() { + return title; } - public void setTitle(String title) { - lblTitle.setText(title); + public StringProperty subtitleProperty() { + return subtitle; } - public void setSubtitle(String subtitle) { - lblSubtitle.setText(subtitle); + public final ObjectProperty> onActionProperty() { + return onAction; } - public void setImage(Image image, Rectangle2D viewport) { - imageView.setImage(image); - imageView.setViewport(viewport); + public final void setOnAction(EventHandler value) { + onActionProperty().set(value); + } + + public final EventHandler getOnAction() { + return onActionProperty().get(); + } + + private ObjectProperty> onAction = new SimpleObjectProperty>(this, "onAction") { + @Override + protected void invalidated() { + setEventHandler(ActionEvent.ACTION, get()); + } + }; + + @Override + protected Skin createDefaultSkin() { + return new AdvancedListItemSkin(this); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItemSkin.java new file mode 100644 index 000000000..901693879 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItemSkin.java @@ -0,0 +1,104 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.construct; + +import com.jfoenix.controls.JFXButton; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.SkinBase; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.TextAlignment; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; + +public class AdvancedListItemSkin extends SkinBase { + + public AdvancedListItemSkin(AdvancedListItem skinnable) { + super(skinnable); + + StackPane stackPane = new StackPane(); + + BorderPane root = new BorderPane(); + root.setPickOnBounds(false); + + HBox left = new HBox(); + left.setAlignment(Pos.CENTER); + left.setMouseTransparent(true); + + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + ImageView imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + imageView.setPreserveRatio(true); + imageView.imageProperty().bind(skinnable.imageProperty()); + imageView.viewportProperty().bind(skinnable.viewportProperty()); + imageViewContainer.getChildren().setAll(imageView); + + VBox vbox = new VBox(); + vbox.setAlignment(Pos.CENTER_LEFT); + vbox.setPadding(new Insets(0, 0, 0, 10)); + + Label title = new Label(); + title.textProperty().bind(skinnable.titleProperty()); + title.setMaxWidth(90); + title.setStyle("-fx-font-size: 15;"); + title.setTextAlignment(TextAlignment.JUSTIFY); + vbox.getChildren().add(title); + + Label subtitle = new Label(); + subtitle.textProperty().bind(skinnable.subtitleProperty()); + subtitle.setMaxWidth(90); + subtitle.setStyle("-fx-font-size: 10;"); + subtitle.setTextAlignment(TextAlignment.JUSTIFY); + vbox.getChildren().add(subtitle); + + FXUtils.onChangeAndOperate(skinnable.subtitleProperty(), subtitleString -> { + if (subtitleString == null) vbox.getChildren().setAll(title); + else vbox.getChildren().setAll(title, subtitle); + }); + + left.getChildren().setAll(imageViewContainer, vbox); + root.setLeft(left); + + HBox right = new HBox(); + right.setAlignment(Pos.CENTER); + right.setPickOnBounds(false); + + JFXButton settings = new JFXButton(); + FXUtils.setLimitWidth(settings, 40); + settings.getStyleClass().setAll("toggle-icon4"); + settings.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1)); + right.getChildren().setAll(settings); + root.setRight(right); + + stackPane.setStyle("-fx-padding: 10 16 10 16;"); + stackPane.getStyleClass().setAll("transparent"); + stackPane.setPickOnBounds(false); + stackPane.getChildren().setAll(root); + + getChildren().setAll(stackPane); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java index 67d93a3f4..ffe131359 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java @@ -60,7 +60,7 @@ public class ComponentList extends StackPane { vbox.getChildren().add(child); } - public void removeChildren(Node node) { + public void removeChild(Node node) { vbox.getChildren().removeIf(node1 -> node1.getProperties().get("node") == node); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java index 6b7d7656b..b4282dacd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogCloseEvent.java @@ -32,7 +32,7 @@ import javafx.scene.layout.Region; */ public class DialogCloseEvent extends Event { - public static final EventType CLOSE = new EventType<>("CLOSE"); + public static final EventType CLOSE = new EventType<>("DIALOG_CLOSE"); public DialogCloseEvent() { super(CLOSE); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FontComboBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FontComboBox.java index aab8ce69c..2708b3f5a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FontComboBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FontComboBox.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.construct; import com.jfoenix.controls.JFXComboBox; import javafx.beans.NamedArg; -import javafx.collections.FXCollections; import javafx.scene.control.ListCell; import javafx.scene.text.Font; @@ -47,6 +46,7 @@ public class FontComboBox extends JFXComboBox { setOnMouseClicked(e -> { if (loaded) return; getItems().setAll(Font.getFamilies()); + loaded = true; }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java new file mode 100644 index 000000000..15e50c090 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java @@ -0,0 +1,155 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.construct; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventTarget; +import javafx.event.EventType; +import javafx.scene.Node; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionHandler; + +import java.util.Optional; +import java.util.Stack; + +public class Navigator extends StackPane { + private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener"; + + private final Stack stack = new Stack<>(); + private final TransitionHandler animationHandler = new TransitionHandler(this); + private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper(); + + public Navigator(Node init) { + stack.push(init); + getChildren().setAll(init); + } + + public void navigate(Node node) { + FXUtils.checkFxUserThread(); + + Node from = stack.peek(); + if (from == node) + return; + + stack.push(node); + fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING)); + setContent(node); + fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED)); + + EventHandler handler = event -> close(node); + node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler); + node.addEventHandler(PageCloseEvent.CLOSE, handler); + } + + public void close() { + close(stack.peek()); + } + + @SuppressWarnings("unchecked") + public void close(Node from) { + FXUtils.checkFxUserThread(); + + stack.remove(from); + Node node = stack.peek(); + fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING)); + setContent(node); + fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED)); + + Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER)) + .ifPresent(handler -> from.removeEventHandler(PageCloseEvent.CLOSE, (EventHandler) handler)); + } + + public Node getCurrentPage() { + return stack.peek(); + } + + public boolean canGoBack() { + return stack.size() > 1; + } + + private void setContent(Node content) { + animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer()); + + if (content instanceof Region) { + ((Region) content).setMinSize(0, 0); + FXUtils.setOverflowHidden((Region) content, true); + } + } + + public EventHandler getOnNavigated() { + return onNavigated.get(); + } + + public ObjectProperty> onNavigatedProperty() { + return onNavigated; + } + + public void setOnNavigated(EventHandler onNavigated) { + this.onNavigated.set(onNavigated); + } + + private ObjectProperty> onNavigated = new SimpleObjectProperty>(this, "onNavigated") { + @Override + protected void invalidated() { + setEventHandler(NavigationEvent.NAVIGATED, get()); + } + }; + + public EventHandler getOnNavigating() { + return onNavigating.get(); + } + + public ObjectProperty> onNavigatingProperty() { + return onNavigating; + } + + public void setOnNavigating(EventHandler onNavigating) { + this.onNavigating.set(onNavigating); + } + + private ObjectProperty> onNavigating = new SimpleObjectProperty>(this, "onNavigating") { + @Override + protected void invalidated() { + setEventHandler(NavigationEvent.NAVIGATING, get()); + } + }; + + public static class NavigationEvent extends Event { + public static final EventType NAVIGATED = new EventType<>("NAVIGATED"); + public static final EventType NAVIGATING = new EventType<>("NAVIGATING"); + + private final Node node; + + public NavigationEvent(Object source, Node target, EventType eventType) { + super(source, target, eventType); + + this.node = target; + } + + public Node getNode() { + return node; + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PageCloseEvent.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PageCloseEvent.java new file mode 100644 index 000000000..55601ded6 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PageCloseEvent.java @@ -0,0 +1,41 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.construct; + +import javafx.event.Event; +import javafx.event.EventTarget; +import javafx.event.EventType; + +/** + * Indicates a close operation on the navigator page. + * + * @author huangyuhui + */ +public class PageCloseEvent extends Event { + + public static final EventType CLOSE = new EventType<>("PAGE_CLOSE"); + + public PageCloseEvent() { + super(CLOSE); + } + + public PageCloseEvent(Object source, EventTarget target) { + super(source, target, CLOSE); + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/TwoLineListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/TwoLineListItem.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java index 2cbcc6a79..1ad0406c5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/TwoLineListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.construct; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorControl.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorControl.java new file mode 100644 index 000000000..6725fe552 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorControl.java @@ -0,0 +1,201 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.decorator; + +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.layout.Background; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +public class DecoratorControl extends Control { + private final ListProperty drawer = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ListProperty content = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ListProperty container = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ObjectProperty contentBackground = new SimpleObjectProperty<>(); + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty drawerTitle = new SimpleStringProperty(); + private final ObjectProperty onCloseButtonAction = new SimpleObjectProperty<>(); + private final ObjectProperty> onCloseNavButtonAction = new SimpleObjectProperty<>(); + private final ObjectProperty> onBackNavButtonAction = new SimpleObjectProperty<>(); + private final ObjectProperty> onRefreshNavButtonAction = new SimpleObjectProperty<>(); + private final BooleanProperty closeNavButtonVisible = new SimpleBooleanProperty(true); + private final BooleanProperty canRefresh = new SimpleBooleanProperty(false); + private final BooleanProperty canBack = new SimpleBooleanProperty(false); + private final BooleanProperty canClose = new SimpleBooleanProperty(false); + private final Stage primaryStage; + private StackPane drawerWrapper; + + public DecoratorControl(Stage primaryStage) { + this.primaryStage = primaryStage; + + primaryStage.initStyle(StageStyle.UNDECORATED); + } + + public Stage getPrimaryStage() { + return primaryStage; + } + + public StackPane getDrawerWrapper() { + return drawerWrapper; + } + + void setDrawerWrapper(StackPane drawerWrapper) { + this.drawerWrapper = drawerWrapper; + } + + public ObservableList getDrawer() { + return drawer.get(); + } + + public ListProperty drawerProperty() { + return drawer; + } + + public void setDrawer(ObservableList drawer) { + this.drawer.set(drawer); + } + + public ObservableList getContent() { + return content.get(); + } + + public ListProperty contentProperty() { + return content; + } + + public void setContent(ObservableList content) { + this.content.set(content); + } + + public String getTitle() { + return title.get(); + } + + public StringProperty titleProperty() { + return title; + } + + public void setTitle(String title) { + this.title.set(title); + } + + public String getDrawerTitle() { + return drawerTitle.get(); + } + + public StringProperty drawerTitleProperty() { + return drawerTitle; + } + + public void setDrawerTitle(String drawerTitle) { + this.drawerTitle.set(drawerTitle); + } + + public Runnable getOnCloseButtonAction() { + return onCloseButtonAction.get(); + } + + public ObjectProperty onCloseButtonActionProperty() { + return onCloseButtonAction; + } + + public void setOnCloseButtonAction(Runnable onCloseButtonAction) { + this.onCloseButtonAction.set(onCloseButtonAction); + } + + public boolean isCloseNavButtonVisible() { + return closeNavButtonVisible.get(); + } + + public BooleanProperty closeNavButtonVisibleProperty() { + return closeNavButtonVisible; + } + + public void setCloseNavButtonVisible(boolean closeNavButtonVisible) { + this.closeNavButtonVisible.set(closeNavButtonVisible); + } + + public ObservableList getContainer() { + return container.get(); + } + + public ListProperty containerProperty() { + return container; + } + + public void setContainer(ObservableList container) { + this.container.set(container); + } + + public Background getContentBackground() { + return contentBackground.get(); + } + + public ObjectProperty contentBackgroundProperty() { + return contentBackground; + } + + public void setContentBackground(Background contentBackground) { + this.contentBackground.set(contentBackground); + } + + public BooleanProperty canRefreshProperty() { + return canRefresh; + } + + public BooleanProperty canBackProperty() { + return canBack; + } + + public BooleanProperty canCloseProperty() { + return canClose; + } + + public ObjectProperty> onBackNavButtonActionProperty() { + return onBackNavButtonAction; + } + + public ObjectProperty> onCloseNavButtonActionProperty() { + return onCloseNavButtonAction; + } + + public ObjectProperty> onRefreshNavButtonActionProperty() { + return onRefreshNavButtonAction; + } + + @Override + protected Skin createDefaultSkin() { + return new DecoratorSkin(this); + } + + public void minimize() { + primaryStage.setIconified(true); + } + + public void close() { + onCloseButtonAction.get().run(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java new file mode 100644 index 000000000..469b4b4ba --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -0,0 +1,374 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.decorator; + +import com.jfoenix.controls.JFXDialog; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.event.EventHandler; +import javafx.event.EventTarget; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.DragEvent; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.util.Duration; +import org.jackhuang.hmcl.Launcher; +import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD; +import org.jackhuang.hmcl.setting.ConfigHolder; +import org.jackhuang.hmcl.setting.EnumBackgroundImage; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.Navigator; +import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.wizard.Refreshable; +import org.jackhuang.hmcl.ui.wizard.WizardProvider; +import org.jackhuang.hmcl.util.FutureCallback; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Random; +import java.util.function.Consumer; +import java.util.logging.Level; + +import static java.util.stream.Collectors.toList; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; +import static org.jackhuang.hmcl.util.Logging.LOG; + +public class DecoratorController { + private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener"; + + private final DecoratorControl decorator; + private final ImageView welcomeView; + private final Navigator navigator; + private final Node mainPage; + + private JFXDialog dialog; + private StackContainerPane dialogPane; + + public DecoratorController(Stage stage, Node mainPage) { + this.mainPage = mainPage; + + decorator = new DecoratorControl(stage); + decorator.titleProperty().set(Metadata.TITLE); + decorator.setOnCloseButtonAction(Launcher::stopApplication); + + navigator = new Navigator(mainPage); + navigator.setOnNavigating(this::onNavigating); + navigator.setOnNavigated(this::onNavigated); + + decorator.getContent().setAll(navigator); + decorator.onCloseNavButtonActionProperty().set(e -> close()); + decorator.onBackNavButtonActionProperty().set(e -> back()); + decorator.onRefreshNavButtonActionProperty().set(e -> refresh()); + + welcomeView = new ImageView(); + welcomeView.setImage(new Image("/assets/img/welcome.png")); + welcomeView.setCursor(Cursor.HAND); + welcomeView.setOnMouseClicked(e -> { + Timeline nowAnimation = new Timeline(); + nowAnimation.getKeyFrames().addAll( + new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)), + new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)), + new KeyFrame(new Duration(300), e2 -> decorator.getContainer().remove(welcomeView)) + ); + nowAnimation.play(); + }); + if (ConfigHolder.isNewlyCreated() && config().getLocalization().getLocale() == Locale.CHINA) + decorator.getContainer().setAll(welcomeView); + + setupBackground(); + + setupAuthlibInjectorDnD(); + } + + public DecoratorControl getDecorator() { + return decorator; + } + + // ==== Background ==== + + private void setupBackground() { + decorator.backgroundProperty().bind( + Bindings.createObjectBinding( + () -> { + Image image = null; + if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) { + image = tryLoadImage(Paths.get(config().getBackgroundImage())) + .orElse(null); + } + if (image == null) { + image = loadDefaultBackgroundImage(); + } + return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true))); + }, + config().backgroundImageTypeProperty(), + config().backgroundImageProperty())); + } + + private Image defaultBackground = new Image("/assets/img/background.jpg"); + + /** + * Load background image from bg/, background.png, background.jpg + */ + private Image loadDefaultBackgroundImage() { + Optional image = randomImageIn(Paths.get("bg")); + if (!image.isPresent()) { + image = tryLoadImage(Paths.get("background.png")); + } + if (!image.isPresent()) { + image = tryLoadImage(Paths.get("background.jpg")); + } + return image.orElse(defaultBackground); + } + + private Optional randomImageIn(Path imageDir) { + if (!Files.isDirectory(imageDir)) { + return Optional.empty(); + } + + List candidates; + try { + candidates = Files.list(imageDir) + .filter(Files::isRegularFile) + .filter(it -> { + String filename = it.getFileName().toString(); + return filename.endsWith(".png") || filename.endsWith(".jpg"); + }) + .collect(toList()); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to list files in ./bg", e); + return Optional.empty(); + } + + Random rnd = new Random(); + while (candidates.size() > 0) { + int selected = rnd.nextInt(candidates.size()); + Optional loaded = tryLoadImage(candidates.get(selected)); + if (loaded.isPresent()) { + return loaded; + } else { + candidates.remove(selected); + } + } + return Optional.empty(); + } + + private Optional tryLoadImage(Path path) { + if (Files.isRegularFile(path)) { + try { + return Optional.of(new Image(path.toAbsolutePath().toUri().toString())); + } catch (IllegalArgumentException ignored) { + } + } + return Optional.empty(); + } + + // ==== Navigation ==== + + public Navigator getNavigator() { + return navigator; + } + + private void close() { + if (navigator.getCurrentPage() instanceof DecoratorPage) { + DecoratorPage page = (DecoratorPage) navigator.getCurrentPage(); + + page.onForceToClose(); + } else { + navigator.close(); + } + } + + private void back() { + if (navigator.getCurrentPage() instanceof DecoratorPage) { + DecoratorPage page = (DecoratorPage) navigator.getCurrentPage(); + + if (page.onClose()) + navigator.close(); + } else { + navigator.close(); + } + } + + private void refresh() { + if (navigator.getCurrentPage() instanceof Refreshable) { + Refreshable refreshable = (Refreshable) navigator.getCurrentPage(); + + if (refreshable.canRefreshProperty().get()) + refreshable.refresh(); + } + } + + private void onNavigating(Navigator.NavigationEvent event) { + Node from = event.getNode(); + + if (from instanceof DecoratorPage) + ((DecoratorPage) from).onClose(); + } + + private void onNavigated(Navigator.NavigationEvent event) { + Node to = event.getNode(); + + if (to instanceof Refreshable) { + decorator.canRefreshProperty().bind(((Refreshable) to).canRefreshProperty()); + } else { + decorator.canRefreshProperty().unbind(); + decorator.canRefreshProperty().set(false); + } + + if (to instanceof DecoratorPage) { + decorator.drawerTitleProperty().bind(((DecoratorPage) to).titleProperty()); + decorator.canCloseProperty().set(((DecoratorPage) to).canForceToClose()); + } else { + decorator.drawerTitleProperty().unbind(); + decorator.drawerTitleProperty().set(""); + decorator.canCloseProperty().set(false); + } + + decorator.canBackProperty().set(navigator.canGoBack()); + + if (navigator.canGoBack()) { + decorator.setContentBackground(new Background(new BackgroundFill(Color.rgb(244, 244, 244, 0.5), CornerRadii.EMPTY, Insets.EMPTY))); + } else { + decorator.setContentBackground(null); + } + + if (to instanceof Region) { + Region region = (Region) to; + // Let root pane fix window size. + StackPane parent = (StackPane) region.getParent(); + region.prefWidthProperty().bind(parent.widthProperty()); + region.prefHeightProperty().bind(parent.heightProperty()); + } + } + + // ==== Dialog ==== + + public void showDialog(Node node) { + FXUtils.checkFxUserThread(); + + if (dialog == null) { + dialog = new JFXDialog(); + dialogPane = new StackContainerPane(); + + dialog.setContent(dialogPane); + dialog.setDialogContainer(decorator.getDrawerWrapper()); + dialog.setOverlayClose(false); + dialog.show(); + } + + dialogPane.push(node); + + EventHandler handler = event -> closeDialog(node); + node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler); + node.addEventHandler(DialogCloseEvent.CLOSE, handler); + } + + @SuppressWarnings("unchecked") + private void closeDialog(Node node) { + FXUtils.checkFxUserThread(); + + Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER)) + .ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler) handler)); + + if (dialog != null) { + dialogPane.pop(node); + + if (dialogPane.getChildren().isEmpty()) { + dialog.close(); + dialog = null; + dialogPane = null; + } + } + } + + public void showDialog(String text) { + showDialog(text, null); + } + + public void showDialog(String text, String title) { + showDialog(text, title, MessageBox.INFORMATION_MESSAGE); + } + + public void showDialog(String text, String title, int type) { + showDialog(text, title, type, null); + } + + public void showDialog(String text, String title, int type, Runnable onAccept) { + showDialog(new MessageDialogPane(text, title, type, onAccept)); + } + + public void showConfirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) { + showDialog(new MessageDialogPane(text, title, onAccept, onCancel)); + } + + public InputDialogPane showInputDialog(String text, FutureCallback onResult) { + InputDialogPane pane = new InputDialogPane(text, onResult); + showDialog(pane); + return pane; + } + + public Region showTaskDialog(TaskExecutor executor, String title, String subtitle) { + return showTaskDialog(executor, title, subtitle, null); + } + + public Region showTaskDialog(TaskExecutor executor, String title, String subtitle, Consumer onCancel) { + TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel); + pane.setTitle(title); + pane.setSubtitle(subtitle); + pane.setExecutor(executor); + showDialog(pane); + return pane; + } + + // ==== Wizard ==== + + public void startWizard(WizardProvider wizardProvider) { + startWizard(wizardProvider, null); + } + + public void startWizard(WizardProvider wizardProvider, String category) { + FXUtils.checkFxUserThread(); + + getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category)); + } + + // ==== Authlib Injector DnD ==== + + private void setupAuthlibInjectorDnD() { + decorator.addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler()); + decorator.addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler( + url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url)))); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java similarity index 80% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java index 239e3ee5e..658c73e3f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/DecoratorPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorPage.java @@ -15,13 +15,21 @@ * 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; +package org.jackhuang.hmcl.ui.decorator; import javafx.beans.property.StringProperty; public interface DecoratorPage { StringProperty titleProperty(); - default void onClose() { + default boolean canForceToClose() { + return false; + } + + default boolean onClose() { + return true; + } + + default void onForceToClose() { } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java new file mode 100644 index 000000000..f43925753 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java @@ -0,0 +1,422 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.decorator; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.svg.SVGGlyph; +import javafx.beans.binding.Bindings; +import javafx.collections.ListChangeListener; +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.SkinBase; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.stage.Stage; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.util.Lang; + +public class DecoratorSkin extends SkinBase { + 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 final BorderPane titleContainer; + private final StackPane contentPlaceHolder; + private final JFXButton refreshNavButton; + private final JFXButton closeNavButton; + private final HBox navLeft; + private final Stage primaryStage; + + private double xOffset, yOffset, newX, newY, initX, initY; + private boolean allowMove, isDragging; + private BoundingBox originalBox, maximizedBox; + + /** + * Constructor for all SkinBase instances. + * + * @param control The control for which this Skin should attach to. + */ + public DecoratorSkin(DecoratorControl control) { + super(control); + + primaryStage = control.getPrimaryStage(); + + minus.fillProperty().bind(Theme.foregroundFillBinding()); + + DecoratorControl skinnable = getSkinnable(); + + BorderPane root = new BorderPane(); + root.getStyleClass().setAll("jfx-decorator", "resize-border"); + root.setMaxHeight(519); + root.setMaxWidth(800); + + StackPane drawerWrapper = new StackPane(); + skinnable.setDrawerWrapper(drawerWrapper); + drawerWrapper.getStyleClass().setAll("jfx-decorator-drawer"); + drawerWrapper.backgroundProperty().bind(skinnable.backgroundProperty()); + FXUtils.setOverflowHidden(drawerWrapper, true); + { + BorderPane drawer = new BorderPane(); + + { + BorderPane leftRootPane = new BorderPane(); + FXUtils.setLimitWidth(leftRootPane, 200); + leftRootPane.getStyleClass().setAll("jfx-decorator-content-container"); + + StackPane drawerContainer = new StackPane(); + drawerContainer.getStyleClass().setAll("gray-background"); + Bindings.bindContent(drawerContainer.getChildren(), skinnable.drawerProperty()); + leftRootPane.setCenter(drawerContainer); + + Rectangle separator = new Rectangle(); + separator.heightProperty().bind(drawer.heightProperty()); + separator.setWidth(1); + separator.setFill(Color.GRAY); + + leftRootPane.setRight(separator); + + drawer.setLeft(leftRootPane); + } + + { + contentPlaceHolder = new StackPane(); + contentPlaceHolder.getStyleClass().setAll("jfx-decorator-content-container"); + contentPlaceHolder.backgroundProperty().bind(skinnable.contentBackgroundProperty()); + FXUtils.setOverflowHidden(contentPlaceHolder, true); + Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty()); + + drawer.setCenter(contentPlaceHolder); + } + + drawerWrapper.getChildren().add(drawer); + } + + { + StackPane container = new StackPane(); + Bindings.bindContent(container.getChildren(), skinnable.containerProperty()); + ListChangeListener listener = new ListChangeListener() { + @Override + public void onChanged(Change c) { + if (skinnable.getContainer().isEmpty()) { + container.setMouseTransparent(true); + container.setVisible(false); + } else { + container.setMouseTransparent(false); + container.setVisible(true); + } + } + }; + skinnable.containerProperty().addListener(listener); + listener.onChanged(null); + + drawerWrapper.getChildren().add(container); + } + + root.setCenter(drawerWrapper); + + titleContainer = new BorderPane(); + titleContainer.setOnMouseReleased(this::onMouseReleased); + titleContainer.setOnMouseDragged(this::onMouseDragged); + titleContainer.setOnMouseMoved(this::onMouseMoved); + titleContainer.setPickOnBounds(false); + titleContainer.setMinHeight(40); + titleContainer.getStyleClass().setAll("jfx-tool-bar"); + titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true); + titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { + if (!isDragging) allowMove = false; + }); + + Rectangle rectangle = new Rectangle(0, 0, 0, 0); + rectangle.widthProperty().bind(titleContainer.widthProperty()); + rectangle.heightProperty().bind(Bindings.createDoubleBinding(() -> titleContainer.getHeight() + 100, titleContainer.heightProperty())); + titleContainer.setClip(rectangle); + { + BorderPane titleWrapper = new BorderPane(); + FXUtils.setLimitWidth(titleWrapper, 200); + { + Label lblTitle = new Label(); + BorderPane.setMargin(lblTitle, new Insets(0, 0, 0, 3)); + lblTitle.setStyle("-fx-background-color: transparent; -fx-text-fill: -fx-base-text-fill; -fx-font-size: 15px;"); + lblTitle.setMouseTransparent(true); + lblTitle.textProperty().bind(skinnable.titleProperty()); + BorderPane.setAlignment(lblTitle, Pos.CENTER); + titleWrapper.setCenter(lblTitle); + + Rectangle separator = new Rectangle(); + separator.heightProperty().bind(titleWrapper.heightProperty()); + separator.setWidth(1); + separator.setFill(Color.GRAY); + titleWrapper.setRight(separator); + } + titleContainer.setLeft(titleWrapper); + + BorderPane navBar = new BorderPane(); + { + navLeft = new HBox(); + navLeft.setAlignment(Pos.CENTER_LEFT); + navLeft.setPadding(new Insets(0, 5, 0, 5)); + { + JFXButton backNavButton = new JFXButton(); + backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1)); + backNavButton.getStyleClass().setAll("jfx-decorator-button"); + backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); + backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty()); + backNavButton.visibleProperty().bind(skinnable.canBackProperty()); + + closeNavButton = new JFXButton(); + closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1)); + closeNavButton.getStyleClass().setAll("jfx-decorator-button"); + closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); + closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty()); + + navLeft.getChildren().setAll(backNavButton); + + skinnable.canCloseProperty().addListener((a, b, newValue) -> { + if (newValue) navLeft.getChildren().setAll(backNavButton, closeNavButton); + else navLeft.getChildren().setAll(backNavButton); + }); + } + navBar.setLeft(navLeft); + + VBox navCenter = new VBox(); + navCenter.setAlignment(Pos.CENTER_LEFT); + Label titleLabel = new Label(); + titleLabel.getStyleClass().setAll("jfx-decorator-title"); + titleLabel.textProperty().bind(skinnable.drawerTitleProperty()); + navCenter.getChildren().setAll(titleLabel); + navBar.setCenter(navCenter); + + HBox navRight = new HBox(); + navRight.setAlignment(Pos.CENTER_RIGHT); + refreshNavButton = new JFXButton(); + refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)); + refreshNavButton.getStyleClass().setAll("jfx-decorator-button"); + refreshNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); + refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty()); + refreshNavButton.visibleProperty().bind(skinnable.canRefreshProperty()); + navRight.getChildren().setAll(refreshNavButton); + navBar.setRight(navRight); + } + titleContainer.setCenter(navBar); + + HBox buttonsContainer = new HBox(); + buttonsContainer.setStyle("-fx-background-color: transparent;"); + buttonsContainer.setAlignment(Pos.CENTER_RIGHT); + buttonsContainer.setPadding(new Insets(4)); + { + Rectangle separator = new Rectangle(); + separator.visibleProperty().bind(refreshNavButton.visibleProperty()); + separator.heightProperty().bind(navBar.heightProperty()); + + JFXButton btnMin = new JFXButton(); + StackPane pane = new StackPane(minus); + pane.setAlignment(Pos.CENTER); + btnMin.setGraphic(pane); + btnMin.getStyleClass().setAll("jfx-decorator-button"); + btnMin.setOnAction(e -> skinnable.minimize()); + + JFXButton btnClose = new JFXButton(); + btnClose.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1)); + btnClose.getStyleClass().setAll("jfx-decorator-button"); + btnClose.setOnAction(e -> skinnable.close()); + + buttonsContainer.getChildren().setAll(separator, btnMin, btnClose); + } + titleContainer.setRight(buttonsContainer); + } + root.setTop(titleContainer); + + getChildren().setAll(root); + + getSkinnable().closeNavButtonVisibleProperty().addListener((a, b, newValue) -> { + if (newValue) navLeft.getChildren().add(closeNavButton); + else navLeft.getChildren().remove(closeNavButton); + }); + } + + 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 < getSkinnable().getWidth() && x > getSkinnable().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 < getSkinnable().getHeight() && y > getSkinnable().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; + } + } + + // ==== + + protected void onMouseMoved(MouseEvent mouseEvent) { + if (!primaryStage.isFullScreen()) { + if (!primaryStage.isResizable()) + updateInitMouseValues(mouseEvent); + else { + double x = mouseEvent.getX(), y = mouseEvent.getY(); + Bounds boundsInParent = getSkinnable().getBoundsInParent(); + if (getSkinnable().getBorder() != null && getSkinnable().getBorder().getStrokes().size() > 0) { + double borderWidth = contentPlaceHolder.snappedLeftInset(); + if (this.isRightEdge(x, y, boundsInParent)) { + if (y < borderWidth) { + getSkinnable().setCursor(Cursor.NE_RESIZE); + } else if (y > getSkinnable().getHeight() - borderWidth) { + getSkinnable().setCursor(Cursor.SE_RESIZE); + } else { + getSkinnable().setCursor(Cursor.E_RESIZE); + } + } else if (this.isLeftEdge(x, y, boundsInParent)) { + if (y < borderWidth) { + getSkinnable().setCursor(Cursor.NW_RESIZE); + } else if (y > getSkinnable().getHeight() - borderWidth) { + getSkinnable().setCursor(Cursor.SW_RESIZE); + } else { + getSkinnable().setCursor(Cursor.W_RESIZE); + } + } else if (this.isTopEdge(x, y, boundsInParent)) { + getSkinnable().setCursor(Cursor.N_RESIZE); + } else if (this.isBottomEdge(x, y, boundsInParent)) { + getSkinnable().setCursor(Cursor.S_RESIZE); + } else { + getSkinnable().setCursor(Cursor.DEFAULT); + } + + this.updateInitMouseValues(mouseEvent); + } + } + } else { + getSkinnable().setCursor(Cursor.DEFAULT); + } + } + + protected void onMouseReleased(MouseEvent mouseEvent) { + isDragging = false; + } + + protected 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.newX = mouseEvent.getScreenX(); + this.newY = mouseEvent.getScreenY(); + double deltaX = this.newX - this.initX; + double deltaY = this.newY - this.initY; + Cursor cursor = getSkinnable().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(); + } + } + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java new file mode 100644 index 000000000..7e16b0c62 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java @@ -0,0 +1,126 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.decorator; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.animation.TransitionHandler; +import org.jackhuang.hmcl.ui.construct.PageCloseEvent; +import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer; +import org.jackhuang.hmcl.ui.wizard.*; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorDialogWizardDisplayer, Refreshable, DecoratorPage { + private final StringProperty title = new SimpleStringProperty(); + private final BooleanProperty canRefresh = new SimpleBooleanProperty(); + + private final TransitionHandler transitionHandler = new TransitionHandler(this); + private final WizardController wizardController = new WizardController(this); + private final Queue cancelQueue = new ConcurrentLinkedQueue<>(); + + private final String category; + + private Node nowPage; + + public DecoratorWizardDisplayer(WizardProvider provider) { + this(provider, null); + } + + public DecoratorWizardDisplayer(WizardProvider provider, String category) { + this.category = category; + + wizardController.setProvider(provider); + wizardController.onStart(); + + getStyleClass().setAll("white-background"); + } + + @Override + public StringProperty titleProperty() { + return title; + } + + @Override + public BooleanProperty canRefreshProperty() { + return canRefresh; + } + + @Override + public WizardController getWizardController() { + return wizardController; + } + + @Override + public Queue getCancelQueue() { + return cancelQueue; + } + + @Override + public void onStart() { + + } + + @Override + public void onEnd() { + fireEvent(new PageCloseEvent()); + } + + @Override + public void navigateTo(Node page, Navigation.NavigationDirection nav) { + nowPage = page; + + transitionHandler.setContent(page, nav.getAnimation().getAnimationProducer()); + + canRefresh.set(page instanceof Refreshable); + + String prefix = category == null ? "" : category + " - "; + + if (page instanceof WizardPage) + title.set(prefix + ((WizardPage) page).getTitle()); + } + + @Override + public boolean canForceToClose() { + return true; + } + + @Override + public void onForceToClose() { + wizardController.onCancel(); + } + + @Override + public boolean onClose() { + if (wizardController.canPrev()) { + wizardController.onPrev(true); + return false; + } else + return true; + } + + @Override + public void refresh() { + ((Refreshable) nowPage).refresh(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java index 2c6721799..e11925936 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadWizardProvider.java @@ -25,11 +25,12 @@ import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.task.Schedulers; 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; @@ -38,17 +39,23 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class DownloadWizardProvider implements WizardProvider { private Profile profile; + private final int type; + + public DownloadWizardProvider(int type) { + this.type = type; + } @Override public void start(Map settings) { - profile = Settings.instance().getSelectedProfile(); + profile = Profiles.getSelectedProfile(); settings.put(PROFILE, profile); } private Task finishVersionDownloadingAsync(Map settings) { GameBuilder builder = profile.getDependency().gameBuilder(); - builder.name((String) settings.get("name")); + String name = (String) settings.get("name"); + builder.name(name); builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion()); if (settings.containsKey("forge")) @@ -60,7 +67,8 @@ public final class DownloadWizardProvider implements WizardProvider { if (settings.containsKey("optifine")) builder.version((RemoteVersion) settings.get("optifine")); - return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions()); + return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions()) + .then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name))); } private Task finishModpackInstallingAsync(Map settings) { @@ -72,7 +80,8 @@ public final class DownloadWizardProvider implements WizardProvider { String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null); if (selected == null || modpack == null || name == null) return null; - return ModpackHelper.getInstallTask(profile, selected, name, modpack); + return ModpackHelper.getInstallTask(profile, selected, name, modpack) + .then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name))); } @Override @@ -80,7 +89,7 @@ public final class DownloadWizardProvider implements WizardProvider { settings.put("success_message", i18n("install.success")); settings.put("failure_message", i18n("install.failed")); - switch (Lang.parseInt(settings.get(InstallTypePage.INSTALL_TYPE), -1)) { + switch (type) { case 0: return finishVersionDownloadingAsync(settings); case 1: return finishModpackInstallingAsync(settings); default: return null; @@ -92,16 +101,13 @@ public final class DownloadWizardProvider implements WizardProvider { 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) { + switch (type) { case 0: return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", 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 + ", pages: " + controller.getPages()); + throw new IllegalStateException("Error step " + step + ", subStep " + type + ", settings: " + settings + ", pages: " + controller.getPages()); } default: throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallTypePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallTypePage.java deleted file mode 100644 index 00a119b5f..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallTypePage.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.ui.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 static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -import java.util.Map; - -public final class InstallTypePage extends StackPane implements WizardPage { - - @FXML private JFXListView list; - - public InstallTypePage(WizardController controller) { - - FXUtils.loadFXML(this, "/assets/fxml/download/dltype.fxml"); - list.setOnMouseClicked(e -> { - if (list.getSelectionModel().getSelectedIndex() < 0) - return; - controller.getSettings().put(INSTALL_TYPE, list.getSelectionModel().getSelectedIndex()); - controller.onNext(); - }); - } - - @Override - public void cleanup(Map settings) { - settings.remove(INSTALL_TYPE); - } - - @Override - public String getTitle() { - return i18n("install.select"); - } - - public static final String INSTALL_TYPE = "INSTALL_TYPE"; -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java index b5d08c8c5..c98451914 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.download; import javafx.scene.Node; -import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index dfc810060..6e4ff09d1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -27,7 +27,6 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.VersionList; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.FXUtils; @@ -39,7 +38,6 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; public final class VersionsPage extends StackPane implements WizardPage, Refreshable { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java index 4a9ee699e..93469c1e4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java @@ -79,8 +79,6 @@ public final class ExportWizardProvider implements WizardProvider { if (includeLauncher) { dependency = dependency.then(Task.of(() -> { - boolean flag = true; - try (Zipper zip = new Zipper(modpackFile.toPath())) { Config exported = new Config(); exported.setBackgroundImageType(config().getBackgroundImageType()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileAdvancedListItem.java new file mode 100644 index 000000000..47bd4b01a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileAdvancedListItem.java @@ -0,0 +1,50 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.profile; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.ui.construct.AdvancedListItem; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class ProfileAdvancedListItem extends AdvancedListItem { + private ObjectProperty profile = new SimpleObjectProperty() { + + @Override + protected void invalidated() { + Profile profile = get(); + if (profile == null) { + } else { + titleProperty().set(Profiles.getProfileDisplayName(profile)); + subtitleProperty().set(profile.getGameDir().toString()); + } + } + }; + + public ProfileAdvancedListItem() { + imageProperty().set(new Image("/assets/img/craft_table.png")); + } + + public ObjectProperty profileProperty() { + return profile; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java new file mode 100644 index 000000000..fec55ec7b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java @@ -0,0 +1,82 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.profile; + +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.ToggleGroup; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.util.MappedObservableList; + +import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class ProfileList extends Control implements DecoratorPage { + private final StringProperty title = new SimpleStringProperty(i18n("profile.manage")); + private final ListProperty items = new SimpleListProperty<>(FXCollections.observableArrayList()); + private ObjectProperty selectedProfile = new SimpleObjectProperty() { + { + items.addListener(onInvalidating(this::invalidated)); + } + + @Override + protected void invalidated() { + Profile selected = get(); + items.forEach(item -> item.selectedProperty().set(item.getProfile() == selected)); + } + }; + + private ToggleGroup toggleGroup; + + public ProfileList() { + toggleGroup = new ToggleGroup(); + + items.bindContent(MappedObservableList.create( + Profiles.profilesProperty(), + profile -> new ProfileListItem(toggleGroup, profile))); + + selectedProfile.bindBidirectional(Profiles.selectedProfileProperty()); + toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { + if (toggle == null || toggle.getUserData() == null) return; + selectedProfile.set(((ProfileListItem) toggle.getUserData()).getProfile()); + }); + } + + @Override + protected Skin createDefaultSkin() { + return new ProfileListSkin(this); + } + + public void addNewProfile() { + Controllers.navigate(new ProfilePage(null)); + } + + public ListProperty itemsProperty() { + return items; + } + + @Override + public StringProperty titleProperty() { + return title; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java new file mode 100644 index 000000000..e49db6441 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java @@ -0,0 +1,83 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.profile; + +import javafx.beans.property.*; +import javafx.geometry.Rectangle2D; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; +import org.jackhuang.hmcl.auth.offline.OfflineAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.task.Schedulers; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class ProfileListItem extends Control { + private final Profile profile; + private final ToggleGroup toggleGroup; + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty subtitle = new SimpleStringProperty(); + private final BooleanProperty selected = new SimpleBooleanProperty(); + + public ProfileListItem(ToggleGroup toggleGroup, Profile profile) { + this.profile = profile; + this.toggleGroup = toggleGroup; + + title.set(Profiles.getProfileDisplayName(profile)); + subtitle.set(profile.getGameDir().toString()); + selected.set(Profiles.selectedProfileProperty().get() == profile); + } + + @Override + protected Skin createDefaultSkin() { + return new ProfileListItemSkin(this); + } + + public ToggleGroup getToggleGroup() { + return toggleGroup; + } + + public Profile getProfile() { + return profile; + } + + public StringProperty titleProperty() { + return title; + } + + public StringProperty subtitleProperty() { + return subtitle; + } + + public BooleanProperty selectedProperty() { + return selected; + } + + public void remove() { + Profiles.getProfiles().remove(profile); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java new file mode 100644 index 000000000..841c3adaf --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java @@ -0,0 +1,85 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.profile; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXRadioButton; +import com.jfoenix.effects.JFXDepthManager; +import javafx.geometry.Pos; +import javafx.scene.control.SkinBase; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; + +public class ProfileListItemSkin extends SkinBase { + + public ProfileListItemSkin(ProfileListItem skinnable) { + super(skinnable); + + BorderPane root = new BorderPane(); + + JFXRadioButton chkSelected = new JFXRadioButton(); + BorderPane.setAlignment(chkSelected, Pos.CENTER); + chkSelected.setUserData(skinnable); + chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty()); + chkSelected.setToggleGroup(skinnable.getToggleGroup()); + root.setLeft(chkSelected); + + HBox center = new HBox(); + center.setSpacing(8); + center.setAlignment(Pos.CENTER_LEFT); + + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + ImageView imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + imageView.imageProperty().set(new Image("/assets/img/craft_table.png")); + imageViewContainer.getChildren().setAll(imageView); + + TwoLineListItem item = new TwoLineListItem(); + BorderPane.setAlignment(item, Pos.CENTER); + center.getChildren().setAll(imageView, item); + root.setCenter(center); + + HBox right = new HBox(); + right.setAlignment(Pos.CENTER_RIGHT); + + JFXButton btnRemove = new JFXButton(); + btnRemove.setOnMouseClicked(e -> skinnable.remove()); + btnRemove.getStyleClass().add("toggle-icon4"); + BorderPane.setAlignment(btnRemove, Pos.CENTER); + btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1)); + right.getChildren().add(btnRemove); + root.setRight(right); + + root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;"); + JFXDepthManager.setDepth(root, 1); + item.titleProperty().bind(skinnable.titleProperty()); + item.subtitleProperty().bind(skinnable.subtitleProperty()); + + getChildren().setAll(root); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListSkin.java new file mode 100644 index 000000000..588fda41f --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListSkin.java @@ -0,0 +1,77 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.profile; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXScrollPane; +import javafx.beans.binding.Bindings; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; + +public class ProfileListSkin extends SkinBase { + + public ProfileListSkin(ProfileList skinnable) { + super(skinnable); + + StackPane root = new StackPane(); + + ScrollPane scrollPane = new ScrollPane(); + { + scrollPane.setFitToWidth(true); + + VBox accountList = new VBox(); + accountList.maxWidthProperty().bind(scrollPane.widthProperty()); + accountList.setSpacing(10); + accountList.setStyle("-fx-padding: 10 10 10 10;"); + + Bindings.bindContent(accountList.getChildren(), skinnable.itemsProperty()); + + scrollPane.setContent(accountList); + JFXScrollPane.smoothScrolling(scrollPane); + } + + VBox vBox = new VBox(); + { + vBox.setAlignment(Pos.BOTTOM_RIGHT); + vBox.setPickOnBounds(false); + vBox.setPadding(new Insets(15)); + vBox.setSpacing(15); + + JFXButton btnAdd = new JFXButton(); + FXUtils.setLimitWidth(btnAdd, 40); + FXUtils.setLimitHeight(btnAdd, 40); + btnAdd.getStyleClass().setAll("jfx-button-raised-round"); + btnAdd.setButtonType(JFXButton.ButtonType.RAISED); + btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1)); + btnAdd.setOnMouseClicked(e -> skinnable.addNewProfile()); + + vBox.getChildren().setAll(btnAdd); + } + + root.getChildren().setAll(scrollPane, vBox); + + getChildren().setAll(root); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java similarity index 93% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java index c50b96c08..b85c904ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ProfilePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.profile; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; @@ -27,9 +27,10 @@ import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; -import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.FileItem; -import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.StringUtils; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -80,7 +81,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage { @FXML private void onDelete() { if (profile != null) { - Settings.instance().deleteProfile(profile); + Profiles.getProfiles().remove(profile); Controllers.navigate(null); } } @@ -99,10 +100,9 @@ public final class ProfilePage extends StackPane implements DecoratorPage { } Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation())); newProfile.setUseRelativePath(toggleUseRelativePath.isSelected()); - Settings.instance().putProfile(newProfile); + Profiles.getProfiles().add(newProfile); } - Settings.instance().onProfileLoading(); Controllers.navigate(null); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java new file mode 100644 index 000000000..58fbb22c2 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java @@ -0,0 +1,74 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.concurrency.JFXUtilities; +import javafx.beans.InvalidationListener; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.ui.construct.AdvancedListItem; +import org.jackhuang.hmcl.ui.WeakListenerHelper; + +import java.io.File; + +public class GameAdvancedListItem extends AdvancedListItem { + private final WeakListenerHelper helper = new WeakListenerHelper(); + + private Profile profile; + private InvalidationListener listener = o -> loadVersion(); + + public GameAdvancedListItem() { + Profiles.selectedProfileProperty().addListener(helper.weak((a, b, newValue) -> { + JFXUtilities.runInFX(() -> loadProfile(newValue)); + })); + helper.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> { + JFXUtilities.runInFX(() -> { + if (profile != null && profile.getRepository() == event.getSource()) + loadVersion(); + }); + })); + loadProfile(Profiles.getSelectedProfile()); + } + + private void loadProfile(Profile newProfile) { + if (profile != null) + profile.selectedVersionProperty().removeListener(listener); + profile = newProfile; + profile.selectedVersionProperty().addListener(listener); + loadVersion(); + } + + private void loadVersion() { + Profile profile = this.profile; + if (profile == null || !profile.getRepository().isLoaded()) return; + String version = profile.getSelectedVersion(); + File iconFile = profile.getRepository().getVersionIcon(version); + + JFXUtilities.runInFX(() -> { + if (iconFile.exists()) + imageProperty().set(new Image("file:" + iconFile.getAbsolutePath())); + else + imageProperty().set(new Image("/assets/img/grass.png")); + + titleProperty().set(version); + }); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java new file mode 100644 index 000000000..5655cefd3 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java @@ -0,0 +1,129 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.concurrency.JFXUtilities; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.ToggleGroup; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshedVersionsEvent; +import org.jackhuang.hmcl.event.RefreshingVersionsEvent; +import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.download.DownloadWizardProvider; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.util.VersionNumber; +import org.jackhuang.hmcl.util.i18n.I18n; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class GameList extends Control implements DecoratorPage { + private final StringProperty title = new SimpleStringProperty(I18n.i18n("version.manage")); + private final BooleanProperty loading = new SimpleBooleanProperty(true); + private final ListProperty items = new SimpleListProperty<>(FXCollections.observableArrayList()); + + private Profile profile; + private ToggleGroup toggleGroup; + + public GameList() { + EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> { + if (event.getSource() == profile.getRepository()) + loadVersions((HMCLGameRepository) event.getSource()); + }); + EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> { + if (event.getSource() == profile.getRepository()) + JFXUtilities.runInFX(() -> loading.set(true)); + }); + Profiles.selectedProfileProperty().addListener((a, b, newValue) -> profile = newValue); + + profile = Profiles.getSelectedProfile(); + if (profile.getRepository().isLoaded()) + loadVersions(profile.getRepository()); + else + profile.getRepository().refreshVersionsAsync().start(); + } + + private void loadVersions(HMCLGameRepository repository) { + toggleGroup = new ToggleGroup(); + List children = repository.getVersions().parallelStream() + .filter(version -> !version.isHidden()) + .sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId()))) + .map(version -> new GameListItem(toggleGroup, profile, version.getId())) + .collect(Collectors.toList()); + JFXUtilities.runInFX(() -> { + if (profile == repository.getProfile()) { + loading.set(false); + items.setAll(children); + children.forEach(GameListItem::checkSelection); + + profile.selectedVersionProperty().addListener((a, b, newValue) -> { + toggleGroup.getToggles().stream() + .filter(it -> ((GameListItem) it.getUserData()).getVersion().equals(newValue)) + .findFirst() + .ifPresent(it -> it.setSelected(true)); + }); + } + toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { + GameListItem model = (GameListItem) toggle.getUserData(); + model.getProfile().setSelectedVersion(model.getVersion()); + }); + }); + } + + @Override + protected Skin createDefaultSkin() { + return new GameListSkin(this); + } + + public void addNewGame() { + Controllers.getDecorator().startWizard(new DownloadWizardProvider(0), i18n("install.new_game")); + } + + public void importModpack() { + Controllers.getDecorator().startWizard(new DownloadWizardProvider(1), i18n("install.modpack")); + } + + public void refresh() { + profile.getRepository().refreshVersionsAsync().start(); + } + + public void modifyGlobalGameSettings() { + Versions.modifyGlobalSettings(profile); + } + + @Override + public StringProperty titleProperty() { + return title; + } + + public BooleanProperty loadingProperty() { + return loading; + } + + public ListProperty itemsProperty() { + return items; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java new file mode 100644 index 000000000..daf47bcee --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java @@ -0,0 +1,148 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.versions; + +import javafx.beans.property.*; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.Image; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameVersion; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.ui.Controllers; + +import java.io.File; + +import static org.jackhuang.hmcl.util.StringUtils.removePrefix; +import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class GameListItem extends Control { + private final Profile profile; + private final String version; + private final boolean isModpack; + private final ToggleGroup toggleGroup; + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty subtitle = new SimpleStringProperty(); + private final BooleanProperty selected = new SimpleBooleanProperty(); + private final ObjectProperty image = new SimpleObjectProperty<>(); + + public GameListItem(ToggleGroup toggleGroup, Profile profile, String id) { + this.profile = profile; + this.version = id; + this.toggleGroup = toggleGroup; + this.isModpack = profile.getRepository().isModpack(id); + + String game = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse("Unknown"); + + StringBuilder libraries = new StringBuilder(game); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getVersion(id)); + analyzer.getForge().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", "")))); + analyzer.getLiteLoader().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", "")))); + analyzer.getOptiFine().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", "")))); + + title.set(id); + subtitle.set(libraries.toString()); + selected.set(profile.getSelectedVersion().equals(id)); + + File iconFile = profile.getRepository().getVersionIcon(version); + if (iconFile.exists()) + image.set(new Image("file:" + iconFile.getAbsolutePath())); + else + image.set(new Image("/assets/img/grass.png")); + } + + @Override + protected Skin createDefaultSkin() { + return new GameListItemSkin(this); + } + + public ToggleGroup getToggleGroup() { + return toggleGroup; + } + + public Profile getProfile() { + return profile; + } + + public String getVersion() { + return version; + } + + public StringProperty titleProperty() { + return title; + } + + public StringProperty subtitleProperty() { + return subtitle; + } + + public BooleanProperty selectedProperty() { + return selected; + } + + public ObjectProperty imageProperty() { + return image; + } + + public void checkSelection() { + selected.set(version.equals(profile.getSelectedVersion())); + } + + public void rename() { + Versions.renameVersion(profile, version); + } + + public void remove() { + Versions.deleteVersion(profile, version); + } + + public void export() { + Versions.exportVersion(profile, version); + } + + public void browse() { + Versions.openFolder(profile, version); + } + + public void launch() { + Versions.launch(profile, version); + } + + public void modifyGameSettings() { + Controllers.getVersionPage().load(version, profile); + Controllers.navigate(Controllers.getVersionPage()); + } + + public void generateLaunchScript() { + Versions.generateLaunchScript(profile, version); + } + + public boolean canUpdate() { + return isModpack; + } + + public void update() { + Versions.updateVersion(profile, version); + } + + private static String modifyVersion(String gameVersion, String version) { + return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_"); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java new file mode 100644 index 000000000..4835e544d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java @@ -0,0 +1,134 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.concurrency.JFXUtilities; +import com.jfoenix.controls.*; +import com.jfoenix.effects.JFXDepthManager; +import javafx.geometry.Pos; +import javafx.scene.control.SkinBase; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class GameListItemSkin extends SkinBase { + + public GameListItemSkin(GameListItem skinnable) { + super(skinnable); + + BorderPane root = new BorderPane(); + + JFXRadioButton chkSelected = new JFXRadioButton(); + BorderPane.setAlignment(chkSelected, Pos.CENTER); + chkSelected.setUserData(skinnable); + chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty()); + chkSelected.setToggleGroup(skinnable.getToggleGroup()); + root.setLeft(chkSelected); + + HBox center = new HBox(); + center.setSpacing(8); + center.setAlignment(Pos.CENTER_LEFT); + + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + ImageView imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + imageView.imageProperty().bind(skinnable.imageProperty()); + imageViewContainer.getChildren().setAll(imageView); + + TwoLineListItem item = new TwoLineListItem(); + BorderPane.setAlignment(item, Pos.CENTER); + center.getChildren().setAll(imageView, item); + root.setCenter(center); + + JFXListView menu = new JFXListView<>(); + menu.getItems().setAll( + i18n("settings"), + i18n("version.manage.rename"), + i18n("version.manage.remove"), + i18n("modpack.export"), + i18n("folder.game"), + i18n("version.launch"), + i18n("version.launch_script")); + JFXPopup popup = new JFXPopup(menu); + menu.setOnMouseClicked(e -> { + popup.hide(); + switch (menu.getSelectionModel().getSelectedIndex()) { + case 0: + skinnable.modifyGameSettings(); + break; + case 1: + skinnable.rename(); + break; + case 2: + skinnable.remove(); + break; + case 3: + skinnable.export(); + break; + case 4: + skinnable.browse(); + break; + case 5: + skinnable.launch(); + break; + case 6: + skinnable.generateLaunchScript(); + break; + } + }); + + HBox right = new HBox(); + right.setAlignment(Pos.CENTER_RIGHT); + if (skinnable.canUpdate()) { + JFXButton btnUpgrade = new JFXButton(); + btnUpgrade.setOnMouseClicked(e -> skinnable.update()); + btnUpgrade.getStyleClass().add("toggle-icon4"); + btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1)); + JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnUpgrade, i18n("version.update"))); + right.getChildren().add(btnUpgrade); + } + + JFXButton btnManage = new JFXButton(); + btnManage.setOnMouseClicked(e -> { + menu.getSelectionModel().select(-1); + popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); + }); + btnManage.getStyleClass().add("toggle-icon4"); + BorderPane.setAlignment(btnManage, Pos.CENTER); + btnManage.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1)); + right.getChildren().add(btnManage); + root.setRight(right); + + root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;"); + JFXDepthManager.setDepth(root, 1); + item.titleProperty().bind(skinnable.titleProperty()); + item.subtitleProperty().bind(skinnable.subtitleProperty()); + + getChildren().setAll(root); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListSkin.java new file mode 100644 index 000000000..b7cf3bb13 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListSkin.java @@ -0,0 +1,120 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXScrollPane; +import com.jfoenix.controls.JFXSpinner; +import com.jfoenix.effects.JFXDepthManager; +import javafx.beans.binding.Bindings; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.util.i18n.I18n; + +public class GameListSkin extends SkinBase { + + private static Node wrap(Node node) { + StackPane stackPane = new StackPane(); + stackPane.setPadding(new Insets(0, 5, 0, 2)); + stackPane.getChildren().setAll(node); + return stackPane; + } + + public GameListSkin(GameList skinnable) { + super(skinnable); + + BorderPane root = new BorderPane(); + + { + HBox toolbar = new HBox(); + toolbar.getStyleClass().setAll("jfx-tool-bar-second"); + JFXDepthManager.setDepth(toolbar, 1); + toolbar.setPickOnBounds(false); + + JFXButton btnAddNewGame = new JFXButton(); + btnAddNewGame.getStyleClass().add("jfx-tool-bar-button"); + btnAddNewGame.textFillProperty().bind(Theme.foregroundFillBinding()); + btnAddNewGame.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1))); + btnAddNewGame.setText(I18n.i18n("install.new_game")); + btnAddNewGame.setOnMouseClicked(e -> skinnable.addNewGame()); + toolbar.getChildren().add(btnAddNewGame); + + JFXButton btnImportModpack = new JFXButton(); + btnImportModpack.getStyleClass().add("jfx-tool-bar-button"); + btnImportModpack.textFillProperty().bind(Theme.foregroundFillBinding()); + btnImportModpack.setGraphic(wrap(SVG.importIcon(Theme.foregroundFillBinding(), -1, -1))); + btnImportModpack.setText(I18n.i18n("install.modpack")); + btnImportModpack.setOnMouseClicked(e -> skinnable.importModpack()); + toolbar.getChildren().add(btnImportModpack); + + JFXButton btnRefresh = new JFXButton(); + btnRefresh.getStyleClass().add("jfx-tool-bar-button"); + btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding()); + btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1))); + btnRefresh.setText(I18n.i18n("button.refresh")); + btnRefresh.setOnMouseClicked(e -> skinnable.refresh()); + toolbar.getChildren().add(btnRefresh); + + JFXButton btnModify = new JFXButton(); + btnModify.getStyleClass().add("jfx-tool-bar-button"); + btnModify.textFillProperty().bind(Theme.foregroundFillBinding()); + btnModify.setGraphic(wrap(SVG.gear(Theme.foregroundFillBinding(), -1, -1))); + btnModify.setText(I18n.i18n("settings.type.global.manage")); + btnModify.setOnMouseClicked(e -> skinnable.modifyGlobalGameSettings()); + toolbar.getChildren().add(btnModify); + + root.setTop(toolbar); + } + + { + StackPane center = new StackPane(); + + JFXSpinner spinner = new JFXSpinner(); + spinner.getStyleClass().setAll("first-spinner"); + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + + VBox gameList = new VBox(); + gameList.maxWidthProperty().bind(scrollPane.widthProperty()); + gameList.setSpacing(10); + gameList.setStyle("-fx-padding: 10 10 10 10;"); + + Bindings.bindContent(gameList.getChildren(), skinnable.itemsProperty()); + + scrollPane.setContent(gameList); + JFXScrollPane.smoothScrolling(scrollPane); + + FXUtils.onChangeAndOperate(skinnable.loadingProperty(), + loading -> center.getChildren().setAll(loading ? spinner : scrollPane)); + + root.setCenter(center); + } + + getChildren().setAll(root); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerController.java similarity index 95% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerController.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerController.java index de0082e72..e4eae6aed 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerController.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.versions; import javafx.fxml.FXML; import javafx.scene.control.ScrollPane; @@ -30,6 +30,9 @@ import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.InstallerItem; import org.jackhuang.hmcl.ui.download.InstallerWizardProvider; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModController.java similarity index 97% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModController.java index 88d1255a7..5f8425bba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ModController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModController.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.versions; import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.controls.JFXSpinner; @@ -31,6 +31,9 @@ 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.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.ModItem; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.Logging; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java similarity index 70% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index ee14e1281..b8741055b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXListView; @@ -27,10 +27,9 @@ import javafx.fxml.FXML; import javafx.scene.control.Tab; import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask; -import org.jackhuang.hmcl.setting.EnumGameDirectory; import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.ui.export.ExportWizardProvider; -import org.jackhuang.hmcl.ui.wizard.DecoratorPage; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.FileUtils; import java.io.File; @@ -41,7 +40,7 @@ public final class VersionPage extends StackPane implements DecoratorPage { private final StringProperty title = new SimpleStringProperty(this, "title", null); @FXML - private VersionSettingsController versionSettingsController; + private VersionSettingsPage versionSettings; @FXML private Tab modTab; @FXML @@ -93,7 +92,7 @@ public final class VersionPage extends StackPane implements DecoratorPage { title.set(i18n("settings.game") + " - " + id); - versionSettingsController.loadVersionSetting(profile, id); + versionSettings.loadVersionSetting(profile, id); modController.setParentTab(tabPane); modTab.setUserData(modController); modController.loadMods(profile.getModManager(), id); @@ -112,16 +111,6 @@ public final class VersionPage extends StackPane implements DecoratorPage { managementPopup.show(btnManagementMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12, 15); } - @FXML - private void onDelete() { - deleteVersion(profile, version); - } - - @FXML - private void onExport() { - exportVersion(profile, version); - } - @FXML private void onBrowse() { String sub; @@ -157,10 +146,10 @@ public final class VersionPage extends StackPane implements DecoratorPage { private void onManagement() { switch (managementList.getSelectionModel().getSelectedIndex()) { case 0: // rename a version - renameVersion(profile, version); + Versions.renameVersion(profile, version); break; case 1: // remove a version - deleteVersion(profile, version); + Versions.deleteVersion(profile, version); break; case 2: // redownload asset index new GameAssetIndexDownloadTask(profile.getDependency(), profile.getRepository().getResolvedVersion(version)).start(); @@ -185,34 +174,4 @@ public final class VersionPage extends StackPane implements DecoratorPage { public void setTitle(String title) { this.title.set(title); } - - public static void deleteVersion(Profile profile, String version) { - boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == EnumGameDirectory.VERSION_FOLDER; - boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported(); - String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) : - isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") : - i18n("version.manage.remove.confirm", version); - Controllers.confirmDialog(message, i18n("message.confirm"), () -> { - if (profile.getRepository().removeVersionFromDisk(version)) { - profile.getRepository().refreshVersionsAsync().start(); - Controllers.navigate(null); - } - }, null); - } - - public static void renameVersion(Profile profile, String version) { - Controllers.inputDialog(i18n("version.manage.rename.message"), (res, resolve, reject) -> { - if (profile.getRepository().renameVersion(version, res)) { - profile.getRepository().refreshVersionsAsync().start(); - Controllers.navigate(null); - resolve.run(); - } else { - reject.accept(i18n("version.manage.rename.fail")); - } - }).setInitialText(version); - } - - public static void exportVersion(Profile profile, String version) { - Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), i18n("modpack.wizard")); - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionSettingsController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java similarity index 80% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionSettingsController.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index aea799b94..582c5d360 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/VersionSettingsController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -15,19 +15,20 @@ * 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; +package org.jackhuang.hmcl.ui.versions; -import com.jfoenix.controls.JFXCheckBox; -import com.jfoenix.controls.JFXComboBox; -import com.jfoenix.controls.JFXTextField; -import com.jfoenix.controls.JFXToggleButton; +import com.jfoenix.controls.*; import javafx.application.Platform; +import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.control.Toggle; import javafx.scene.image.Image; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; @@ -36,9 +37,13 @@ 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.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ImagePickerItem; import org.jackhuang.hmcl.ui.construct.MultiFileItem; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.util.*; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -50,11 +55,14 @@ import java.util.List; import java.util.logging.Level; import java.util.stream.Collectors; -public final class VersionSettingsController { +public final class VersionSettingsPage extends StackPane implements DecoratorPage { + private final StringProperty title = new SimpleStringProperty(); + private VersionSetting lastVersionSetting = null; private Profile profile; private String versionId; private boolean javaItemsLoaded; + private InvalidationListener specificSettingsListener; @FXML private VBox rootPane; @FXML private ScrollPane scroll; @@ -68,16 +76,22 @@ public final class VersionSettingsController { @FXML private JFXTextField txtPrecallingCommand; @FXML private JFXTextField txtServerIP; @FXML private ComponentList advancedSettingsPane; + @FXML private ComponentList componentList; @FXML private JFXComboBox cboLauncherVisibility; @FXML private JFXCheckBox chkFullscreen; @FXML private Label lblPhysicalMemory; @FXML private JFXToggleButton chkNoJVMArgs; @FXML private JFXToggleButton chkNoGameCheck; - @FXML private MultiFileItem globalItem; @FXML private MultiFileItem javaItem; @FXML private MultiFileItem gameDirItem; @FXML private JFXToggleButton chkShowLogs; @FXML private ImagePickerItem iconPickerItem; + @FXML private JFXCheckBox chkEnableSpecificSettings; + @FXML private BorderPane settingsTypePane; + + public VersionSettingsPage() { + FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml"); + } @FXML private void initialize() { @@ -106,20 +120,42 @@ public final class VersionSettingsController { gameDirItem.createChildren(i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER) )); - globalItem.loadChildren(Arrays.asList( - globalItem.createChildren(i18n("settings.type.global"), true), - globalItem.createChildren(i18n("settings.type.special"), false) - )); + chkEnableSpecificSettings.selectedProperty().addListener((a, b, newValue) -> { + if (versionId == null) return; + + // do not call versionSettings.setUsesGlobal(true/false) + // because versionSettings can be the global one. + // global versionSettings.usesGlobal is always true. + if (newValue) + profile.specializeVersionSetting(versionId); + else + profile.globalizeVersionSetting(versionId); + + Platform.runLater(() -> loadVersionSetting(profile, versionId)); + }); + + specificSettingsListener = o -> { + chkEnableSpecificSettings.setSelected(!lastVersionSetting.isUsesGlobal()); + }; + + componentList.disableProperty().bind(chkEnableSpecificSettings.selectedProperty().not()); + advancedSettingsPane.disableProperty().bind(chkEnableSpecificSettings.selectedProperty().not()); } public void loadVersionSetting(Profile profile, String versionId) { this.profile = profile; this.versionId = versionId; + if (versionId == null) { + componentList.removeChild(iconPickerItem); + rootPane.getChildren().remove(settingsTypePane); + chkEnableSpecificSettings.setSelected(true); + } + VersionSetting versionSetting = profile.getVersionSetting(versionId); - gameDirItem.setDisable(profile.getRepository().isModpack(versionId)); - globalItem.setDisable(profile.getRepository().isModpack(versionId)); + gameDirItem.setDisable(versionId != null && profile.getRepository().isModpack(versionId)); + settingsTypePane.setDisable(versionId != null && profile.getRepository().isModpack(versionId)); // unbind data fields if (lastVersionSetting != null) { @@ -140,14 +176,13 @@ public final class VersionSettingsController { FXUtils.unbindBoolean(chkShowLogs, lastVersionSetting.showLogsProperty()); FXUtils.unbindEnum(cboLauncherVisibility); - globalItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.usesGlobalProperty()); + lastVersionSetting.usesGlobalProperty().removeListener(specificSettingsListener); gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty()); gameDirItem.subtitleProperty().unbind(); } // unbind data fields - globalItem.setToggleSelectedListener(null); javaItem.setToggleSelectedListener(null); // bind new data fields @@ -168,6 +203,10 @@ public final class VersionSettingsController { FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty()); FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty()); + versionSetting.usesGlobalProperty().addListener(specificSettingsListener); + if (versionId != null) + chkEnableSpecificSettings.setSelected(!versionSetting.isUsesGlobal()); + javaItem.setToggleSelectedListener(newValue -> { if (javaItem.isCustomToggle(newValue)) { versionSetting.setUsesCustomJavaDir(); @@ -180,21 +219,6 @@ public final class VersionSettingsController { versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting)); initJavaSubtitle(versionSetting); - globalItem.selectedDataProperty().bindBidirectional(versionSetting.usesGlobalProperty()); - globalItem.subtitleProperty().bind(Bindings.createStringBinding(() -> i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special"), - versionSetting.usesGlobalProperty())); - globalItem.setToggleSelectedListener(newValue -> { - // do not call versionSettings.setUsesGlobal(true/false) - // because versionSettings can be the global one. - // global versionSettings.usesGlobal is always true. - if ((Boolean) newValue.getUserData()) - profile.globalizeVersionSetting(versionId); - else - profile.specializeVersionSetting(versionId); - - Platform.runLater(() -> loadVersionSetting(profile, versionId)); - }); - gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty()); gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(), versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty())); @@ -230,8 +254,16 @@ public final class VersionSettingsController { .map(JavaVersion::getBinary).map(File::getAbsolutePath).orElse("Invalid Java Directory")))); } + @FXML + private void editGlobalSettings() { + Versions.modifyGlobalSettings(profile); + } + @FXML private void onExploreIcon() { + if (versionId == null) + return; + FileChooser chooser = new FileChooser(); chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png")); File selectedFile = chooser.showOpenDialog(Controllers.getStage()); @@ -247,11 +279,21 @@ public final class VersionSettingsController { } private void loadIcon() { + if (versionId == null) { + iconPickerItem.setImage(new Image("/assets/img/grass.png")); + return; + } + File iconFile = profile.getRepository().getVersionIcon(versionId); if (iconFile.exists()) iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath())); else - iconPickerItem.setImage(Constants.DEFAULT_ICON.get()); + iconPickerItem.setImage(new Image("/assets/img/grass.png")); FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32); } + + @Override + public StringProperty titleProperty() { + return title; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java new file mode 100644 index 000000000..3317e6ce0 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -0,0 +1,141 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.ui.versions; + +import javafx.scene.layout.Region; +import javafx.stage.FileChooser; +import org.jackhuang.hmcl.game.GameRepository; +import org.jackhuang.hmcl.game.LauncherHelper; +import org.jackhuang.hmcl.game.ModpackHelper; +import org.jackhuang.hmcl.mod.MismatchedModpackTypeException; +import org.jackhuang.hmcl.mod.UnsupportedModpackException; +import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.setting.EnumGameDirectory; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.MessageBox; +import org.jackhuang.hmcl.ui.export.ExportWizardProvider; +import org.jackhuang.hmcl.util.FileUtils; +import org.jackhuang.hmcl.util.OperatingSystem; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class Versions { + + public static void deleteVersion(Profile profile, String version) { + boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == EnumGameDirectory.VERSION_FOLDER; + boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported(); + String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) : + isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") : + i18n("version.manage.remove.confirm", version); + Controllers.confirmDialog(message, i18n("message.confirm"), () -> { + if (profile.getRepository().removeVersionFromDisk(version)) { + profile.getRepository().refreshVersionsAsync().start(); + Controllers.navigate(null); + } + }, null); + } + + public static void renameVersion(Profile profile, String version) { + Controllers.inputDialog(i18n("version.manage.rename.message"), (res, resolve, reject) -> { + if (profile.getRepository().renameVersion(version, res)) { + profile.getRepository().refreshVersionsAsync().start(); + Controllers.navigate(null); + resolve.run(); + } else { + reject.accept(i18n("version.manage.rename.fail")); + } + }).setInitialText(version); + } + + public static void exportVersion(Profile profile, String version) { + Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), i18n("modpack.wizard")); + } + + public static void openFolder(Profile profile, String version) { + FXUtils.openFolder(profile.getRepository().getRunDirectory(version)); + } + + public static void updateVersion(Profile profile, String version) { + FileChooser chooser = new FileChooser(); + chooser.setTitle(i18n("modpack.choose")); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip")); + File selectedFile = chooser.showOpenDialog(Controllers.getStage()); + if (selectedFile != null) { + AtomicReference region = new AtomicReference<>(); + try { + TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, version, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(version))) + .then(Task.of(Schedulers.javafx(), () -> region.get().fireEvent(new DialogCloseEvent()))).executor(); + region.set(Controllers.taskDialog(executor, i18n("modpack.update"), "")); + executor.start(); + } catch (UnsupportedModpackException e) { + region.get().fireEvent(new DialogCloseEvent()); + Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageBox.ERROR_MESSAGE); + } catch (MismatchedModpackTypeException e) { + region.get().fireEvent(new DialogCloseEvent()); + Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageBox.ERROR_MESSAGE); + } catch (IOException e) { + region.get().fireEvent(new DialogCloseEvent()); + Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE); + } + } + } + + public static void generateLaunchScript(Profile profile, String id) { + GameRepository repository = profile.getRepository(); + + if (Accounts.getSelectedAccount() == null) + Controllers.dialog(i18n("login.empty_username")); + else { + FileChooser chooser = new FileChooser(); + if (repository.getRunDirectory(id).isDirectory()) + chooser.setInitialDirectory(repository.getRunDirectory(id)); + chooser.setTitle(i18n("version.launch_script.save")); + chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS + ? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat") + : new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh")); + File file = chooser.showSaveDialog(Controllers.getStage()); + if (file != null) + LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file); + } + } + + public static void launch(Profile profile, String id) { + if (Accounts.getSelectedAccount() == null) + Controllers.getLeftPaneController().checkAccount(); + else + LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null); + } + + public static void modifyGlobalSettings(Profile profile) { + VersionSettingsPage page = new VersionSettingsPage(); + page.loadVersionSetting(profile, null); + page.titleProperty().set(Profiles.getProfileDisplayName(profile) + " - " + i18n("settings.type.global.manage")); + Controllers.navigate(page); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java index 0c8fd8320..e4a70aa0b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Refreshable.java @@ -17,6 +17,13 @@ */ package org.jackhuang.hmcl.ui.wizard; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + public interface Refreshable { void refresh(); + + default BooleanProperty canRefreshProperty() { + return new SimpleBooleanProperty(false); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java similarity index 100% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogWizardDisplayer.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index c3e29e931..486a6deaa 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -52,7 +52,7 @@ } .advanced-list-box-item { - -fx-padding: 10 16 10 16; + -fx-padding: 0 16 0 16; } .advanced-list-box-content { @@ -294,9 +294,39 @@ -jfx-rippler-fill: WHITE; } +.jfx-tool-bar-second { + -fx-padding: 2 2 2 2; + -fx-background-color: -fx-base-check-color; +} + +.jfx-tool-bar-second .jfx-rippler { + -jfx-rippler-fill: WHITE; +} + +.jfx-tool-bar-button { + -fx-toggle-icon4-size: 35px; + -fx-pref-height: -fx-toggle-icon4-size; + -fx-max-height: -fx-toggle-icon4-size; + -fx-min-height: -fx-toggle-icon4-size; + -fx-background-radius: 5px; + -fx-background-color: transparent; + -jfx-toggle-color: white; + -jfx-untoggle-color: transparent; +} + +.jfx-tool-bar-button .icon { + -fx-fill: rgb(204.0, 204.0, 51.0); + -fx-padding: 10.0; +} + +.jfx-tool-bar-button .jfx-rippler { + -jfx-rippler-fill: white; + -jfx-mask-type: CIRCLE; +} + .jfx-decorator-button { -fx-max-width: 35px; - -fx-background-radius: 40px; + -fx-background-radius: 5px; -fx-max-height: 35px; -fx-background-color: transparent; -jfx-toggle-color: rgba(128, 128, 255, 0.2); @@ -977,6 +1007,15 @@ -jfx-focus-color: -fx-base-check-color; } +.jfx-combo-box-warning { + -jfx-focus-color: #D34336; + -jfx-unfocus-color: #D34336; +} + +.jfx-combo-box-warning .text { + -fx-fill: #D34336; +} + .combo-box-popup .list-view .jfx-list-cell .label, .combo-box-popup .list-view .jfx-list-cell:filled:hover .label { -fx-text-fill: BLACK; @@ -1023,6 +1062,10 @@ .jfx-decorator-drawer { } +.jfx-decorator-title { + -fx-text-fill: -fx-base-text-fill; -fx-font-size: 15; +} + .resize-border { -fx-border-color: -fx-base-color; -fx-border-width: 0 2 2 2; diff --git a/HMCL/src/main/resources/assets/fxml/decorator.fxml b/HMCL/src/main/resources/assets/fxml/decorator.fxml deleted file mode 100644 index fa26b7b10..000000000 --- a/HMCL/src/main/resources/assets/fxml/decorator.fxml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - -
- - - - - -
- - - -
- - - -
-
-
-
- - - - - -
-
- - - -
-
- - - - -
- -
- - - -
-
-
- - - - - - - -
- - -
- - - - - -
-
- - - - - - - - - - - - - - -
-
-
-
\ No newline at end of file diff --git a/HMCL/src/main/resources/assets/fxml/download/dltype.fxml b/HMCL/src/main/resources/assets/fxml/download/dltype.fxml deleted file mode 100644 index c558bc756..000000000 --- a/HMCL/src/main/resources/assets/fxml/download/dltype.fxml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HMCL/src/main/resources/assets/fxml/main.fxml b/HMCL/src/main/resources/assets/fxml/main.fxml index 11835a8fd..d5b4908a9 100644 --- a/HMCL/src/main/resources/assets/fxml/main.fxml +++ b/HMCL/src/main/resources/assets/fxml/main.fxml @@ -1,32 +1,12 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/HMCL/src/main/resources/assets/fxml/version/installer.fxml b/HMCL/src/main/resources/assets/fxml/version/installer.fxml index 02df43515..e29ff41fa 100644 --- a/HMCL/src/main/resources/assets/fxml/version/installer.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/installer.fxml @@ -6,7 +6,7 @@ + fx:controller="org.jackhuang.hmcl.ui.versions.InstallerController"> diff --git a/HMCL/src/main/resources/assets/fxml/version/mod.fxml b/HMCL/src/main/resources/assets/fxml/version/mod.fxml index bfc7cc8b0..aaa8d0828 100644 --- a/HMCL/src/main/resources/assets/fxml/version/mod.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/mod.fxml @@ -8,7 +8,7 @@ + fx:controller="org.jackhuang.hmcl.ui.versions.ModController"> diff --git a/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml b/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml index 01479693b..6591599fd 100644 --- a/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml @@ -8,15 +8,24 @@ - + - + - + + + + + + + + - + @@ -25,7 +34,7 @@ - @@ -100,11 +109,11 @@ - + @@ -150,4 +159,4 @@ - + diff --git a/HMCL/src/main/resources/assets/fxml/version/version.fxml b/HMCL/src/main/resources/assets/fxml/version/version.fxml index cbb876f00..bddb50096 100644 --- a/HMCL/src/main/resources/assets/fxml/version/version.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/version.fxml @@ -4,6 +4,7 @@ + - + @@ -23,18 +24,6 @@ - - - - - - - - - - diff --git a/HMCL/src/main/resources/assets/img/craft_table.png b/HMCL/src/main/resources/assets/img/craft_table.png new file mode 100644 index 000000000..45740510a Binary files /dev/null and b/HMCL/src/main/resources/assets/img/craft_table.png differ diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 1709e9db3..38c50db4f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -48,11 +48,13 @@ account.injector.http=Warning: This server is using HTTP, which will cause your account.injector.server=Auth Server account.injector.server_url=Server URL account.injector.server_name=Server Name +account.manage=Account List account.methods=Login Type account.methods.authlib_injector=authlib-injector account.methods.offline=Offline account.methods.yggdrasil=Mojang -account.missing=None +account.missing=No Account +account.missing.add=Click the button on the right to add account.password=Password account.username=Name @@ -238,12 +240,12 @@ mods.add.success=Successfully added mods %s. mods.choose_mod=Choose your mods mods.remove=Remove - -profile=Profile +profile=Game Directories profile.default=Current directory profile.home=User home profile.instance_directory=Game Directory profile.instance_directory.choose=Choose Game Directory +profile.manage=Game Directory List profile.new=New Config profile.title=Game Directories profile.selected=Selected @@ -253,7 +255,7 @@ selector.choose=Choose selector.choose_file=Select a file selector.custom=Custom -settings=Settings +settings=Game Settings settings.advanced=Advanced Settings settings.advanced.dont_check_game_completeness=Don't check game completeness @@ -282,15 +284,16 @@ settings.game.exploration=Explore settings.game.fullscreen=Fullscreen settings.game.java_directory=Java Directory settings.game.java_directory.choose=Choose Java Directory. -settings.game.run_directory=Run Directory -settings.game.run_directory.choose=Choose Run Directory settings.game.management=Manage +settings.game.working_directory=Working Directory +settings.game.working_directory.choose=Choose Working Directory settings.icon=Game Icon settings.launcher=Settings settings.launcher.common_path.tooltip=This app will save all game libraries and assets here unless there are existant files in game folder. settings.launcher.download_source=Download Source +settings.launcher.enable_game_list=Display game list in main page settings.launcher.language=Language settings.launcher.log_font=Log Font settings.launcher.proxy=Proxy @@ -308,9 +311,11 @@ settings.max_memory=Max Memory/MB settings.physical_memory=Physical Memory Size settings.show_log=Show Logs settings.tabs.installers=Installers -settings.type.global=Global version settings(all shared) settings.type=Version setting type -settings.type.special=Specialized version settings(will not affect other versions) +settings.type.global=Global global settings(all shared) +settings.type.global.manage=Global Game Settings +settings.type.global.edit=Configure global game settings +settings.type.special.enable=Enable specialized settings for this game update=Update update.channel.dev=Update to development version @@ -324,6 +329,7 @@ update.latest=This is latest Version. update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update. update.tooltip=Update +version=Games version.cannot_read=Unable to gather the game version. Cannot continue auto-installing. version.forbidden_name=Forbidden name, do not use this. version.game.old=Old @@ -334,6 +340,7 @@ version.launch_script=Make Launching Script version.launch_script.failed=Unable to make launch script. version.launch_script.save=Save the launch script version.launch_script.success=Finished script creation, %s. +version.manage=Game List version.manage.redownload_assets_index=Redownload Assets Index version.manage.remove=Delete this game version.manage.remove.confirm=Sure to remove game %s? You cannot restore this game again! @@ -344,7 +351,7 @@ version.manage.rename=Rename this game version.manage.rename.message=Please enter the new name version.manage.rename.fail=Failed to rename this game. version.settings=Settings -version.update=Update +version.update=Update modpack wizard.prev=< Prev wizard.failed=Failed diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 1695d20b1..3f6a216b7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -48,11 +48,13 @@ account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您 account.injector.server=認證伺服器 account.injector.server_url=伺服器位址 account.injector.server_name=伺服器名稱 +account.manage=帳戶列表 account.methods=登入方式 account.methods.authlib_injector=authlib-injector 登入 account.methods.offline=離線模式 account.methods.yggdrasil=正版登入 -account.missing=沒有帳戶 +account.missing=沒有遊戲帳戶 +account.missing.add=點擊右邊按鈕添加 account.password=密碼 account.username=使用者名稱 @@ -238,12 +240,12 @@ mods.add.success=成功新增模組 %s。 mods.choose_mod=選擇模組 mods.remove=刪除 - -profile=設定 +profile=遊戲目錄 profile.default=目前目錄 profile.home=主資料夾 profile.instance_directory=遊戲路徑 profile.instance_directory.choose=選擇遊戲路徑 +profile.manage=遊戲目錄列表 profile.new=建立設定 profile.title=遊戲目錄 profile.selected=已選取 @@ -253,7 +255,7 @@ selector.choose=選擇 selector.choose_file=選擇檔案 selector.custom=自訂 -settings=普通設定 +settings=遊戲設定 settings.advanced=進階設定 settings.advanced.dont_check_game_completeness=不檢查遊戲完整性 @@ -282,15 +284,16 @@ settings.game.exploration=瀏覽 settings.game.fullscreen=全螢幕 settings.game.java_directory=Java 路徑 settings.game.java_directory.choose=選擇 Java 路徑 -settings.game.run_directory=運行路徑(版本隔離) -settings.game.run_directory.choose=選擇運行路徑 settings.game.management=管理 +settings.game.working_directory=運行路徑(版本隔離) +settings.game.working_directory.choose=選擇運行路徑 settings.icon=遊戲圖示 settings.launcher=啟動器設定 settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及依賴庫檔案放於此集中管理,如果遊戲資料夾內有現成的將不會使用公共庫檔案 settings.launcher.download_source=下載來源 +settings.launcher.enable_game_list=在首頁內顯示遊戲列表 settings.launcher.language=語言 settings.launcher.log_font=記錄字體 settings.launcher.proxy=代理 @@ -308,9 +311,11 @@ settings.max_memory=最大記憶體(MB) settings.physical_memory=實體記憶體大小 settings.show_log=查看記錄 settings.tabs.installers=自動安裝 -settings.type.global=全域版本設定(使用該設定的版本共用一套設定) settings.type=版本設定類型 -settings.type.special=單獨版本設定(不會影響到其他版本的設定) +settings.type.global=全域版本設定(使用該設定的版本共用一套設定) +settings.type.global.manage=全域遊戲設定 +settings.type.global.edit=編輯全域遊戲設定 +settings.type.special.enable=啟用遊戲特別設定(不影響其他遊戲版本) update=啟動器更新 update.channel.dev=更新到開發版 @@ -324,6 +329,7 @@ update.latest=目前版本為最新版本 update.no_browser=無法打開瀏覽器,網址已經複製到剪貼簿了,您可以手動複製網址打開頁面 update.tooltip=更新 +version=遊戲 version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝 version.forbidden_name=此版本名稱不受支援,請換一個名字 version.game.old=老舊版本 @@ -334,6 +340,7 @@ version.launch_script=生成啟動腳本 version.launch_script.failed=生成啟動腳本失敗 version.launch_script.save=儲存啟動腳本 version.launch_script.success=啟動腳本已生成完畢:%s +version.manage=遊戲列表 version.manage.redownload_assets_index=重新下載資源設定(assets_index.json) version.manage.remove=刪除該版本 version.manage.remove.confirm=真的要刪除版本 %s 嗎?你將無法找回被刪除的檔案! @@ -344,7 +351,7 @@ version.manage.rename=重新命名該版本 version.manage.rename.message=請輸入新名稱 version.manage.rename.fail=重新命名版本失敗,可能檔案被佔用或者名字有特殊字元 version.settings=遊戲設定 -version.update=更新 +version.update=更新整合包 wizard.prev=< 上一步 wizard.failed=失敗 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 08cec0547..830a4ecc1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -48,11 +48,13 @@ account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您 account.injector.server=认证服务器 account.injector.server_url=服务器地址 account.injector.server_name=服务器名称 +account.manage=账户列表 account.methods=登录方式 account.methods.authlib_injector=authlib-injector 登录 account.methods.offline=离线模式 account.methods.yggdrasil=正版登录 -account.missing=没有账户 +account.missing=没有游戏账户 +account.missing.add=点击右边按钮添加 account.password=密码 account.username=用户名 @@ -238,12 +240,12 @@ mods.add.success=成功添加模组 %s。 mods.choose_mod=选择模组 mods.remove=删除 - -profile=配置 +profile=游戏目录 profile.default=当前目录 profile.home=主文件夹 profile.instance_directory=游戏路径 profile.instance_directory.choose=选择游戏路径 +profile.manage=游戏目录列表 profile.new=新建配置 profile.title=游戏目录 profile.selected=已选中 @@ -253,7 +255,7 @@ selector.choose=选择 selector.choose_file=选择文件 selector.custom=自定义 -settings=普通设置 +settings=游戏设置 settings.advanced=高级设置 settings.advanced.dont_check_game_completeness=不检查游戏完整性 @@ -282,15 +284,16 @@ settings.game.exploration=浏览 settings.game.fullscreen=全屏 settings.game.java_directory=Java 路径 settings.game.java_directory.choose=选择 Java 路径 -settings.game.run_directory=运行路径(版本隔离) -settings.game.run_directory.choose=选择运行路径 settings.game.management=管理 +settings.game.working_directory=运行路径(版本隔离) +settings.game.working_directory.choose=选择运行路径 settings.icon=游戏图标 settings.launcher=启动器设置 settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件放于此集中管理,如果游戏文件夹内有现成的将不会使用公共库文件 settings.launcher.download_source=下载源 +settings.launcher.enable_game_list=在主页内显示游戏列表 settings.launcher.language=语言 settings.launcher.log_font=日志字体 settings.launcher.proxy=代理 @@ -308,9 +311,11 @@ settings.max_memory=最大内存(MB) settings.physical_memory=物理内存大小 settings.show_log=查看日志 settings.tabs.installers=自动安装 -settings.type.global=全局版本设置(使用该设置的版本共用一套设定) settings.type=版本设置类型 -settings.type.special=单独版本设置(不会影响到其他版本的设定) +settings.type.global=全局版本设置(使用该设置的版本共用一套设定) +settings.type.global.manage=全局游戏设置 +settings.type.global.edit=编辑全局版本设置 +settings.type.special.enable=启用游戏特定设置(不影响其他游戏版本) update=启动器更新 update.channel.dev=更新到开发版 @@ -324,6 +329,7 @@ update.latest=当前版本为最新版本 update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面 update.tooltip=更新 +version=游戏 version.cannot_read=读取游戏版本失败,无法进行自动安装 version.forbidden_name=此版本名称不受支持,请换一个名字 version.game.old=远古版 @@ -334,6 +340,7 @@ version.launch_script=生成启动脚本 version.launch_script.failed=生成启动脚本失败 version.launch_script.save=保存启动脚本 version.launch_script.success=启动脚本已生成完毕:%s +version.manage=游戏列表 version.manage.redownload_assets_index=重新下载资源配置(assets_index.json) version.manage.remove=删除该版本 version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件! @@ -344,7 +351,7 @@ version.manage.rename=重命名该版本 version.manage.rename.message=请输入要改成的名字 version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符 version.settings=游戏设置 -version.update=更新 +version.update=更新整合包 wizard.prev=< 上一步 wizard.failed=失败 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index 585f45e80..f045c1251 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -54,7 +54,7 @@ public abstract class Account implements Observable { * * @throws CredentialExpiredException when the stored credentials has expired, in which case a password login will be performed */ - public abstract AuthInfo logIn() throws CredentialExpiredException, AuthenticationException; + public abstract AuthInfo logIn() throws AuthenticationException; /** * Login with specified password. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountBuilder.java index 2e2ecfd93..2b865e409 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountBuilder.java @@ -28,22 +28,22 @@ public final class AccountBuilder { public AccountBuilder() { } - public AccountBuilder setSelector(CharacterSelector selector) { + public AccountBuilder setSelector(CharacterSelector selector) { this.selector = Objects.requireNonNull(selector); return this; } - public AccountBuilder setUsername(String username) { + public AccountBuilder setUsername(String username) { this.username = Objects.requireNonNull(username); return this; } - public AccountBuilder setPassword(String password) { + public AccountBuilder setPassword(String password) { this.password = password; return this; } - public AccountBuilder setAdditionalData(Object additionalData) { + public AccountBuilder setAdditionalData(Object additionalData) { this.additionalData = additionalData; return this; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 08559af84..b59b444a1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -53,11 +53,10 @@ public class DefaultGameBuilder extends GameBuilder { Version version = Constants.GSON.fromJson(variables.get(VersionJsonDownloadTask.ID), Version.class); version = version.setId(name).setJar(null); variables.set("version", version); - Task result = new ParallelTask( + Task result = downloadGameAsync(gameVersion, version).then(new ParallelTask( new GameAssetDownloadTask(dependencyManager, version), - downloadGameAsync(gameVersion, version), new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries. - ).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version)); // using [with] because download failure here are tolerant. + ).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version))); // using [with] because download failure here are tolerant. if (toolVersions.containsKey("forge")) result = result.then(libraryTaskHelper(gameVersion, "forge")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java index d8940ca0e..f43b482ce 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java @@ -28,16 +28,12 @@ import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.NetworkUtils; import java.io.File; -import java.io.IOException; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; -import static org.jackhuang.hmcl.util.DigestUtils.digest; -import static org.jackhuang.hmcl.util.Hex.encodeHex; - /** * * @author huangyuhui diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java index ced1afb49..97c4b51f5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.download.game; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.VersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java index 9c4c4d868..7cb061812 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java @@ -14,8 +14,6 @@ import org.tukaani.xz.XZInputStream; import java.io.*; import java.net.URL; import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java index c66764c93..2bb4830b5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.event; import org.jackhuang.hmcl.util.SimpleMultimap; +import java.lang.ref.WeakReference; import java.util.EnumMap; import java.util.HashSet; import java.util.function.Consumer; @@ -31,8 +32,16 @@ public final class EventManager { private final SimpleMultimap> handlers = new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new); - private final SimpleMultimap handlers2 - = new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new); + + public Consumer registerWeak(Consumer consumer) { + register(new WeakListener(consumer)); + return consumer; + } + + public Consumer registerWeak(Consumer consumer, EventPriority priority) { + register(new WeakListener(consumer), priority); + return consumer; + } public void register(Consumer consumer) { register(consumer, EventPriority.NORMAL); @@ -44,28 +53,17 @@ public final class EventManager { } public void register(Runnable runnable) { - register(runnable, EventPriority.NORMAL); + register(t -> runnable.run()); } public void register(Runnable runnable, EventPriority priority) { - if (!handlers2.get(priority).contains(runnable)) - handlers2.put(priority, runnable); - } - - public void unregister(Consumer consumer) { - handlers.removeValue(consumer); - } - - public void unregister(Runnable runnable) { - handlers2.removeValue(runnable); + register(t -> runnable.run(), priority); } public Event.Result fireEvent(T event) { for (EventPriority priority : EventPriority.values()) { for (Consumer handler : handlers.get(priority)) handler.accept(event); - for (Runnable runnable : handlers2.get(priority)) - runnable.run(); } if (event.hasResult()) @@ -74,4 +72,21 @@ public final class EventManager { return Event.Result.DEFAULT; } + private class WeakListener implements Consumer { + private final WeakReference> ref; + + public WeakListener(Consumer listener) { + this.ref = new WeakReference<>(listener); + } + + @Override + public void accept(T t) { + Consumer listener = ref.get(); + if (listener == null) { + handlers.removeValue(this); + } else { + listener.accept(t); + } + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java index 36c70856f..7786c13d5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/ProcessStoppedEvent.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.event; import org.jackhuang.hmcl.util.ManagedProcess; -import org.jackhuang.hmcl.util.ToStringBuilder; /** * This event gets fired when minecraft process exited successfully and the exit code is 0. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java index e3b0e64f4..e898be8ff 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java @@ -47,7 +47,7 @@ public final class LibrariesDownloadInfo { } public Map getClassifiers() { - return classifiers == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(classifiers); + return classifiers == null ? Collections.emptyMap() : Collections.unmodifiableMap(classifiers); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java index c1af9e157..cd62e2978 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java @@ -116,15 +116,15 @@ public class Version implements Comparable, Validation { } public Map getLogging() { - return logging == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(logging); + return logging == null ? Collections.emptyMap() : Collections.unmodifiableMap(logging); } public List getLibraries() { - return libraries == null ? Collections.EMPTY_LIST : Collections.unmodifiableList(libraries); + return libraries == null ? Collections.emptyList() : Collections.unmodifiableList(libraries); } public List getCompatibilityRules() { - return compatibilityRules == null ? Collections.EMPTY_LIST : Collections.unmodifiableList(compatibilityRules); + return compatibilityRules == null ? Collections.emptyList() : Collections.unmodifiableList(compatibilityRules); } public DownloadInfo getDownloadInfo() { @@ -247,14 +247,14 @@ public class Version implements Comparable, Validation { if (StringUtils.isBlank(id)) throw new JsonParseException("Version ID cannot be blank"); if (downloads != null) - for (Map.Entry entry : downloads.entrySet()) { + for (Map.Entry entry : downloads.entrySet()) { if (!(entry.getKey() instanceof DownloadType)) throw new JsonParseException("Version downloads key must be DownloadType"); if (!(entry.getValue() instanceof DownloadInfo)) throw new JsonParseException("Version downloads value must be DownloadInfo"); } if (logging != null) - for (Map.Entry entry : logging.entrySet()) { + for (Map.Entry entry : logging.entrySet()) { if (!(entry.getKey() instanceof DownloadType)) throw new JsonParseException("Version logging key must be DownloadType"); if (!(entry.getValue() instanceof LoggingInfo)) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 1448dc485..e061ecb0d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -300,10 +300,6 @@ public class DefaultLauncher extends Launcher { throw new PermissionException(); } - private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener) { - startMonitors(managedProcess, processListener, true); - } - private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) { processListener.setProcess(managedProcess); Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java index 92e6d639f..ab3e4f8e6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.mod; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.FileUtils; -import org.jackhuang.hmcl.util.IOUtils; import org.jackhuang.hmcl.util.Unzipper; import java.io.File; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java index 08a8cfa41..056f7d08d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java @@ -260,7 +260,7 @@ public class FileDownloadTask extends Task { } if (exception != null) - throw new IOException("Unable to download file " + currentURL, exception); + throw new IOException("Unable to download file " + currentURL + ". " + exception.getMessage(), exception); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalRunnable.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalRunnable.java index 941b8cbb2..ed693979a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalRunnable.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ExceptionalRunnable.java @@ -34,7 +34,7 @@ public interface ExceptionalRunnable { }; } - static ExceptionalRunnable fromRunnable(Runnable r) { + static ExceptionalRunnable fromRunnable(Runnable r) { return r::run; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IntVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IntVersionNumber.java index b6a19590b..b1171bdde 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IntVersionNumber.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IntVersionNumber.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.util; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Zipper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Zipper.java index cfb57c817..ac3c4b695 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Zipper.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Zipper.java @@ -67,8 +67,6 @@ public final class Zipper implements Closeable { * @param filter returns false if you do not want that file or directory */ public void putDirectory(Path source, String targetDir, Predicate filter) throws IOException { - File[] files = null; - Path root = fs.getPath(targetDir); Files.createDirectories(root); Files.walkFileTree(source, new SimpleFileVisitor() {