From c11d3abe80f2671282e9df5dadc40ee2b064a427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Fournier?= Date: Mon, 28 Jan 2019 17:06:33 +0100 Subject: [PATCH] Treat node selection as events --- .../fxdesigner/MainDesignerController.java | 44 ++++++--- .../fxdesigner/NodeInfoPanelController.java | 41 ++++---- .../fxdesigner/SourceEditorController.java | 38 ++++---- .../util/fxdesigner/XPathPanelController.java | 33 +++++-- .../util/AbstractNodeSelectingController.java | 11 +++ .../util/CompositeSelectionSource.java | 50 ++++++++++ .../fxdesigner/util/NodeSelectionSource.java | 96 +++++++++++++++++++ .../fxdesigner/util/controls/AstTreeView.java | 40 ++++---- .../util/controls/NodeParentageCrumbBar.java | 30 +++--- 9 files changed, 288 insertions(+), 95 deletions(-) create mode 100644 pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/AbstractNodeSelectingController.java create mode 100644 pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/CompositeSelectionSource.java create mode 100644 pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/NodeSelectionSource.java diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java index ca232b6e95..407b0f8220 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/MainDesignerController.java @@ -17,7 +17,9 @@ import java.util.Stack; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.mutable.MutableInt; import org.reactfx.value.Val; +import org.reactfx.value.Var; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; @@ -25,14 +27,17 @@ import net.sourceforge.pmd.lang.symboltable.NameOccurrence; import net.sourceforge.pmd.util.fxdesigner.model.XPathEvaluationException; import net.sourceforge.pmd.util.fxdesigner.popups.EventLogController; import net.sourceforge.pmd.util.fxdesigner.util.AbstractController; +import net.sourceforge.pmd.util.fxdesigner.util.CompositeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; import net.sourceforge.pmd.util.fxdesigner.util.LimitedSizeStack; +import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.util.SoftReferenceCache; import net.sourceforge.pmd.util.fxdesigner.util.TextAwareNodeWrapper; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty; -import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; @@ -62,7 +67,7 @@ import javafx.stage.FileChooser; * @since 6.0.0 */ @SuppressWarnings("PMD.UnusedPrivateField") -public class MainDesignerController extends AbstractController { +public class MainDesignerController extends AbstractController implements CompositeSelectionSource { /** * Callback to the owner. @@ -145,8 +150,28 @@ public class MainDesignerController extends AbstractController { updateRecentFilesMenu(); refreshAST(); // initial refreshing sourceEditorController.moveCaret(0, 0); + + MutableInt mutableInt = new MutableInt(); + Var canProcessEvent = Var.newSimpleVar(true); + // .conditionOn(canProcessEvent) + // .distinct() + getSelectionEvents().hook(n -> System.out.println(mutableInt.incrementAndGet() + ": " + n)) + .subscribe(n -> { + canProcessEvent.setValue(false); + CompositeSelectionSource.super.select(n); +// onNodeItemSelected(n.getSelection()); + canProcessEvent.setValue(true); + }); + } + + @Override + public ObservableSet getComponents() { + return FXCollections.observableSet(nodeInfoPanelController, sourceEditorController, xpathPanelController); + } + + public void shutdown() { try { SettingsPersistenceUtil.persistProperties(this, DesignerUtil.getSettingsFile()); @@ -193,19 +218,8 @@ public class MainDesignerController extends AbstractController { * Executed when the user selects a node in a treeView or listView. */ public void onNodeItemSelected(Node selectedValue) { - onNodeItemSelected(selectedValue, false); - } - - - /** - * Executed when the user selects a node in a treeView or listView. - * - * @param isFromNameDecl Whether the node was selected in the scope hierarchy treeview - */ - public void onNodeItemSelected(Node selectedValue, boolean isFromNameDecl) { - // doing that in parallel speeds it up - Platform.runLater(() -> nodeInfoPanelController.setFocusNode(selectedValue, isFromNameDecl)); - Platform.runLater(() -> sourceEditorController.setFocusNode(selectedValue)); + nodeInfoPanelController.setFocusNode(selectedValue); + sourceEditorController.setFocusNode(selectedValue); } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java index c11855687d..1ea5630389 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/NodeInfoPanelController.java @@ -17,6 +17,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.reactfx.EventStream; import org.reactfx.EventStreams; import org.reactfx.value.Var; @@ -30,6 +31,7 @@ import net.sourceforge.pmd.lang.symboltable.Scope; import net.sourceforge.pmd.lang.symboltable.ScopedNode; import net.sourceforge.pmd.util.fxdesigner.model.MetricResult; import net.sourceforge.pmd.util.fxdesigner.util.AbstractController; +import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty; import net.sourceforge.pmd.util.fxdesigner.util.controls.ScopeHierarchyTreeCell; import net.sourceforge.pmd.util.fxdesigner.util.controls.ScopeHierarchyTreeItem; @@ -55,7 +57,7 @@ import javafx.scene.control.TreeView; * @since 6.0.0 */ @SuppressWarnings("PMD.UnusedPrivateField") -public class NodeInfoPanelController extends AbstractController { +public class NodeInfoPanelController extends AbstractController implements NodeSelectionSource { private final MainDesignerController parent; @@ -91,13 +93,6 @@ public class NodeInfoPanelController extends AbstractController { xpathAttributesListView.setPlaceholder(new Label("No available attributes")); - EventStreams.valuesOf(scopeHierarchyTreeView.getSelectionModel().selectedItemProperty()) - .filter(Objects::nonNull) - .map(TreeItem::getValue) - .filterMap(o -> o instanceof NameDeclaration, o -> (NameDeclaration) o) - .filter(nd -> !Objects.equals(nd.getNode(), selectedNode)) - .subscribe(declaration -> Platform.runLater(() -> parent.onNodeItemSelected(declaration.getNode(), true))); - scopeHierarchyTreeView.setCellFactory(view -> new ScopeHierarchyTreeCell()); hideCommonAttributesProperty() @@ -108,20 +103,25 @@ public class NodeInfoPanelController extends AbstractController { } - /**
{@linkplain #setFocusNode(Node, boolean) setFocusNode}(node, false)
*/ - public void setFocusNode(Node node) { - setFocusNode(node, false); + @Override + public EventStream getSelectionEvents() { + return EventStreams.valuesOf(scopeHierarchyTreeView.getSelectionModel().selectedItemProperty()) + .filter(Objects::nonNull) + .map(TreeItem::getValue) + .filterMap(o -> o instanceof NameDeclaration, o -> (NameDeclaration) o) + .map(NameDeclaration::getNode) + .map(n -> new NodeSelectionEvent(n, this)); + } /** * Displays info about a node. If null, the panels are reset. * - * @param node Node to inspect - * @param isFromNameDecl Whether the node was selected in the scope hierarchy treeview. - * If so we'll attempt to preserve that selection. + * @param node Node to inspect */ - public void setFocusNode(Node node, boolean isFromNameDecl) { + @Override + public void setFocusNode(Node node) { if (node == null) { invalidateInfo(); return; @@ -132,15 +132,14 @@ public class NodeInfoPanelController extends AbstractController { } selectedNode = node; - displayAttributes(node); - displayMetrics(node); - displayScopes(node, isFromNameDecl); + Platform.runLater(() -> displayAttributes(node)); + Platform.runLater(() -> displayMetrics(node)); + displayScopes(node); if (node instanceof ScopedNode) { // not null as well highlightNameOccurences((ScopedNode) node); } - } @@ -198,7 +197,7 @@ public class NodeInfoPanelController extends AbstractController { } - private void displayScopes(Node node, boolean focusScopeView) { + private void displayScopes(Node node) { // current selection TreeItem previousSelection = scopeHierarchyTreeView.getSelectionModel().getSelectedItem(); @@ -206,7 +205,7 @@ public class NodeInfoPanelController extends AbstractController { ScopeHierarchyTreeItem rootScope = ScopeHierarchyTreeItem.buildAscendantHierarchy(node); scopeHierarchyTreeView.setRoot(rootScope); - if (focusScopeView && previousSelection != null) { + if (previousSelection != null) { // Try to find the node that was previously selected and focus it in the new ascendant hierarchy. // Otherwise, when you select a node in the scope tree, since focus of the app is shifted to that // node, the scope hierarchy is reset and you lose the selection - even though obviously the node diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java index 7b97fc7a39..9a3076334a 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/SourceEditorController.java @@ -33,7 +33,9 @@ import net.sourceforge.pmd.util.fxdesigner.model.ASTManager; import net.sourceforge.pmd.util.fxdesigner.model.ParseAbortedException; import net.sourceforge.pmd.util.fxdesigner.popups.AuxclasspathSetupController; import net.sourceforge.pmd.util.fxdesigner.util.AbstractController; +import net.sourceforge.pmd.util.fxdesigner.util.CompositeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; +import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.util.TextAwareNodeWrapper; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty; import net.sourceforge.pmd.util.fxdesigner.util.codearea.AvailableSyntaxHighlighters; @@ -45,6 +47,8 @@ import net.sourceforge.pmd.util.fxdesigner.util.controls.NodeParentageCrumbBar; import net.sourceforge.pmd.util.fxdesigner.util.controls.ToolbarTitledPane; import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; import javafx.css.PseudoClass; import javafx.fxml.FXML; import javafx.scene.control.MenuButton; @@ -58,7 +62,7 @@ import javafx.scene.control.ToggleGroup; * @author Clément Fournier * @since 6.0.0 */ -public class SourceEditorController extends AbstractController { +public class SourceEditorController extends AbstractController implements CompositeSelectionSource { private static final Duration AST_REFRESH_DELAY = Duration.ofMillis(100); @@ -132,21 +136,12 @@ public class SourceEditorController extends AbstractController { codeEditorArea.setParagraphGraphicFactory(lineNumberFactory()); - astTreeView.onNodeClickedHandlerProperty().setValue(parent::onNodeItemSelected); - } @Override protected void afterParentInit() { DesignerUtil.rewire(astManager.languageVersionProperty(), languageVersionUIProperty); - - // Focus the crumb - focusNodeParentageCrumbBar.setOnRegularCrumbAction(treeitem -> { - if (treeitem != null && treeitem.getValue() != null) { - astTreeView.focusNode(treeitem.getValue()); - } - }); } @@ -196,6 +191,12 @@ public class SourceEditorController extends AbstractController { } + @Override + public ObservableSet getComponents() { + return FXCollections.observableSet(astTreeView, focusNodeParentageCrumbBar); + } + + /** * Refreshes the AST and returns the new compilation unit if the parse didn't fail. */ @@ -263,20 +264,21 @@ public class SourceEditorController extends AbstractController { * Highlights the given node (or nothing if null). * Removes highlighting on the previously highlighted node. */ + @Override public void setFocusNode(Node node) { - if (Objects.equals(node, currentFocusNode.getValue())) { - return; - } - - codeEditorArea.styleNodes(node == null ? emptyList() : singleton(node), StyleLayerIds.FOCUS, true); - + // editor is always scrolled when re-selecting a node if (node != null) { Platform.runLater(() -> scrollEditorToNode(node)); } + if (Objects.equals(node, currentFocusNode.getValue())) { + return; + } + currentFocusNode.setValue(node); - Platform.runLater(() -> astTreeView.focusNode(node)); - focusNodeParentageCrumbBar.setFocusNode(node); + + // editor is only restyled if the selection has changed + Platform.runLater(() -> codeEditorArea.styleNodes(node == null ? emptyList() : singleton(node), StyleLayerIds.FOCUS, true)); } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java index 4bea9c0c9b..eedd187f14 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/XPathPanelController.java @@ -17,6 +17,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.Validator; +import org.reactfx.EventStream; import org.reactfx.EventStreams; import org.reactfx.collection.LiveArrayList; import org.reactfx.value.Val; @@ -34,6 +35,7 @@ import net.sourceforge.pmd.util.fxdesigner.model.XPathEvaluator; import net.sourceforge.pmd.util.fxdesigner.popups.ExportXPathWizardController; import net.sourceforge.pmd.util.fxdesigner.util.AbstractController; import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; +import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.util.TextAwareNodeWrapper; import net.sourceforge.pmd.util.fxdesigner.util.autocomplete.CompletionResultSource; import net.sourceforge.pmd.util.fxdesigner.util.autocomplete.XPathAutocompleteProvider; @@ -74,7 +76,7 @@ import javafx.stage.StageStyle; * @see ExportXPathWizardController * @since 6.0.0 */ -public class XPathPanelController extends AbstractController { +public class XPathPanelController extends AbstractController implements NodeSelectionSource { private static final Duration XPATH_REFRESH_DELAY = Duration.ofMillis(100); private final DesignerRoot designerRoot; @@ -123,12 +125,6 @@ public class XPathPanelController extends AbstractController { exportXpathToRuleButton.setOnAction(e -> showExportXPathToRuleWizard()); - EventStreams.valuesOf(xpathResultListView.getSelectionModel().selectedItemProperty()) - .conditionOn(xpathResultListView.focusedProperty()) - .filter(Objects::nonNull) - .map(TextAwareNodeWrapper::getNode) - .subscribe(parent::onNodeItemSelected); - xpathExpressionArea.richChanges() .filter(t -> !t.isIdentity()) .successionEnds(XPATH_REFRESH_DELAY) @@ -226,6 +222,23 @@ public class XPathPanelController extends AbstractController { } + @Override + public EventStream getSelectionEvents() { + return EventStreams.valuesOf(xpathResultListView.getSelectionModel().selectedItemProperty()) + .conditionOn(xpathResultListView.focusedProperty()) + .filter(Objects::nonNull) + .map(TextAwareNodeWrapper::getNode) + .map(n -> new NodeSelectionEvent(n, this)); + } + + + @Override + public void setFocusNode(Node node) { + xpathResultListView.getItems().stream() + .filter(wrapper -> wrapper.getNode().equals(node)) + .findFirst() + .ifPresent(xpathResultListView.getSelectionModel()::select); + } /** @@ -256,10 +269,10 @@ public class XPathPanelController extends AbstractController { parent.highlightXPathResults(results); violationsTitledPane.setTitle("Matched nodes (" + results.size() + ")"); // Notify that everything went OK so we can avoid logging very recent exceptions - designerRoot.getLogger().logEvent(new LogEntry(null, Category.XPATH_OK)); + designerRoot.getLogger().logEvent(LogEntry.createExceptionEntry(null, Category.XPATH_OK)); } catch (XPathEvaluationException e) { invalidateResults(true); - designerRoot.getLogger().logEvent(new LogEntry(e, Category.XPATH_EVALUATION_EXCEPTION)); + designerRoot.getLogger().logEvent(LogEntry.createExceptionEntry(e, Category.XPATH_EVALUATION_EXCEPTION)); } } @@ -341,4 +354,6 @@ public class XPathPanelController extends AbstractController { public List getChildrenSettingsNodes() { return Collections.singletonList(getRuleBuilder()); } + + } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/AbstractNodeSelectingController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/AbstractNodeSelectingController.java new file mode 100644 index 0000000000..557dfe34f4 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/AbstractNodeSelectingController.java @@ -0,0 +1,11 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util; + +/** + * @author Clément Fournier + */ +public class AbstractNodeSelectingController { +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/CompositeSelectionSource.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/CompositeSelectionSource.java new file mode 100644 index 0000000000..3f1e40862f --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/CompositeSelectionSource.java @@ -0,0 +1,50 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util; + +import org.reactfx.EventStream; +import org.reactfx.EventStreams; + +import net.sourceforge.pmd.lang.ast.Node; + +import javafx.collections.ObservableSet; + + +/** + * @author Clément Fournier + */ +public interface CompositeSelectionSource extends NodeSelectionSource { + + + ObservableSet getComponents(); + + + @Override + default EventStream getSelectionEvents() { + return EventStreams.merge(getComponents(), NodeSelectionSource::getSelectionEvents); + } + + + @Override + default void setFocusNode(Node node) { + // by default do nothing, + // maybe it should only be handled by the components + } + + + @Override + default void select(NodeSelectionEvent selectionEvent) { + System.out.println("\t" + this.getClass().getSimpleName() + " handling " + selectionEvent); + for (NodeSelectionSource source : getComponents()) { + if (!selectionEvent.getOrigin().equals(source)) { + source.select(selectionEvent); + } + } + + if (!this.equals(selectionEvent.getOrigin())) { + setFocusNode(selectionEvent.getSelection()); + } + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/NodeSelectionSource.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/NodeSelectionSource.java new file mode 100644 index 0000000000..e16b39d6e3 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/NodeSelectionSource.java @@ -0,0 +1,96 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.util; + +import java.util.Objects; + +import org.reactfx.EventStream; + +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.util.fxdesigner.MainDesignerController; +import net.sourceforge.pmd.util.fxdesigner.util.controls.AstTreeView; +import net.sourceforge.pmd.util.fxdesigner.util.controls.NodeParentageCrumbBar; + + +/** + * A control or controller that has the ability to push node selection events. + * When a node is selected in the control (e.g. {@link AstTreeView}, {@link NodeParentageCrumbBar}, etc), + * the whole UI is synchronized to the node. Selection events are merged iteratively into + * a global stream for the whole app. Events from that stream are handled by {@link MainDesignerController}. + * + * @author Clément Fournier + */ +public interface NodeSelectionSource { + + /** + * Returns a stream of nodes that pushes an event every time + * this control records a *user* change in selection. + */ + + EventStream getSelectionEvents(); + + + default void select(NodeSelectionEvent selectionEvent) { + System.out.println("\t\t" + this.getClass().getSimpleName() + " handling " + selectionEvent); + if (selectionEvent.getOrigin() != this) { + setFocusNode(selectionEvent.getSelection()); + } else { + System.out.println("\tUnhandled"); + } + } + + + void setFocusNode(Node node); + + + final class NodeSelectionEvent { + + private final Node selection; + private final NodeSelectionSource origin; + + + public NodeSelectionEvent(Node selection, NodeSelectionSource origin) { + this.selection = selection; + this.origin = origin; + } + + + public Node getSelection() { + return selection; + } + + + public NodeSelectionSource getOrigin() { + return origin; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NodeSelectionEvent that = (NodeSelectionEvent) o; + return Objects.equals(selection, that.selection) && + Objects.equals(origin, that.origin); + } + + + @Override + public int hashCode() { + return Objects.hash(selection, origin); + } + + + @Override + public String toString() { + return getSelection().getXPathNodeName() + "(" + hashCode() + ")\t\tfrom " + getOrigin().getClass().getSimpleName(); + } + } + +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/AstTreeView.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/AstTreeView.java index bf1a3668f6..6165a73f59 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/AstTreeView.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/AstTreeView.java @@ -8,15 +8,16 @@ import static net.sourceforge.pmd.internal.util.IteratorUtil.toIterable; import static net.sourceforge.pmd.util.fxdesigner.util.DesignerIteratorUtil.parentIterator; import java.util.Objects; -import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; +import org.reactfx.EventSource; +import org.reactfx.EventStream; import org.reactfx.EventStreams; -import org.reactfx.Subscription; import org.reactfx.value.Var; import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource; import javafx.scene.control.SelectionModel; import javafx.scene.control.TreeItem; @@ -27,38 +28,43 @@ import javafx.scene.control.TreeView; * @author Clément Fournier * @since 7.0.0 */ -public class AstTreeView extends TreeView { +public class AstTreeView extends TreeView implements NodeSelectionSource { private final Var> onNodeClickedHandler = Var.newSimpleVar(n -> {}); private final TreeViewWrapper myWrapper = new TreeViewWrapper<>(this); - private Subscription myNodeSelectedSub; private ASTTreeItem selectedTreeItem; - + private final EventSource selectionEvents = new EventSource<>(); public AstTreeView() { + // push a node selection event whenever... + // * The selection changes + EventStreams.valuesOf(getSelectionModel().selectedItemProperty()) + .filterMap(Objects::nonNull, TreeItem::getValue) + .subscribe(selectionEvents::push); + // * a cell is explicitly clicked. This catches the case where the cell was already selected + setCellFactory(tv -> new ASTTreeCell(n -> { + // only push an event if the node was already selected + if (selectedTreeItem != null && selectedTreeItem.getValue() != null && selectedTreeItem.getValue().equals(n)) { + selectionEvents.push(n); + } + })); - onNodeClickedHandler.values() - .subscribe(handler -> { - setCellFactory(tv -> new ASTTreeCell(handler)); - - Optional.ofNullable(myNodeSelectedSub).ifPresent(Subscription::unsubscribe); - - myNodeSelectedSub = EventStreams.valuesOf(getSelectionModel().selectedItemProperty()) - .filterMap(Objects::nonNull, TreeItem::getValue) - .subscribe(handler); - - }); + } + @Override + public EventStream getSelectionEvents() { + return selectionEvents.map(n -> new NodeSelectionEvent(n, this)); } /** * Focus the given node, handling scrolling if needed. */ - public void focusNode(Node node) { + @Override + public void setFocusNode(Node node) { SelectionModel> selectionModel = getSelectionModel(); if (selectedTreeItem == null && node != null diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeParentageCrumbBar.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeParentageCrumbBar.java index ad39de017c..cf48d77f2f 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeParentageCrumbBar.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeParentageCrumbBar.java @@ -9,13 +9,15 @@ import static net.sourceforge.pmd.internal.util.IteratorUtil.asReversed; import static net.sourceforge.pmd.internal.util.IteratorUtil.count; import static net.sourceforge.pmd.util.fxdesigner.util.DesignerIteratorUtil.parentIterator; -import java.util.function.Consumer; import java.util.function.Function; import org.controlsfx.control.BreadCrumbBar; +import org.reactfx.EventSource; +import org.reactfx.EventStream; import org.reactfx.value.Val; import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource; import javafx.application.Platform; import javafx.css.PseudoClass; @@ -35,7 +37,7 @@ import javafx.util.Callback; * @author Clément Fournier * @since 7.0.0 */ -public class NodeParentageCrumbBar extends BreadCrumbBar { +public class NodeParentageCrumbBar extends BreadCrumbBar implements NodeSelectionSource { private static final int DEFAULT_PX_BY_CHAR = 5; private static final int DEFAULT_CONSTANT_PADDING = 19; @@ -44,7 +46,7 @@ public class NodeParentageCrumbBar extends BreadCrumbBar { private final TreeItem ellipsisCrumb = new TreeItem<>(null); /** number of nodes currently behind the ellipsis */ private int numElidedNodes = 0; - + private final EventSource selectionEvents = new EventSource<>(); public NodeParentageCrumbBar() { // This allows to click on a parent crumb and keep the children crumb @@ -53,6 +55,12 @@ public class NodeParentageCrumbBar extends BreadCrumbBar { // captured in the closure final Callback, Button> originalCrumbFactory = getCrumbFactory(); + setOnCrumbAction(ev -> { + if (ev.getSelectedCrumb() != ellipsisCrumb) { + selectionEvents.push(ev.getSelectedCrumb().getValue()); + } + }); + setCrumbFactory(item -> { Button button = originalCrumbFactory.call(item); if (item == ellipsisCrumb) { @@ -74,18 +82,9 @@ public class NodeParentageCrumbBar extends BreadCrumbBar { } - /** - * Set a handler that executes when the user selects a crumb other than the ellipsis. - * This shouldn't be calling {@link #setFocusNode(Node)} on the same node otherwise - * the crumb bar will set the deepest node to the node and the children won't be - * available. - */ - public void setOnRegularCrumbAction(Consumer> handler) { - setOnCrumbAction(e -> { - if (e.getSelectedCrumb() != ellipsisCrumb) { - handler.accept(e.getSelectedCrumb()); - } - }); + @Override + public EventStream getSelectionEvents() { + return selectionEvents.map(n -> new NodeSelectionEvent(n, this)); } // getSelectedCrumb gets the deepest displayed node @@ -96,6 +95,7 @@ public class NodeParentageCrumbBar extends BreadCrumbBar { * sets the focus on it. Otherwise, sets the node to be * the deepest one of the crumb bar. Noop if node is null. */ + @Override public void setFocusNode(Node node) { if (node == null) { return;