Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;

import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;

import java.util.List;

// TODO: Replace ToolbarListPageSkin with this class gradually
public abstract class ToolbarListPageSkin2<E, P extends ListPageBase<E>> extends SkinBase<P> {

protected final JFXListView<E> listView;

public ToolbarListPageSkin2(P skinnable) {
super(skinnable);

SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
spinnerPane.getStyleClass().add("large-spinner-pane");

ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
StackPane.setMargin(root, new Insets(10));

List<Node> toolbarButtons = initializeToolbar(skinnable);
if (!toolbarButtons.isEmpty()) {
HBox toolbar = new HBox();
toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(toolbarButtons);
root.getContent().add(toolbar);
}

{
this.listView = new JFXListView<>();
this.listView.setPadding(Insets.EMPTY);
ComponentList.setVgrow(listView, Priority.ALWAYS);
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
root.getContent().add(listView);
}

spinnerPane.setContent(root);

getChildren().setAll(spinnerPane);
}

protected abstract List<Node> initializeToolbar(P skinnable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ public void add(Object obj) {
public boolean remove(Object obj) {
return refs.remove(obj);
}

public void clear() {
refs.clear();
}
}
41 changes: 4 additions & 37 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
Expand Down Expand Up @@ -59,10 +57,10 @@
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.GameItem;
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
Expand All @@ -73,7 +71,6 @@
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;

import java.io.IOException;
import java.util.List;
Expand All @@ -92,16 +89,10 @@ public final class MainPage extends StackPane implements DecoratorPage {

private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();

private final PopupMenu menu = new PopupMenu();

private final StackPane popupWrapper = new StackPane(menu);
private final JFXPopup popup = new JFXPopup(popupWrapper);

private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion");
private final ObservableList<Version> versions = FXCollections.observableArrayList();
private final ObservableList<Node> versionNodes;
private Profile profile;

private TransitionPane announcementPane;
Expand Down Expand Up @@ -273,19 +264,6 @@ public void accept(String currentGame) {

getChildren().addAll(updatePane, launchPane);

menu.setMaxHeight(365);
menu.setMaxWidth(545);
menu.setAlwaysShowingVBar(false);
FXUtils.onClicked(menu, popup::hide);
versionNodes = MappedObservableList.create(versions, version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
FXUtils.onClicked(node, () -> {
profile.setSelectedVersion(version.getId());
popup.hide();
});
return node;
});
Bindings.bindContent(menu.getContent(), versionNodes);
}

private void showUpdate(boolean show) {
Expand Down Expand Up @@ -365,20 +343,9 @@ private void launchNoGame() {
}

private void onMenu() {
Node contentNode;
if (menu.getContent().isEmpty()) {
Label placeholder = new Label(i18n("version.empty"));
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
contentNode = placeholder;
} else {
contentNode = menu;
}

popupWrapper.getChildren().setAll(contentNode);

if (popup.isShowing()) {
popup.hide();
}
GameListPopupMenu menu = new GameListPopupMenu();
menu.getItems().setAll(versions.stream().map(it -> new GameItem(profile, it.getId())).toList());
JFXPopup popup = new JFXPopup(menu);
popup.show(
menuButton,
JFXPopup.PopupVPosition.BOTTOM,
Expand Down
148 changes: 83 additions & 65 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -17,107 +17,125 @@
*/
package org.jackhuang.hmcl.ui.versions;

import com.google.gson.JsonParseException;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.beans.property.*;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

public class GameItem extends Control {
public class GameItem {
private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 10, TimeUnit.SECONDS);

private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 1, TimeUnit.SECONDS);
protected final Profile profile;
protected final String id;

private final Profile profile;
private final String version;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty tag = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
private boolean initialized = false;
private StringProperty title;
private StringProperty tag;
private StringProperty subtitle;
private ObjectProperty<Image> image;

public GameItem(Profile profile, String id) {
this.profile = profile;
this.version = id;

// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
CompletableFuture.supplyAsync(() -> profile.getRepository().getGameVersion(id), POOL_VERSION_RESOLVE)
.thenAcceptAsync(game -> {
StringBuilder libraries = new StringBuilder(game.orElse(i18n("message.unknown")));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), game.orElse(null));
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
if (I18n.hasKey("install.installer." + libraryId)) {
libraries.append(", ").append(i18n("install.installer." + libraryId));
if (libraryVersion != null)
libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, ""));
}
}

subtitle.set(libraries.toString());
}, Platform::runLater)
.exceptionally(handleUncaught);

CompletableFuture.runAsync(() -> {
try {
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
if (config == null) return;
tag.set(config.getVersion());
} catch (IOException | JsonParseException e) {
LOG.warning("Failed to read modpack configuration from " + version, e);
}
}, Platform::runLater)
.exceptionally(handleUncaught);

title.set(id);
image.set(profile.getRepository().getVersionIconImage(version));
}

@Override
protected Skin<?> createDefaultSkin() {
return new GameItemSkin(this);
this.id = id;
}

public Profile getProfile() {
return profile;
}

public String getVersion() {
return version;
public String getId() {
return id;
}

private void init() {
if (initialized)
return;

initialized = true;
title = new SimpleStringProperty();
tag = new SimpleStringProperty();
subtitle = new SimpleStringProperty();
image = new SimpleObjectProperty<>();

record Result(@Nullable String gameVersion, @Nullable String tag) {
}

CompletableFuture.supplyAsync(() -> {
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
Optional<String> gameVersion = profile.getRepository().getGameVersion(id);
String modPackVersion = null;
try {
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(id);
modPackVersion = config != null ? config.getVersion() : null;
} catch (IOException e) {
LOG.warning("Failed to read modpack configuration from " + id, e);
}
return new Result(gameVersion.orElse(null), modPackVersion);
}, POOL_VERSION_RESOLVE).whenCompleteAsync((result, exception) -> {
if (exception == null) {
if (result.gameVersion != null) {
title.set(result.gameVersion);
}
if (result.tag != null) {
tag.set(result.tag);
}

StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown")));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion);
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
if (I18n.hasKey("install.installer." + libraryId)) {
libraries.append(", ").append(i18n("install.installer." + libraryId));
if (libraryVersion != null)
libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, ""));
}
}

subtitle.set(libraries.toString());
} else {
LOG.warning("Failed to read version info from " + id, exception);
}
}, Schedulers.javafx());

title.set(id);
image.set(profile.getRepository().getVersionIconImage(id));
}

public StringProperty titleProperty() {
public ReadOnlyStringProperty titleProperty() {
init();
return title;
}

public StringProperty tagProperty() {
public ReadOnlyStringProperty tagProperty() {
init();
return tag;
}

public StringProperty subtitleProperty() {
public ReadOnlyStringProperty subtitleProperty() {
init();
return subtitle;
}

public ObjectProperty<Image> imageProperty() {
public ReadOnlyObjectProperty<Image> imageProperty() {
init();
return image;
}
}
Loading