mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-01-12 14:14:52 +08:00
* close #2043: Make ModListPage searchable * Optimize layout --------- Co-authored-by: Cyenoch <cyenoch@qq.com>
This commit is contained in:
parent
bb66be7c3a
commit
9a3b8545ca
@ -20,7 +20,9 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
@ -34,6 +36,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.util.Debouncer;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@ -113,6 +116,30 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}, Platform::runLater);
|
||||
}
|
||||
|
||||
public FilteredList<ModListPageSkin.ModInfoObject> getFilteredItems(StringProperty queryStringProperty) {
|
||||
FilteredList<ModListPageSkin.ModInfoObject> filteredList = new FilteredList<>(getItems());
|
||||
Debouncer<Integer> searchFieldDebouncer = new Debouncer<>((key) -> runInFX(() -> {
|
||||
String searchText = queryStringProperty.get();
|
||||
if (searchText.isEmpty())
|
||||
filteredList.setPredicate((item) -> true);
|
||||
else
|
||||
filteredList.setPredicate((item) -> {
|
||||
LocalModFile modInfo = item.getModInfo();
|
||||
if (searchText.startsWith("$:")) {
|
||||
// Use regular matching pattern.
|
||||
try {
|
||||
return modInfo.getFileName().matches(searchText.substring(2));
|
||||
} catch (Exception exception) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return modInfo.getFileName().toLowerCase().contains(searchText.toLowerCase());
|
||||
});
|
||||
}), 400);
|
||||
FXUtils.onChangeAndOperate(queryStringProperty, (text) -> searchFieldDebouncer.call(1));
|
||||
return filteredList;
|
||||
}
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.choose_mod"));
|
||||
|
@ -23,6 +23,7 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
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;
|
||||
@ -59,7 +60,7 @@ import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@ -78,6 +79,31 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
JFXListView<ModInfoObject> listView = new JFXListView<>();
|
||||
JFXTextField searchField = new JFXTextField();
|
||||
|
||||
{
|
||||
HBox searchBar = new HBox();
|
||||
searchBar.setAlignment(Pos.BASELINE_CENTER);
|
||||
searchBar.setPadding(new Insets(8, 8, 8, 8));
|
||||
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
searchField.setPromptText(i18n("search"));
|
||||
|
||||
JFXButton clearBtn = new JFXButton();
|
||||
clearBtn.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
|
||||
clearBtn.setOnMouseClicked((event) -> {
|
||||
searchField.textProperty().set("");
|
||||
});
|
||||
Node clearBtnWrapped = wrapMargin(clearBtn, new Insets(0, 0, 0, 9));
|
||||
FXUtils.onChangeAndOperate(searchField.textProperty(), (text) -> {
|
||||
if (text.isEmpty() && searchBar.getChildren().contains(clearBtnWrapped))
|
||||
searchBar.getChildren().remove(clearBtnWrapped);
|
||||
else if (!searchBar.getChildren().contains(clearBtnWrapped))
|
||||
searchBar.getChildren().add(clearBtnWrapped);
|
||||
});
|
||||
searchBar.getChildren().setAll(searchField);
|
||||
root.getContent().add(searchBar);
|
||||
}
|
||||
|
||||
{
|
||||
TransitionPane toolBarPane = new TransitionPane();
|
||||
@ -87,7 +113,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
|
||||
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("mods.check_updates"), SVG::update, skinnable::checkUpdates),
|
||||
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download));
|
||||
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download)
|
||||
);
|
||||
HBox toolbarSelecting = new HBox();
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
|
||||
@ -122,7 +149,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView, lastCell));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getFilteredItems(searchField.textProperty()));
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
|
74
HMCL/src/main/java/org/jackhuang/hmcl/util/Debouncer.java
Normal file
74
HMCL/src/main/java/org/jackhuang/hmcl/util/Debouncer.java
Normal file
@ -0,0 +1,74 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Debouncer<T> {
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<>();
|
||||
private final Callback<T> callback;
|
||||
private final int interval;
|
||||
|
||||
public Debouncer(Callback<T> c, int interval) {
|
||||
this.callback = c;
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
public void call(T key) {
|
||||
TimerTask task = new TimerTask(key);
|
||||
|
||||
TimerTask prev;
|
||||
do {
|
||||
prev = delayedMap.putIfAbsent(key, task);
|
||||
if (prev == null) scheduler.schedule(task, interval, TimeUnit.MILLISECONDS);
|
||||
} while (prev != null && !prev.extend()); // Exit only if new task was added to map, or existing task was extended successfully
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
|
||||
// The task that wakes up when the wait time elapses
|
||||
private class TimerTask implements Runnable {
|
||||
private final T key;
|
||||
private long dueTime;
|
||||
private final Object lock = new Object();
|
||||
|
||||
public TimerTask(T key) {
|
||||
this.key = key;
|
||||
extend();
|
||||
}
|
||||
|
||||
public boolean extend() {
|
||||
synchronized (lock) {
|
||||
if (dueTime < 0) // Task has been shutdown
|
||||
return false;
|
||||
dueTime = System.currentTimeMillis() + interval;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
synchronized (lock) {
|
||||
long remaining = dueTime - System.currentTimeMillis();
|
||||
if (remaining > 0) { // Re-schedule task
|
||||
scheduler.schedule(this, remaining, TimeUnit.MILLISECONDS);
|
||||
} else { // Mark as terminated and invoke callback
|
||||
dueTime = -1;
|
||||
try {
|
||||
callback.call(key);
|
||||
} finally {
|
||||
delayedMap.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback<T> {
|
||||
void call(T t);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user