Проблема, как отмечает @kleopatra, вызвана добавлением большого количества детей, когда выбран один или несколько элементов. Одним из способов решения этой проблемы является попытка реализовать собственный FocusModel
, так как источником проблемы по умолчанию является FocusModel
. Другой, и, на мой взгляд, более простой, способ создания обходного пути - это очистить выбор перед добавлением большой группы детей; после этого вы можете повторно выбрать ранее выбранные элементы.
То, как я это сделал, стреляло TreeModificationEvent
с пользовательскими EventType
с. Кроме того, я решил не переопределять isLeaf()
внутри моего ленивого TreeItem
. Мне проще использовать заполнитель TreeItem
, если родительский элемент TreeItem
является ленивой ветвью. Так как есть заполнитель, родитель автоматически зарегистрируется как ветвь.
Вот пример, который просматривает значение по умолчанию FileSystem
. Чтобы проверить, сработало ли решение, я создал каталог из 100 000 файлов и открыл его; не было никакого повешения для меня. Надеюсь, это означает, что это можно адаптировать к вашему коду.
Примечание. В этом примере удаляются дочерние элементы, когда ветка свернута, как вы делаете это в своем коде.
App.java
import java.nio.file.FileSystems;
import java.nio.file.Path;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.stage.Stage;
public class App extends Application {
private static String pathToString(Path p) {
if (p == null) {
return "null";
} else if (p.getFileName() == null) {
return p.toString();
}
return p.getFileName().toString();
}
@Override
public void start(Stage primaryStage) {
TreeView<Path> tree = new TreeView<>(new TreeItem<>());
tree.setShowRoot(false);
tree.setCellFactory(LazyTreeCell.forTreeView("Loading...", App::pathToString));
TreeViewUtils.installSelectionBugWorkaround(tree);
for (Path fsRoot : FileSystems.getDefault().getRootDirectories()) {
tree.getRoot().getChildren().add(new LoadingTreeItem<>(fsRoot, new DirectoryLoader(fsRoot)));
}
primaryStage.setScene(new Scene(tree, 800, 600));
primaryStage.show();
}
}
DirectoryLoader.java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import javafx.scene.control.TreeItem;
public class DirectoryLoader implements Callable<List<? extends TreeItem<Path>>> {
private static final Comparator<Path> COMPARATOR = (left, right) -> {
boolean leftIsDir = Files.isDirectory(left);
if (leftIsDir ^ Files.isDirectory(right)) {
return leftIsDir ? -1 : 1;
}
return left.compareTo(right);
};
private final Path directory;
public DirectoryLoader(Path directory) {
this.directory = directory;
}
@Override
public List<? extends TreeItem<Path>> call() throws Exception {
try (Stream<Path> stream = Files.list(directory)) {
return stream.sorted(COMPARATOR)
.map(this::toTreeItem)
.collect(Collectors.toList());
}
}
private TreeItem<Path> toTreeItem(Path path) {
return Files.isDirectory(path)
? new LoadingTreeItem<>(path, new DirectoryLoader(path))
: new TreeItem<>(path);
}
}
LoadingTreeItem.java
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.control.TreeItem;
public class LoadingTreeItem<T> extends TreeItem<T> {
private static final EventType<?> PRE_ADD_LOADED_CHILDREN
= new EventType<>(treeNotificationEvent(), "PRE_ADD_LOADED_CHILDREN");
private static final EventType<?> POST_ADD_LOADED_CHILDREN
= new EventType<>(treeNotificationEvent(), "POST_ADD_LOADED_CHILDREN");
@SuppressWarnings("unchecked")
static <T> EventType<TreeModificationEvent<T>> preAddLoadedChildrenEvent() {
return (EventType<TreeModificationEvent<T>>) PRE_ADD_LOADED_CHILDREN;
}
@SuppressWarnings("unchecked")
static <T> EventType<TreeModificationEvent<T>> postAddLoadedChildrenEvent() {
return (EventType<TreeModificationEvent<T>>) POST_ADD_LOADED_CHILDREN;
}
private final Callable<List<? extends TreeItem<T>>> callable;
private boolean needToLoadData = true;
private CompletableFuture<?> future;
public LoadingTreeItem(T value, Callable<List<? extends TreeItem<T>>> callable) {
super(value);
this.callable = callable;
super.getChildren().add(new TreeItem<>());
addExpandedListener();
}
@SuppressWarnings("unchecked")
private void addExpandedListener() {
expandedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
needToLoadData = true;
if (future != null) {
future.cancel(true);
}
super.getChildren().setAll(new TreeItem<>());
}
});
}
@Override
public ObservableList<TreeItem<T>> getChildren() {
if (needToLoadData) {
needToLoadData = false;
future = CompletableFuture.supplyAsync(new CallableToSupplierAdapter<>(callable))
.whenCompleteAsync(this::handleAsyncLoadComplete, Platform::runLater);
}
return super.getChildren();
}
private void handleAsyncLoadComplete(List<? extends TreeItem<T>> result, Throwable th) {
if (th != null) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), th);
} else {
Event.fireEvent(this, new TreeModificationEvent<>(preAddLoadedChildrenEvent(), this));
super.getChildren().setAll(result);
Event.fireEvent(this, new TreeModificationEvent<>(postAddLoadedChildrenEvent(), this));
}
future = null;
}
private static class CallableToSupplierAdapter<T> implements Supplier<T> {
private final Callable<T> callable;
private CallableToSupplierAdapter(Callable<T> callable) {
this.callable = callable;
}
@Override
public T get() {
try {
return callable.call();
} catch (Exception ex) {
throw new CompletionException(ex);
}
}
}
}
LazyTreeCell.java
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.util.Callback;
public class LazyTreeCell<T> extends TreeCell<T> {
public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView(String placeholderText,
Callback<? super T, String> toStringCallback) {
return tree -> new LazyTreeCell<>(placeholderText, toStringCallback);
}
private final String placeholderText;
private final Callback<? super T, String> toStringCallback;
public LazyTreeCell(String placeholderText, Callback<? super T, String> toStringCallback) {
this.placeholderText = placeholderText;
this.toStringCallback = toStringCallback;
}
/*
* Assumes that if "item" is null **and** the parent TreeItem is an instance of
* LoadingTreeItem that this is a "placeholder" cell.
*/
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (item == null && getTreeItem().getParent() instanceof LoadingTreeItem) {
setText(placeholderText);
} else {
setText(toStringCallback.call(item));
}
}
}
TreeViewUtils.java
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeItem.TreeModificationEvent;
import javafx.scene.control.TreeView;
public class TreeViewUtils {
public static <T> void installSelectionBugWorkaround(TreeView<T> tree) {
List<TreeItem<T>> selected = new ArrayList<>(0);
EventHandler<TreeModificationEvent<T>> preAdd = event -> {
event.consume();
selected.addAll(tree.getSelectionModel().getSelectedItems());
tree.getSelectionModel().clearSelection();
};
EventHandler<TreeModificationEvent<T>> postAdd = event -> {
event.consume();
selected.forEach(tree.getSelectionModel()::select);
selected.clear();
};
ChangeListener<TreeItem<T>> rootListener = (observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
oldValue.removeEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);
}
if (newValue != null) {
newValue.addEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
newValue.addEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);
}
};
rootListener.changed(tree.rootProperty(), null, tree.getRoot());
tree.rootProperty().addListener(rootListener);
}
private TreeViewUtils() {}
}
Как реализовано, служебный метод, который устанавливает обходной путь, привязан к вам с помощью LoadingTreeItem
s в TreeView
. Я не мог придумать хороший способ сделать решение достаточно общим, чтобы применить его к произвольному TreeView
; для этого, я полагаю, было бы необходимо создать пользовательский FocusModel
.
Вероятно, есть лучший способ реализовать LazyTreeCell
, используя класс для переноса реальных данных - как то, что вы делаете с Item
. Тогда у вас может быть фактический экземпляр placehoder Item
, который сообщает TreeCell
, что он является заполнителем, а не полагается на тип родительского элемента TreeItem
. На самом деле, моя реализация, вероятно, хрупкая.