diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java index adb6d737848..cbcb86f90ba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java @@ -19,67 +19,214 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXListView; +import com.jfoenix.controls.JFXTextField; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.transformation.FilteredList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.ListCell; +import javafx.scene.control.Label; import javafx.scene.control.SkinBase; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.util.StringUtils; -import java.util.List; +import java.util.function.Predicate; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public abstract class ToolbarListPageSkin> extends SkinBase

{ + protected final StackPane mainContainer; + protected final ComponentList rootList; protected final JFXListView listView; + protected FilteredList filteredList; + + protected final TransitionPane toolbarPane; + protected final HBox normalToolbar; + protected final HBox selectingToolbar; + protected final HBox searchBar; + protected JFXTextField searchField; + + protected final BooleanProperty isSearching = new SimpleBooleanProperty(false); + protected final BooleanProperty isSelecting = new SimpleBooleanProperty(false); - public ToolbarListPageSkin(P skinnable) { + public ToolbarListPageSkin(P skinnable, boolean hasSearch) { super(skinnable); - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - - StackPane container = new StackPane(); - container.getChildren().add(root); - StackPane.setMargin(root, new Insets(10)); - - List toolbarButtons = initializeToolbar(skinnable); - if (!toolbarButtons.isEmpty()) { - HBox toolbar = new HBox(); - toolbar.setAlignment(Pos.CENTER_LEFT); - toolbar.setPickOnBounds(false); - toolbar.getChildren().setAll(toolbarButtons); - FXUtils.setOverflowHidden(toolbar, 8); - root.getContent().add(toolbar); + mainContainer = new StackPane(); + mainContainer.setPadding(new Insets(10)); + mainContainer.getStyleClass().addAll("notice-pane"); + + rootList = new ComponentList(); + rootList.getStyleClass().add("no-padding"); + + listView = new JFXListView<>(); + listView.setPadding(Insets.EMPTY); + listView.getStyleClass().add("no-horizontal-scrollbar"); + FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + toolbarPane = new TransitionPane(); + normalToolbar = new HBox(); + selectingToolbar = new HBox(); + searchBar = new HBox(); + + if (hasSearch) { + filteredList = new FilteredList<>(skinnable.getItems()); + listView.setItems(filteredList); + + searchField = new JFXTextField(); + searchField.setPromptText(i18n("search")); + HBox.setHgrow(searchField, Priority.ALWAYS); + + PauseTransition searchPause = new PauseTransition(Duration.millis(100)); + searchPause.setOnFinished(e -> { + if (filteredList != null) { + filteredList.setPredicate(updateSearchPredicate(searchField.getText())); + } + }); + + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + if (isSearching.get() || !StringUtils.isBlank(newValue)) { + searchPause.setRate(1); + searchPause.playFromStart(); + } + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { + searchField.clear(); + searchPause.stop(); + filteredList.setPredicate(null); + isSearching.set(false); + }); + + FXUtils.onEscPressed(searchField, closeSearchBar::fire); + + searchBar.setAlignment(Pos.CENTER); + searchBar.setPadding(new Insets(0, 5, 0, 5)); + searchBar.getChildren().setAll(searchField, closeSearchBar); + } else { + Bindings.bindContent(listView.getItems(), skinnable.itemsProperty()); + } + } + + protected void setupSkin(Node[] normalBtns, Node[] selectingBtns) { + if (normalBtns != null && normalBtns.length > 0) { + normalToolbar.setAlignment(Pos.CENTER_LEFT); + normalToolbar.setPickOnBounds(false); + normalToolbar.getChildren().setAll(normalBtns); } - SpinnerPane spinnerPane = new SpinnerPane(); - spinnerPane.loadingProperty().bind(skinnable.loadingProperty()); - spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty()); - spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); + if (selectingBtns != null && selectingBtns.length > 0) { + selectingToolbar.setAlignment(Pos.CENTER_LEFT); + selectingToolbar.setPickOnBounds(false); + selectingToolbar.getChildren().setAll(selectingBtns); - ComponentList.setVgrow(spinnerPane, Priority.ALWAYS); + JFXButton cancel = createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> listView.getSelectionModel().clearSelection()); - { - this.listView = new JFXListView<>(); - this.listView.setPadding(Insets.EMPTY); - this.listView.setCellFactory(listView -> createListCell((JFXListView) listView)); - this.listView.getStyleClass().add("no-horizontal-scrollbar"); - Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty()); - FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + JFXButton selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size())); + ListChangeListener listener = change -> { + selectAll.setDisable(!listView.getItems().isEmpty() + && listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size()); + }; + listView.getSelectionModel().getSelectedItems().addListener(listener); + listView.getItems().addListener(listener); - spinnerPane.setContent(listView); + selectingToolbar.getChildren().addAll(selectAll, cancel); } - root.getContent().add(spinnerPane); + toolbarPane.setContent(normalToolbar, ContainerAnimations.FADE); + FXUtils.setOverflowHidden(toolbarPane, 8); + rootList.getContent().add(toolbarPane); - getChildren().setAll(container); + rootList.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.loadingProperty().bind(getSkinnable().loadingProperty()); + center.failedReasonProperty().bind(getSkinnable().failedReasonProperty()); + center.onFailedActionProperty().bind(getSkinnable().onFailedActionProperty()); + center.setContent(listView); + rootList.getContent().add(center); + + StackPane placeholderContainer = new StackPane(); + placeholderContainer.getStyleClass().add("notice-pane"); + Label placeholderLabel = new Label(); + placeholderLabel.textProperty().bind(Bindings.createStringBinding(() -> { + if (isSearching.get()) return i18n("search.no_results_found"); + return getEmptyPlaceholderText(); + }, isSearching)); + placeholderContainer.getChildren().add(placeholderLabel); + listView.setPlaceholder(placeholderContainer); + + listView.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> { + isSelecting.set(newV != null); + }); + + InvalidationListener toolbarSwitchListener = obs -> { + if (isSelecting.get() && selectingBtns != null && selectingBtns.length > 0) { + changeToolbar(selectingToolbar); + } else if (isSearching.get() && hasSearch()) { + changeToolbar(searchBar); + } else { + changeToolbar(normalToolbar); + } + }; + + isSearching.addListener(toolbarSwitchListener); + isSelecting.addListener(toolbarSwitchListener); + + toolbarSwitchListener.invalidated(null); + + mainContainer.getChildren().add(rootList); + getChildren().setAll(mainContainer); + } + + protected void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar && searchField != null) { + Platform.runLater(searchField::requestFocus); + } + } + } + + protected boolean hasSearch() { + return searchField != null; + } + + protected void startSearch() { + isSearching.set(true); + } + + protected Predicate updateSearchPredicate(String query) { + return item -> true; + } + + protected String getEmptyPlaceholderText() { + return i18n("mods.empty"); } public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) { @@ -90,31 +237,4 @@ public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onCl ret.setOnAction(e -> onClick.run()); return ret; } - - public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) { - JFXButton ret = new JFXButton(); - ret.getStyleClass().add("jfx-decorator-button"); - ret.setGraphic(svg.createIcon(20)); - FXUtils.installFastTooltip(ret, tooltip); - ret.setOnAction(e -> onClick.run()); - return ret; - } - - protected abstract List initializeToolbar(P skinnable); - - protected ListCell createListCell(JFXListView listView) { - return new ListCell<>() { - @Override - protected void updateItem(E item, boolean empty) { - super.updateItem(item, empty); - if (!empty && item instanceof Node node) { - setGraphic(node); - setText(null); - } else { - setGraphic(null); - setText(null); - } - } - }; - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java index 70261079b31..f9f008c7bba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java @@ -37,8 +37,8 @@ import org.jackhuang.hmcl.java.JavaInfo; import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.java.JavaRuntime; -import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.setting.DownloadProviders; +import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; @@ -50,16 +50,17 @@ import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; -import org.jackhuang.hmcl.util.tree.ArchiveFileTree; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; +import org.jackhuang.hmcl.util.tree.ArchiveFileTree; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -193,11 +194,10 @@ private void loadJava(Collection javaRuntimes) { private static final class JavaPageSkin extends ToolbarListPageSkin { JavaPageSkin(JavaManagementPage skinnable) { - super(skinnable); - } + super(skinnable, false); + + listView.setCellFactory(x -> new JavaItemCell(listView)); - @Override - protected List initializeToolbar(JavaManagementPage skinnable) { ArrayList res = new ArrayList<>(4); res.add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, JavaManager::refresh)); @@ -216,12 +216,7 @@ protected List initializeToolbar(JavaManagementPage skinnable) { } res.add(disableJava); - return res; - } - - @Override - protected ListCell createListCell(JFXListView listView) { - return new JavaItemCell(listView); + setupSkin(res.toArray(new Node[0]), null); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java index 7830d38720e..068a95c47c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java @@ -27,8 +27,10 @@ import javafx.collections.ObservableSet; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Skin; +import javafx.scene.control.SkinBase; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import org.jackhuang.hmcl.java.JavaManager; @@ -43,8 +45,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -188,12 +188,8 @@ private static final class DisabledJavaItemSkin extends SkinBase { JavaRestorePageSkin(JavaRestorePage skinnable) { - super(skinnable); - } - - @Override - protected List initializeToolbar(JavaRestorePage skinnable) { - return Collections.emptyList(); + super(skinnable, false); + setupSkin(null, null); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java index 3c6eb4dd864..8d67ee7ee71 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java @@ -19,37 +19,21 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXTextField; -import javafx.animation.PauseTransition; -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; 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.ListCell; import javafx.scene.control.ScrollPane; import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; import javafx.scene.layout.*; import javafx.stage.FileChooser; -import javafx.util.Duration; import org.jackhuang.hmcl.theme.*; import org.jackhuang.hmcl.ui.*; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -65,8 +49,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import static org.jackhuang.hmcl.ui.FXUtils.*; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -82,13 +65,6 @@ public final class ThemePackManagementPage extends ListPageBase state = new SimpleObjectProperty<>(State.fromTitle(i18n("theme_pack.manage"))); - /// Source list containing all installed theme packs. - private final ObservableList sourceList = - FXCollections.observableArrayList(); - - /// Filtered list shown by the current search query. - private final FilteredList filteredList = new FilteredList<>(sourceList); - /// Page-scoped icon image cache cleared when installed theme packs are reloaded. private final IconCache iconCache = new IconCache(); @@ -102,7 +78,6 @@ public ThemePackManagementPage(Runnable onThemePacksChanged) { getStyleClass().add("gray-background"); this.onThemePacksChanged = Objects.requireNonNull(onThemePacksChanged); - setItems(filteredList); setOnFailedAction(event -> refreshThemePacks()); refreshThemePacks(); @@ -126,35 +101,15 @@ private void refreshThemePacks() { iconCache.clear(); try { List themePacks = ThemePackManager.listInstalled(); - sourceList.setAll(themePacks); + getItems().setAll(themePacks); setFailedReason(themePacks.isEmpty() ? i18n("theme_pack.empty") : null); } catch (IOException | RuntimeException e) { LOG.warning("Failed to load installed theme packs", e); - sourceList.clear(); + getItems().clear(); setFailedReason(i18n("theme_pack.load.failed")); } } - /// Creates a predicate used by the theme-pack search field. - private Predicate createPredicate(String searchText) { - if (StringUtils.isBlank(searchText)) { - return themePack -> true; - } - - String query = searchText.toLowerCase(Locale.ROOT); - return themePack -> { - ThemePackManifest manifest = themePack.manifest(); - return containsIgnoreCase(manifest.displayName(), query) - || containsIgnoreCase(manifest.id(), query) - || containsIgnoreCase(manifest.version(), query) - || containsIgnoreCase(manifest.displayDescription(), query) - || manifest.authors().stream().anyMatch(author -> containsIgnoreCase(author.displayName(), query)) - || manifest.themes().stream() - .flatMap(theme -> theme.authors().stream()) - .anyMatch(author -> containsIgnoreCase(author.displayName(), query)); - }; - } - /// Opens a theme-pack file and installs it. private void importThemePack() { FileChooser chooser = new FileChooser(); @@ -291,7 +246,7 @@ private static String getThemeDisplayName(ThemePackManifest manifest, Theme them } /// Returns whether a nullable value contains the lower-cased query. - private static boolean containsIgnoreCase(@Nullable String value, String query) { + static boolean containsIgnoreCase(@Nullable String value, String query) { return value != null && value.toLowerCase(Locale.ROOT).contains(query); } @@ -339,17 +294,10 @@ private void updateIcon( } /// Cache key for one decoded icon image. - /// - /// @param location the source theme-pack location - /// @param icon the theme-pack asset path @NotNullByDefault private record IconCacheKey( ThemePackManager.ThemePackLocation location, String icon) { - /// Creates an icon cache key. - /// - /// @param location the source theme-pack location - /// @param icon the theme-pack asset path private IconCacheKey { Objects.requireNonNull(location); Objects.requireNonNull(icon); @@ -359,19 +307,14 @@ private record IconCacheKey( /// Page-scoped cache for decoded icon images. @NotNullByDefault private static final class IconCache { - /// Decoded theme-pack icons keyed by location and asset path. private final Map> icons = new HashMap<>(); - - /// Decoded built-in fallback icon. private @Nullable Image defaultBuiltinIcon; - /// Clears all decoded images held by this cache. private void clear() { icons.clear(); defaultBuiltinIcon = null; } - /// Returns a theme-pack icon image, or `null` when it cannot be loaded. private @Nullable Image getThemePackIcon( ThemePackManager.ThemePackLocation location, String icon) { @@ -379,7 +322,6 @@ private void clear() { return icons.computeIfAbsent(key, this::loadThemePackIcon).orElse(null); } - /// Returns the built-in default icon for built-in theme packs. private Image getDefaultBuiltinIcon() { if (defaultBuiltinIcon == null) { defaultBuiltinIcon = FXUtils.newBuiltinImage( @@ -392,7 +334,6 @@ private Image getDefaultBuiltinIcon() { return defaultBuiltinIcon; } - /// Loads and decodes one theme-pack icon image. private Optional loadThemePackIcon(IconCacheKey key) { try { ThemePackResource resource = ThemePackManager.resolveInstalledAsset(key.location(), key.icon()); @@ -413,9 +354,6 @@ private Optional loadThemePackIcon(IconCacheKey key) { /// Dialog showing all themes declared by one installed theme pack. @NotNullByDefault private static final class ThemePackInfoDialog extends JFXDialogLayout { - /// Creates the theme-pack information dialog. - /// - /// @param themePack the installed theme pack to display private ThemePackInfoDialog(ThemePackManagementPage page, ThemePackManager.InstalledThemePack themePack) { ThemePackManifest manifest = themePack.manifest(); maxWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7)); @@ -495,169 +433,68 @@ private ThemePackInfoDialog(ThemePackManagementPage page, ThemePackManager.Insta setActions(okButton); onEscPressed(this, okButton::fire); } - } /// Skin for the theme-pack management list page. @NotNullByDefault - private static final class ThemePackManagementPageSkin extends SkinBase { - /// Toolbar container used to switch between normal and search actions. - private final TransitionPane toolbarPane = new TransitionPane(); - - /// Search toolbar. - private final HBox searchBar = new HBox(); - - /// Toolbar shown during normal browsing. - private final HBox toolbarNormal = new HBox(); + private static final class ThemePackManagementPageSkin extends ToolbarListPageSkin { - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - /// Search input. - private final JFXTextField searchField = new JFXTextField(); - - /// List of installed theme packs. - private final JFXListView listView = new JFXListView<>(); - - /// Creates the management page skin. - /// - /// @param skinnable the page controlled by this skin private ThemePackManagementPageSkin(ThemePackManagementPage skinnable) { - super(skinnable); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - - initializeToolbar(skinnable, root); - initializeList(skinnable, root); - - pane.getChildren().setAll(root); - getChildren().setAll(pane); - } - - /// Initializes toolbar actions and search. - private void initializeToolbar(ThemePackManagementPage skinnable, ComponentList root) { - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - - PauseTransition pause = new PauseTransition(Duration.millis(100)); - pause.setOnFinished(event -> - skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText()))); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - pause.setRate(1); - pause.playFromStart(); - } - }); - - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { - changeToolbar(toolbarNormal); + super(skinnable, true); - searchField.clear(); - pause.stop(); - - skinnable.filteredList.setPredicate(null); - isSearching.set(false); - }); - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - toolbarNormal.setAlignment(Pos.CENTER_LEFT); - toolbarNormal.setPickOnBounds(false); - toolbarNormal.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshThemePacks), - createToolbarButton2(i18n("theme_pack.import"), SVG.FILE_OPEN, skinnable::importThemePack), - createToolbarButton2(i18n("theme_pack.directory"), SVG.FOLDER_OPEN, - () -> FXUtils.openFolder(ThemePackManager.THEME_PACKS_DIRECTORY)), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))); - - toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE); - FXUtils.setOverflowHidden(toolbarPane, 8); + listView.setCellFactory(x -> new ThemePackItemCell(skinnable)); - root.getContent().add(toolbarPane); + setupSkin( + new Node[]{ + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshThemePacks), + createToolbarButton2(i18n("theme_pack.import"), SVG.FILE_OPEN, skinnable::importThemePack), + createToolbarButton2(i18n("theme_pack.directory"), SVG.FOLDER_OPEN, + () -> FXUtils.openFolder(ThemePackManager.THEME_PACKS_DIRECTORY)), + createToolbarButton2(i18n("search"), SVG.SEARCH, this::startSearch) + }, + null + ); } - /// Initializes the installed theme-pack list. - private void initializeList(ThemePackManagementPage skinnable, ComponentList root) { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(skinnable.loadingProperty()); - center.failedReasonProperty().bind(skinnable.failedReasonProperty()); - center.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); - - listView.setPadding(Insets.EMPTY); - listView.setCellFactory(x -> new ThemePackItemCell(skinnable)); - listView.setItems(skinnable.getItems()); - listView.getStyleClass().add("no-horizontal-scrollbar"); - ignoreEvent(listView, KeyEvent.KEY_PRESSED, event -> event.getCode() == KeyCode.ESCAPE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(); - placeholderLabel.textProperty().bind( - Bindings.when(isSearching).then(i18n("search.no_results_found")).otherwise("") - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); + @Override + protected Predicate updateSearchPredicate(String searchText) { + if (StringUtils.isBlank(searchText)) { + return themePack -> true; + } - center.setContent(listView); - root.getContent().add(center); + String query = searchText.toLowerCase(Locale.ROOT); + return themePack -> { + ThemePackManifest manifest = themePack.manifest(); + return containsIgnoreCase(manifest.displayName(), query) + || containsIgnoreCase(manifest.id(), query) + || containsIgnoreCase(manifest.version(), query) + || containsIgnoreCase(manifest.displayDescription(), query) + || manifest.authors().stream().anyMatch(author -> containsIgnoreCase(author.displayName(), query)) + || manifest.themes().stream() + .flatMap(theme -> theme.authors().stream()) + .anyMatch(author -> containsIgnoreCase(author.displayName(), query)); + }; } - /// Switches the visible toolbar. - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - runInFX(searchField::requestFocus); - - isSearching.set(true); - } - } + @Override + protected String getEmptyPlaceholderText() { + return i18n("theme_pack.empty"); } } /// List cell that renders one installed theme pack. @NotNullByDefault private static final class ThemePackItemCell extends ListCell { - /// The owning management page. private final ThemePackManagementPage page; - - /// Root graphic reused by this cell. private final Region graphic; - - /// The text content shown for the current theme pack. private final TwoLineListItem content = new TwoLineListItem(); - - /// Left-side package icon container. private final StackPane icon = new StackPane(); - - /// Reused icon image node. private final ImageContainer iconImage = new ImageContainer(ICON_SIZE); - - /// Reused fallback icon. private final SVGContainer iconFallback = createFallbackIcon(SVG.PACKAGE2); - - /// Right-side action container. private final HBox right = new HBox(); - - /// Shows the package file. private final JFXButton revealButton; - - /// Deletes the package. private final JFXButton deleteButton; - /// Creates a reusable theme-pack list cell. - /// - /// @param page the owning management page private ThemePackItemCell(ThemePackManagementPage page) { this.page = page; @@ -710,7 +547,6 @@ private ThemePackItemCell(ThemePackManagementPage page) { FXUtils.installFastTooltip(deleteButton, i18n("theme_pack.delete")); } - /// Updates this cell for one installed theme pack. @Override protected void updateItem(ThemePackManager.@Nullable InstalledThemePack themePack, boolean empty) { var currentItem = getItem(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java index 9ee38a15b0f..2a4acbaeb6a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java @@ -21,25 +21,20 @@ import javafx.collections.ObservableList; import javafx.scene.control.Skin; import javafx.stage.FileChooser; -import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.addon.datapack.DataPack; +import org.jackhuang.hmcl.game.World; 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.ListPageBase; -import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.javafx.MappedObservableList; -import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.file.Path; import java.util.List; -import java.util.Locale; import java.util.Objects; -import java.util.function.Predicate; -import java.util.regex.Pattern; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -127,29 +122,4 @@ void disableSelected(ObservableList sel void openDataPackFolder() { FXUtils.openFolder(dataPack.getPath()); } - - @NotNull Predicate updateSearchPredicate(String queryString) { - if (queryString.isBlank()) { - return dataPack -> true; - } - - final Predicate stringPredicate; - if (queryString.startsWith("regex:")) { - try { - Pattern pattern = Pattern.compile(StringUtils.substringAfter(queryString, "regex:")); - stringPredicate = s -> s != null && pattern.matcher(s).find(); - } catch (Exception e) { - return dataPack -> false; - } - } else { - String lowerCaseFilter = queryString.toLowerCase(Locale.ROOT); - stringPredicate = s -> s != null && s.toLowerCase(Locale.ROOT).contains(lowerCaseFilter); - } - - return dataPack -> { - String id = dataPack.getPackInfo().getId(); - String description = dataPack.getPackInfo().getDescription().toString(); - return stringPredicate.test(id) || stringPredicate.test(description); - }; - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java index bb8753568ff..5196a13e83f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java @@ -20,39 +20,27 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject; -import javafx.animation.PauseTransition; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.collections.ListChangeListener; -import javafx.collections.transformation.FilteredList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; -import javafx.scene.control.SkinBase; import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; -import javafx.util.Duration; import org.jackhuang.hmcl.addon.datapack.DataPack; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.ToolbarListPageSkin; +import org.jackhuang.hmcl.ui.construct.ImageContainer; +import org.jackhuang.hmcl.ui.construct.MDListCell; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jetbrains.annotations.NotNullByDefault; @@ -63,177 +51,54 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.IntStream; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @NotNullByDefault -final class DataPackListPageSkin extends SkinBase { - - private final TransitionPane toolbarPane; - private final HBox searchBar; - private final HBox normalToolbar; - private final HBox selectingToolbar; - InvalidationListener updateBarByStateWeakListener; - - private final JFXListView listView; - private final FilteredList filteredList; - - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - private final BooleanProperty isSelecting = new SimpleBooleanProperty(false); - private final JFXTextField searchField; - - /// Timer for debouncing search input to avoid executing search on every keystroke. - private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); +final class DataPackListPageSkin extends ToolbarListPageSkin { private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1); final Consumer toggleSelect; DataPackListPageSkin(DataPackListPage skinnable) { - super(skinnable); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - listView = new JFXListView<>(); - filteredList = new FilteredList<>(skinnable.getItems()); - - // reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker - var selectAllButton = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size())); - - ListChangeListener listener = change -> { - selectAllButton.setDisable(!listView.getItems().isEmpty() - && listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size()); - }; - - { - toolbarPane = new TransitionPane(); - searchBar = new HBox(); - normalToolbar = new HBox(); - selectingToolbar = new HBox(); - - normalToolbar.getChildren().addAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openDataPackFolder), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true)) - ); - - JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); - }, null); - }); - JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())); - JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())); - removeButton.disableProperty().bind(getSkinnable().readOnly); - enableButton.disableProperty().bind(getSkinnable().readOnly); - disableButton.disableProperty().bind(getSkinnable().readOnly); - - listView.getSelectionModel().getSelectedItems().addListener(listener); - - selectingToolbar.getChildren().addAll( - removeButton, - enableButton, - disableButton, - selectAllButton, - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField = new JFXTextField(); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - searchPause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText()))); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - searchPause.setRate(1); - searchPause.playFromStart(); - } - }); - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - searchField.clear(); - searchPause.stop(); - - filteredList.setPredicate(null); - - isSearching.set(false); - }); - FXUtils.onEscPressed(searchField, closeSearchBar::fire); - searchBar.getChildren().addAll(searchField, closeSearchBar); - - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), - selectedItem -> isSelecting.set(selectedItem != null)); - root.getContent().add(toolbarPane); - - updateBarByStateWeakListener = FXUtils.observeWeak(() -> { - if (isSelecting.get()) { - changeToolbar(selectingToolbar); - } else if (!isSelecting.get() && !isSearching.get()) { - changeToolbar(normalToolbar); - } else { - changeToolbar(searchBar); - } - }, isSearching, isSelecting); - } - - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(skinnable.loadingProperty()); - - listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("datapack.empty")); - placeholderLabel.textProperty().bind( - Bindings.createStringBinding(() -> { - if (isSearching.get()) { - return i18n("search.no_results_found"); - } else { - return i18n("datapack.empty"); - } + super(skinnable, true); + + listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + + JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); + }, null); + }); + JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> + skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())); + JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> + skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())); + + removeButton.disableProperty().bind(getSkinnable().readOnly); + enableButton.disableProperty().bind(getSkinnable().readOnly); + disableButton.disableProperty().bind(getSkinnable().readOnly); + + setupSkin( + new Node[]{ + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openDataPackFolder), + createToolbarButton2(i18n("search"), SVG.SEARCH, this::startSearch) }, - isSearching) - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - - this.listView.setItems(filteredList); - listView.getItems().addListener(listener); - - // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here - FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); - } + new Node[]{ + removeButton, enableButton, disableButton + } + ); toggleSelect = i -> { if (listView.getSelectionModel().isSelected(i)) { @@ -242,21 +107,37 @@ final class DataPackListPageSkin extends SkinBase { listView.getSelectionModel().select(i); } }; - - pane.getChildren().setAll(root); - getChildren().setAll(pane); } - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - // search button click will get focus while searchField request focus, this cause conflict. - // Defer focus request to next pulse avoids this conflict. - Platform.runLater(searchField::requestFocus); + @Override + protected Predicate updateSearchPredicate(String queryString) { + if (queryString.isBlank()) { + return dataPack -> true; + } + + final Predicate stringPredicate; + if (queryString.startsWith("regex:")) { + try { + Pattern pattern = Pattern.compile(StringUtils.substringAfter(queryString, "regex:")); + stringPredicate = s -> pattern.matcher(s).find(); + } catch (Exception e) { + return dataPack -> false; } + } else { + String lowerCaseFilter = queryString.toLowerCase(Locale.ROOT); + stringPredicate = s -> s.toLowerCase(Locale.ROOT).contains(lowerCaseFilter); } + + return dataPack -> { + String id = dataPack.getPackInfo().getId(); + String description = dataPack.getPackInfo().getDescription().toString(); + return stringPredicate.test(id) || stringPredicate.test(description); + }; + } + + @Override + protected String getEmptyPlaceholderText() { + return i18n("datapack.empty"); } static class DataPackInfoObject extends RecursiveTreeObject { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index b83aac3ac5e..3674151e3ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -17,43 +17,26 @@ */ package org.jackhuang.hmcl.ui.versions; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXTextField; -import javafx.animation.PauseTransition; import javafx.beans.binding.Bindings; -import javafx.beans.property.*; -import javafx.collections.FXCollections; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; -import javafx.geometry.Insets; -import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ScrollPane; import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; -import javafx.util.Duration; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.setting.GameDirectoryManager; import org.jackhuang.hmcl.ui.*; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; -import org.jackhuang.hmcl.ui.construct.ComponentList; -import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.directory.GameDirectoryListItem; import org.jackhuang.hmcl.ui.directory.GameDirectoryPage; +import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.util.FXThread; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.javafx.MappedObservableList; @@ -65,8 +48,6 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -import static org.jackhuang.hmcl.ui.FXUtils.*; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage { @@ -135,14 +116,8 @@ public ReadOnlyObjectProperty stateProperty() { private static class GameList extends ListPageBase { private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - private final ObservableList sourceList = FXCollections.observableArrayList(); - private final FilteredList filteredList = new FilteredList<>(sourceList); - public GameList() { - setItems(filteredList); - GameDirectoryManager.registerVersionsListener(this::loadVersions); - setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage())); } @@ -154,7 +129,7 @@ private void loadVersions(HMCLGameRepository repository) { List versionItems = repository.getDisplayVersions().map(instance -> new GameListItem(repository, instance.getId())).toList(); - sourceList.setAll(versionItems); + getItems().setAll(versionItems); // 直接更新源数据 if (versionItems.isEmpty()) { setFailedReason(i18n("version.empty.hint")); @@ -163,24 +138,6 @@ private void loadVersions(HMCLGameRepository repository) { setLoading(false); } - private Predicate createPredicate(String searchText) { - if (searchText == null || searchText.isEmpty()) { - return item -> true; - } - - if (searchText.startsWith("regex:")) { - String regex = searchText.substring("regex:".length()); - try { - Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); - return item -> pattern.matcher(item.id).find(); - } catch (PatternSyntaxException e) { - return item -> false; - } - } else { - return item -> item.id.toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT)); - } - } - public void refreshList() { GameDirectoryManager.getSelectedRepository().refreshVersionsAsync().start(); } @@ -190,88 +147,41 @@ protected Skin createDefaultSkin() { return new GameListSkin(this); } - private static class GameListSkin extends SkinBase { - private final TransitionPane toolbarPane; - private final HBox searchBar; - private final HBox toolbarNormal; - - private final JFXTextField searchField; + private static class GameListSkin extends ToolbarListPageSkin { public GameListSkin(GameList skinnable) { - super(skinnable); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - JFXListView listView = new JFXListView<>(); - - { - toolbarPane = new TransitionPane(); - - searchBar = new HBox(); - toolbarNormal = new HBox(); - - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField = new JFXTextField(); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - PauseTransition pause = new PauseTransition(Duration.millis(100)); - pause.setOnFinished(e -> skinnable.filteredList.setPredicate(skinnable.createPredicate(searchField.getText()))); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - pause.setRate(1); - pause.playFromStart(); - }); - - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { - changeToolbar(toolbarNormal); - searchField.clear(); - }); - - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - toolbarNormal.getChildren().setAll(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))); - - toolbarPane.setContent(toolbarNormal, ContainerAnimations.FADE); - - FXUtils.setOverflowHidden(toolbarPane, 8); - - root.getContent().add(toolbarPane); - } - - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(skinnable.loadingProperty()); - center.failedReasonProperty().bind(skinnable.failedReasonProperty()); - - listView.setCellFactory(x -> new GameListCell()); - listView.setItems(skinnable.getItems()); - - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); - } - - pane.getChildren().setAll(root); - getChildren().setAll(pane); + super(skinnable, true); + listView.setCellFactory(x -> new GameListCell()); + + setupSkin( + new Node[]{ + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refreshList), + createToolbarButton2(i18n("search"), SVG.SEARCH, this::startSearch) + }, + null + ); } - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - runInFX(searchField::requestFocus); + @Override + protected Predicate updateSearchPredicate(String searchText) { + if (searchText == null || searchText.isEmpty()) return item -> true; + if (searchText.startsWith("regex:")) { + String regex = searchText.substring("regex:".length()); + try { + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + return item -> pattern.matcher(item.id).find(); + } catch (PatternSyntaxException e) { + return item -> false; } + } else { + return item -> item.id.toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT)); } } + + @Override + protected String getEmptyPlaceholderText() { + return i18n("version.empty.hint"); + } } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java index 334115a6d7c..6ed63154ac5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java @@ -35,8 +35,6 @@ import java.nio.file.Path; import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.concurrent.CompletableFuture; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; @@ -166,15 +164,13 @@ public void onStop(boolean success, TaskExecutor executor) { } private class InstallerListPageSkin extends ToolbarListPageSkin { - InstallerListPageSkin() { - super(InstallerListPage.this); - } - - @Override - protected List initializeToolbar(InstallerListPage skinnable) { - return Collections.singletonList( - createToolbarButton2(i18n("install.installer.install_offline"), SVG.ADD, skinnable::installOffline) + super(InstallerListPage.this, false); + setupSkin( + new Node[]{ + createToolbarButton2(i18n("install.installer.install_offline"), SVG.ADD, getSkinnable()::installOffline) + }, + null ); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index ed029d84136..4664ca0261c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -18,30 +18,26 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.*; -import javafx.animation.PauseTransition; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.collections.ListChangeListener; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Tooltip; import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; -import javafx.util.Duration; -import org.jackhuang.hmcl.addon.*; -import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; +import org.jackhuang.hmcl.addon.RemoteAddon; +import org.jackhuang.hmcl.addon.RemoteAddonRepository; import org.jackhuang.hmcl.addon.mod.LocalModFile; import org.jackhuang.hmcl.addon.mod.ModLoaderType; +import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.DownloadProviders; @@ -51,8 +47,7 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.ToolbarListPageSkin; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.FXThread; import org.jackhuang.hmcl.util.Lazy; @@ -74,257 +69,91 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; -import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @NotNullByDefault -final class ModListPageSkin extends SkinBase { - - private final TransitionPane toolbarPane; - private final HBox searchBar; - private final HBox toolbarNormal; - private final HBox toolbarSelecting; - - private final JFXListView listView; - - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - private final JFXTextField searchField; - - /// Timer for debouncing search input to avoid executing search on every keystroke. - private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); +final class ModListPageSkin extends ToolbarListPageSkin { ModListPageSkin(ModListPage skinnable) { - super(skinnable); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - listView = new JFXListView<>(); - listView.getStyleClass().add("no-horizontal-scrollbar"); - - { - toolbarPane = new TransitionPane(); - - searchBar = new HBox(); - toolbarNormal = new HBox(); - toolbarSelecting = new HBox(); - - // Search Bar - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField = new JFXTextField(); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - searchPause.setOnFinished(e -> search()); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - searchPause.setRate(1); - searchPause.playFromStart(); - } - }); + super(skinnable, true); - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(toolbarNormal); - - searchField.clear(); - searchPause.stop(); - - isSearching.set(false); - Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); - }); - - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - // Toolbar Normal - toolbarNormal.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getItems().stream() - .map(ModInfoObject::getModInfo) - .toList() - ) - ), - createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - - // Toolbar Selecting - - // reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker - var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size())); - - ListChangeListener listener = change -> { - selectAll.setDisable(!listView.getItems().isEmpty() - && listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size()); - }; + listView.setCellFactory(x -> new ModInfoListCell(listView)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - listView.getSelectionModel().getSelectedItems().addListener(listener); - listView.getItems().addListener(listener); - - toolbarSelecting.getChildren().setAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); - }, null); - }), - createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())), - createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getSelectionModel().getSelectedItems().stream() - .map(ModInfoObject::getModInfo) - .toList() - ) - ), - selectAll, - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), - selectedItem -> { - if (selectedItem == null) - changeToolbar(isSearching.get() ? searchBar : toolbarNormal); - else - changeToolbar(toolbarSelecting); - }); - - FXUtils.setOverflowHidden(toolbarPane, 8); - - root.getContent().add(toolbarPane); - - // Clear selection when pressing ESC - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - } + listView.setOnContextMenuRequested(event -> { + ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); + if (listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + Controllers.dialog(new ModInfoDialog(selectedItem)); + } + }); - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(skinnable.loadingProperty()); - - listView.setCellFactory(x -> new ModInfoListCell(listView)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("mods.empty")); - placeholderLabel.textProperty().bind( - Bindings.createStringBinding(() -> { - if (isSearching.get()) { - return i18n("search.no_results_found"); - } else { - return i18n("mods.empty"); - } + setupSkin( + new Node[]{ + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + skinnable.checkUpdates(listView.getItems().stream().map(ModInfoObject::getModInfo).toList()) + ), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download), + createToolbarButton2(i18n("search"), SVG.SEARCH, this::startSearch) }, - isSearching) - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - - Bindings.bindContent(listView.getItems(), skinnable.getItems()); - skinnable.getItems().addListener((ListChangeListener) c -> { - if (isSearching.get()) { - search(); + new Node[]{ + createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); + }, null); + }), + createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> + skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())), + createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> + skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + skinnable.checkUpdates(listView.getSelectionModel().getSelectedItems().stream().map(ModInfoObject::getModInfo).toList()) + ) } - }); + ); - listView.setOnContextMenuRequested(event -> { - ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); - if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { - listView.getSelectionModel().clearSelection(); - Controllers.dialog(new ModInfoDialog(selectedItem)); - } - }); - - // ListViewBehavior would consume ESC pressed event, preventing us from handling it - // So we ignore it here - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); - } - - Label label = new Label(i18n("mods.not_modded")); - label.prefWidthProperty().bind(pane.widthProperty().add(-100)); + Label notModdedLabel = new Label(i18n("mods.not_modded")); + notModdedLabel.prefWidthProperty().bind(mainContainer.widthProperty().add(-100)); FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> { - if (modded) pane.getChildren().setAll(root); - else pane.getChildren().setAll(label); + if (modded) mainContainer.getChildren().setAll(rootList); + else mainContainer.getChildren().setAll(notModdedLabel); }); - - getChildren().setAll(pane); } - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - Platform.runLater(searchField::requestFocus); - } - } - } - - private void search() { - isSearching.set(true); - - Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); - - String queryString = searchField.getText(); - if (StringUtils.isBlank(queryString)) { - listView.getItems().setAll(getSkinnable().getItems()); - } else { - listView.getItems().clear(); - - Predicate<@Nullable String> predicate; - try { - predicate = StringUtils.compileQuery(queryString); - } catch (Throwable e) { - LOG.warning("Illegal regular expression", e); - return; - } - - // Do we need to search in the background thread? - for (ModInfoObject item : getSkinnable().getItems()) { + @Override + protected Predicate updateSearchPredicate(String queryString) { + if (StringUtils.isBlank(queryString)) return item -> true; + try { + Predicate<@Nullable String> predicate = StringUtils.compileQuery(queryString); + return item -> { LocalModFile modInfo = item.getModInfo(); - if (predicate.test(modInfo.getFileName()) + return predicate.test(modInfo.getFileName()) || predicate.test(modInfo.getName()) || predicate.test(modInfo.getVersion()) || predicate.test(modInfo.getGameVersion()) || predicate.test(modInfo.getId()) || predicate.test(Objects.toString(modInfo.getModLoaderType())) - || predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null))) { - listView.getItems().add(item); - } - } + || predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null)); + }; + } catch (Throwable e) { + LOG.warning("Illegal regular expression", e); + return item -> true; } } + @Override + protected String getEmptyPlaceholderText() { + return i18n("mods.empty"); + } + static final class ModInfoObject { private final BooleanProperty active; private final LocalModFile localModFile; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index d8688c9c05a..527d0f75950 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -17,41 +17,38 @@ */ package org.jackhuang.hmcl.ui.versions; -import com.jfoenix.controls.*; -import javafx.animation.PauseTransition; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXCheckBox; +import com.jfoenix.controls.JFXDialogLayout; +import com.jfoenix.controls.JFXListView; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Skin; import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; -import javafx.util.Duration; -import org.jackhuang.hmcl.addon.*; +import org.jackhuang.hmcl.addon.LocalAddonFile; +import org.jackhuang.hmcl.addon.RemoteAddon; +import org.jackhuang.hmcl.addon.RemoteAddonRepository; import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile; import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.setting.DownloadProviders; +import org.jackhuang.hmcl.setting.SettingsManager; 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.ListPageBase; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; @@ -66,9 +63,7 @@ import java.util.function.Predicate; import java.util.stream.Stream; -import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -76,6 +71,7 @@ public final class ResourcePackListPage extends ListPageBase implements VersionPage.GameInstanceLoadable { private static final String TIP_KEY = "resourcePackWarning"; + private static @Nullable String getWarning(ResourcePackFile.Compatibility compatibility) { return switch (compatibility) { case TOO_NEW -> i18n("resourcepack.warning.too_new"); @@ -161,7 +157,7 @@ public void addFiles(List files) { }).withRunAsync(Schedulers.javafx(), () -> { if (!failures.isEmpty()) { StringBuilder failure = new StringBuilder(i18n("resourcepack.add.failed")); - for (Path file: failures) { + for (Path file : failures) { failure.append("\n").append(file.toString()); } Controllers.dialog(failure.toString(), i18n("message.error"), MessageDialogPane.MessageType.ERROR); @@ -257,207 +253,74 @@ public void checkUpdates(Collection resourcePacks) { } @NotNullByDefault - private static final class ResourcePackListPageSkin extends SkinBase { - private final JFXListView listView; - private final JFXTextField searchField = new JFXTextField(); - - private final TransitionPane toolbarPane = new TransitionPane(); - private final HBox searchBar = new HBox(); - private final HBox toolbarNormal = new HBox(); - private final HBox toolbarSelecting = new HBox(); - - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - /// Timer for debouncing search input to avoid executing search on every keystroke. - private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); + private static final class ResourcePackListPageSkin extends ToolbarListPageSkin { private ResourcePackListPageSkin(ResourcePackListPage control) { - super(control); + super(control, true); - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); + listView.setCellFactory(x -> new ResourcePackListCell(listView, control)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - - listView = new JFXListView<>(); - - { - - // Toolbar Selecting - toolbarSelecting.getChildren().setAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - control.removeSelected(listView.getSelectionModel().getSelectedItems()); - }, null); - }), - createToolbarButton2(i18n("button.enable"), SVG.CHECK, () -> - control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)), - createToolbarButton2(i18n("button.disable"), SVG.CLOSE, () -> - control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - control.checkUpdates( - listView.getSelectionModel().getSelectedItems().stream().map(ResourcePackInfoObject::getFile).toList() - ) - ), - createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> - listView.getSelectionModel().selectAll()), - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - - // Search Bar - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - searchPause.setOnFinished(e -> search()); - FXUtils.onChange(searchField.textProperty(), newValue -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - searchPause.setRate(1); - searchPause.playFromStart(); - } - }); - - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(toolbarNormal); - - searchField.clear(); - searchPause.stop(); - - isSearching.set(false); - Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); - }); - - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - // Toolbar Normal - toolbarNormal.setAlignment(Pos.CENTER_LEFT); - toolbarNormal.setPickOnBounds(false); - toolbarNormal.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), - createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - control.checkUpdates(listView.getItems().stream().map(ResourcePackInfoObject::getFile).toList()) - ), - createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), - selectedItem -> { - if (selectedItem == null) - changeToolbar(isSearching.get() ? searchBar : toolbarNormal); - else - changeToolbar(toolbarSelecting); - }); - FXUtils.setOverflowHidden(toolbarPane, 8); - root.getContent().add(toolbarPane); - - // Clear selection when pressing ESC - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - } + listView.setOnContextMenuRequested(event -> { + ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); + if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + Controllers.dialog(new ResourcePackInfoDialog(control, selectedItem)); + } + }); - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(control.loadingProperty()); - - listView.setCellFactory(x -> new ResourcePackListCell(listView, control)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("resourcepack.empty")); - placeholderLabel.textProperty().bind( - Bindings.createStringBinding(() -> { - if (isSearching.get()) { - return i18n("search.no_results_found"); - } else { - return i18n("resourcepack.empty"); - } + setupSkin( + new Node[]{ + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), + createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + control.checkUpdates(listView.getItems().stream().map(ResourcePackInfoObject::getFile).toList()) + ), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), + createToolbarButton2(i18n("search"), SVG.SEARCH, this::startSearch) }, - isSearching) - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - - Bindings.bindContent(listView.getItems(), control.getItems()); - - listView.setOnContextMenuRequested(event -> { - ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); - if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { - listView.getSelectionModel().clearSelection(); - Controllers.dialog(new ResourcePackInfoDialog(control, selectedItem)); + new Node[]{ + createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + control.removeSelected(listView.getSelectionModel().getSelectedItems()); + }, null); + }), + createToolbarButton2(i18n("button.enable"), SVG.CHECK, () -> + control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)), + createToolbarButton2(i18n("button.disable"), SVG.CLOSE, () -> + control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + control.checkUpdates( + listView.getSelectionModel().getSelectedItems().stream().map(ResourcePackInfoObject::getFile).toList() + ) + ) } - }); - - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - listView.getStyleClass().add("no-horizontal-scrollbar"); - - center.setContent(listView); - root.getContent().add(center); - } - - pane.getChildren().setAll(root); - getChildren().setAll(pane); + ); } - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - Platform.runLater(searchField::requestFocus); - } - } + @Override + protected String getEmptyPlaceholderText() { + return i18n("resourcepack.empty"); } - private void search() { - isSearching.set(true); - - Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); - - String queryString = searchField.getText(); - if (StringUtils.isBlank(queryString)) { - listView.getItems().setAll(getSkinnable().getItems()); - } else { - listView.getItems().clear(); - - Predicate<@Nullable String> predicate; - try { - predicate = StringUtils.compileQuery(queryString); - } catch (Throwable e) { - LOG.warning("Illegal regular expression", e); - return; - } - - // Do we need to search in the background thread? - for (ResourcePackInfoObject item : getSkinnable().getItems()) { + @Override + protected Predicate updateSearchPredicate(String queryString) { + if (StringUtils.isBlank(queryString)) return item -> true; + try { + Predicate<@Nullable String> queryPredicate = StringUtils.compileQuery(queryString); + return item -> { ResourcePackFile resourcePack = item.getFile(); LocalAddonFile.Description description = resourcePack.getDescription(); Stream descriptionParts = description == null ? Stream.empty() : description.getParts().stream().map(LocalAddonFile.Description.Part::getText); - if (predicate.test(resourcePack.getFileNameWithExtension()) - || predicate.test(resourcePack.getFileName()) - || descriptionParts.anyMatch(predicate)) { - listView.getItems().add(item); - } - } + return queryPredicate.test(resourcePack.getFileNameWithExtension()) + || queryPredicate.test(resourcePack.getFileName()) || descriptionParts.anyMatch(queryPredicate); + }; + } catch (Throwable e) { + LOG.warning("Illegal regular expression", e); + return item -> true; } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 2a63223f498..44bad286040 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -19,7 +19,6 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; -import com.jfoenix.controls.JFXListView; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -53,6 +52,9 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.*; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -635,27 +637,40 @@ protected void updateItem(Item item, boolean empty) { private final class SchematicsPageSkin extends ToolbarListPageSkin { SchematicsPageSkin() { - super(SchematicsPage.this); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("schematics.empty")); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); + super(SchematicsPage.this, true); + + listView.setCellFactory(x -> new Cell()); + + setupSkin( + new Node[]{ + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, getSkinnable()::refresh), + createToolbarButton2(i18n("schematics.add"), SVG.ADD, getSkinnable()::onAddFiles), + createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, getSkinnable()::onCreateDirectory), + createToolbarButton2(i18n("search"), SVG.SEARCH, this::startSearch) + }, + null + ); } @Override - protected List initializeToolbar(SchematicsPage skinnable) { - return Arrays.asList( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("schematics.add"), SVG.ADD, skinnable::onAddFiles), - createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory) - ); + protected String getEmptyPlaceholderText() { + return i18n("schematics.empty"); } @Override - protected ListCell createListCell(JFXListView listView) { - return new Cell(); + protected Predicate updateSearchPredicate(String searchText) { + if (searchText == null || searchText.isEmpty()) return item -> true; + if (searchText.startsWith("regex:")) { + String regex = searchText.substring("regex:".length()); + try { + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + return item -> pattern.matcher(item.getName() + item.getDescription()).find(); + } catch (PatternSyntaxException e) { + return item -> false; + } + } else { + return item -> (item.getName() + item.getDescription()).toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT)); + } } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java index db03b7671c0..6fb443ed897 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java @@ -24,7 +24,6 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Control; -import javafx.scene.control.Label; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; import javafx.scene.layout.BorderPane; @@ -50,9 +49,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Arrays; import java.util.Comparator; -import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -162,25 +159,24 @@ void createBackup() { private final class WorldBackupsPageSkin extends ToolbarListPageSkin { WorldBackupsPageSkin() { - super(WorldBackupsPage.this); + super(WorldBackupsPage.this, false); - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("world.backup.empty")); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - } - - @Override - protected List initializeToolbar(WorldBackupsPage skinnable) { - JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup); + JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, getSkinnable()::createBackup); createBackup.disableProperty().bind(getSkinnable().readOnly); - return Arrays.asList( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createBackup + setupSkin( + new Node[] { + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, getSkinnable()::refresh), + createBackup + }, + null ); } + + @Override + protected String getEmptyPlaceholderText() { + return i18n("world.backup.empty"); + } } public final class BackupInfo extends Control implements Comparable { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java index 2c006a7948f..42f014ea3d6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java @@ -19,7 +19,6 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; -import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -27,7 +26,6 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.Skin; import javafx.scene.control.Tooltip; @@ -52,9 +50,12 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.time.Instant; -import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; @@ -216,31 +217,43 @@ public ReadOnlyBooleanProperty supportQuickPlayProperty() { private final class WorldListPageSkin extends ToolbarListPageSkin { WorldListPageSkin() { - super(WorldListPage.this); + super(WorldListPage.this, true); + listView.setCellFactory(x -> new WorldListCell(getSkinnable())); - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("world.empty")); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); + JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all")); + chkShowAll.selectedProperty().bindBidirectional(getSkinnable().showAllProperty()); + + setupSkin( + new Node[]{ + chkShowAll, + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, getSkinnable()::refresh), + createToolbarButton2(i18n("world.add"), SVG.ADD, getSkinnable()::add), + createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, getSkinnable()::download), + createToolbarButton2(i18n("search"), SVG.SEARCH, this::startSearch) + }, + null + ); } @Override - protected List initializeToolbar(WorldListPage skinnable) { - JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all")); - chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty()); - - return Arrays.asList( - chkShowAll, - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download) - ); + protected Predicate updateSearchPredicate(String searchText) { + if (searchText == null || searchText.isEmpty()) return item -> true; + if (searchText.startsWith("regex:")) { + String regex = searchText.substring("regex:".length()); + try { + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + return item -> pattern.matcher(item.getWorldName()).find(); + } catch (PatternSyntaxException e) { + return item -> false; + } + } else { + return item -> item.getWorldName().toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT)); + } } @Override - protected ListCell createListCell(JFXListView listView) { - return new WorldListCell(getSkinnable()); + protected String getEmptyPlaceholderText() { + return i18n("world.empty"); } } diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 6b2634f711b..bc906580402 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -993,15 +993,6 @@ -fx-text-fill: black; } -.jfx-list-cell .jfx-rippler { - -jfx-rippler-fill: -monet-primary-container; -} - -/*.list-cell:odd:selected > .jfx-rippler > StackPane,*/ -/*.list-cell:even:selected > .jfx-rippler > StackPane {*/ -/* -fx-background-color: derive(-monet-primary, 30%);*/ -/*}*/ - .jfx-list-view { -fx-background-insets: 0.0; -jfx-cell-horizontal-margin: 0.0;