diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java index eb38da4d7..f01875701 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.util.ReflectUtil; +import software.coley.recaf.util.Unchecked; import java.lang.reflect.Field; import java.util.Collections; @@ -33,6 +34,7 @@ public class FilterableTreeItem extends TreeItem { private static final Field CHILDREN_FIELD; private static final Logger logger = Logging.get(FilterableTreeItem.class); private final ObservableList> sourceChildren = FXCollections.observableArrayList(); + private final ObjectProperty> sourceParent = new SimpleObjectProperty<>(); private final ObjectProperty>> predicate = new SimpleObjectProperty<>(); protected FilterableTreeItem() { @@ -62,6 +64,14 @@ public ObservableList> getChildren() { return super.getChildren(); } + /** + * @return Source parent, ignoring filtering. + */ + @Nonnull + public ObjectProperty> sourceParentProperty() { + return sourceParent; + } + /** * @return {@code true} when the item MUST be shown. */ @@ -135,6 +145,8 @@ public void addAndSortChild(@Nonnull TreeItem item) { index = -(index + 1); sourceChildren.add(index, item); } + if (item instanceof FilterableTreeItem filterableItem) + filterableItem.sourceParent.set(Unchecked.cast(this)); } } @@ -146,6 +158,8 @@ public void addAndSortChild(@Nonnull TreeItem item) { */ protected void addPreSortedChild(@Nonnull TreeItem item) { sourceChildren.add(item); + if (item instanceof FilterableTreeItem filterableItem) + filterableItem.sourceParent.set(Unchecked.cast(this)); } /** @@ -159,6 +173,8 @@ protected void addPreSortedChild(@Nonnull TreeItem item) { */ public boolean removeSourceChild(@Nonnull TreeItem child) { synchronized (sourceChildren) { + if (child instanceof FilterableTreeItem filterableItem) + filterableItem.sourceParent.set(null); return sourceChildren.remove(child); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java index 20fbd38f0..ce13f2f99 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeFiltering.java @@ -1,5 +1,6 @@ package software.coley.recaf.ui.control.tree; +import jakarta.annotation.Nonnull; import javafx.scene.control.TextField; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; @@ -24,7 +25,7 @@ public class TreeFiltering { * Assumed that tree contents are {@link FilterableTreeItem}. */ @SuppressWarnings({"unchecked", "rawtypes"}) - public static void install(TextField filter, TreeView tree) { + public static void install(@Nonnull TextField filter, @Nonnull TreeView tree) { NodeEvents.addKeyPressHandler(filter, e -> { if (e.getCode() == KeyCode.ESCAPE) { filter.clear(); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java index 6de0e3836..ac4b23b8b 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java @@ -1,6 +1,7 @@ package software.coley.recaf.ui.control.tree; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; @@ -16,7 +17,7 @@ public class TreeItems { * Expand all parents to this item. */ public static void expandParents(@Nonnull TreeItem item) { - while ((item = item.getParent()) != null) + while ((item = getParent(item)) != null) item.setExpanded(true); } @@ -62,4 +63,18 @@ private static void recurseClose(@Nonnull TreeItem item) { item.getChildren().forEach(TreeItems::recurseClose); } } + + /** + * @param item + * Tree item to get parent of. + * + * @return Parent tree item. + */ + @Nullable + private static TreeItem getParent(@Nonnull TreeItem item) { + TreeItem parent = item.getParent(); + if (parent == null && item instanceof FilterableTreeItem filterableItem) + parent = filterableItem.sourceParentProperty().get(); + return parent; + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java index 90dcab76e..dafc3cd6d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeFilterPane.java @@ -1,13 +1,18 @@ package software.coley.recaf.ui.control.tree; +import atlantafx.base.controls.CustomTextField; +import atlantafx.base.theme.Styles; import jakarta.annotation.Nonnull; +import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; +import org.kordamp.ikonli.carbonicons.CarbonIcons; import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.path.DirectoryPathNode; import software.coley.recaf.path.FilePathNode; import software.coley.recaf.path.PathNode; -import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.ui.control.BoundToggleIcon; +import software.coley.recaf.ui.control.FontIconView; import software.coley.recaf.util.Lang; /** @@ -16,45 +21,54 @@ * @author Matt Coley */ public class WorkspaceTreeFilterPane extends BorderPane { - private final TextField textField = new TextField(); + private final SimpleBooleanProperty caseSensitivity = new SimpleBooleanProperty(false); + private final CustomTextField textField = new CustomTextField(); /** * @param tree * Tree to filter. */ public WorkspaceTreeFilterPane(@Nonnull WorkspaceTree tree) { + BoundToggleIcon toggleSensitivity = new BoundToggleIcon(new FontIconView(CarbonIcons.LETTER_CC), caseSensitivity).withTooltip("misc.casesensitive"); + toggleSensitivity.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.ACCENT, Styles.FLAT, Styles.SMALL); + textField.rightProperty().set(toggleSensitivity); + textField.promptTextProperty().bind(Lang.getBinding("workspace.filter-prompt")); setCenter(textField); getStyleClass().add("workspace-filter-pane"); textField.getStyleClass().add("workspace-filter-text"); - // TODO: - // - option to hide supporting resources - // - case sensitivity toggle - // Setup tree item predicate property on FX thread. - // The root is assigned on the FX thread, it won't be available if we call it immediately. - FxThreadUtil.run(() -> { - // We're not binding from the root's property since that will trigger immediately. - // That will force-expand the entire workspace, which we do not want to do. - textField.textProperty().addListener((ob, old, cur) -> { - WorkspaceTreeNode root = (WorkspaceTreeNode) tree.getRoot(); - root.predicateProperty().set(item -> { - String path; - PathNode node = item.getValue(); - if (node instanceof DirectoryPathNode directoryNode) { - path = directoryNode.getValue(); - } else if (node instanceof ClassPathNode classPathNode) { - path = classPathNode.getValue().getName(); - } else if (node instanceof FilePathNode classPathNode) { - path = classPathNode.getValue().getName(); - } else { - path = null; - } - return path == null || path.contains(cur); - }); + textField.textProperty().addListener((ob, old, cur) -> update(tree)); + caseSensitivity.addListener((ob, old, cur) -> update(tree)); + } + + private void update(@Nonnull WorkspaceTree tree) { + WorkspaceTreeNode root = (WorkspaceTreeNode) tree.getRoot(); + if (root == null) return; + + if (textField.getText().isEmpty()) + root.predicateProperty().set(null); + else + root.predicateProperty().set(item -> { + String path; + PathNode node = item.getValue(); + if (node instanceof DirectoryPathNode directoryNode) { + path = directoryNode.getValue(); + } else if (node instanceof ClassPathNode classPathNode) { + path = classPathNode.getValue().getName(); + } else if (node instanceof FilePathNode classPathNode) { + path = classPathNode.getValue().getName(); + } else { + path = null; + } + + if (path == null) return true; + + return caseSensitivity.get() ? + path.contains(textField.getText()) : + path.toLowerCase().contains(textField.getText().toLowerCase()); }); - }); } /**