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 e9393b3043..dc6e222afa 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 @@ -25,7 +25,6 @@ import org.reactfx.value.Val; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.util.fxdesigner.app.AbstractController; -import net.sourceforge.pmd.util.fxdesigner.app.CompositeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.app.DesignerRoot; import net.sourceforge.pmd.util.fxdesigner.app.NodeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.model.XPathEvaluationException; @@ -37,8 +36,6 @@ 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.collections.FXCollections; -import javafx.collections.ObservableSet; import javafx.fxml.FXML; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; @@ -48,7 +45,6 @@ import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; -import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.stage.FileChooser; @@ -64,7 +60,7 @@ import javafx.stage.FileChooser; * @since 6.0.0 */ @SuppressWarnings("PMD.UnusedPrivateField") -public class MainDesignerController extends AbstractController> implements CompositeSelectionSource { +public class MainDesignerController extends AbstractController> { /* Menu bar */ @@ -117,7 +113,7 @@ public class MainDesignerController extends AbstractController showLicensePopup()); @@ -146,17 +142,9 @@ public class MainDesignerController extends AbstractController CompositeSelectionSource.super.bubbleDown(n)); - } - @Override - public ObservableSet getSubSelectionSources() { - return FXCollections.observableSet(nodeInfoPanelController, sourceEditorController, xpathPanelController); - } public void shutdown() { try { @@ -290,7 +278,7 @@ public class MainDesignerController extends AbstractController getSelectionEvents() { + public EventStream getSelectionEvents() { return myScopeItemSelectionEvents.filter(Objects::nonNull) .map(TreeItem::getValue) .filterMap(o -> o instanceof NameDeclaration, o -> (NameDeclaration) o) - .map(NameDeclaration::getNode) - .map(n -> new NodeSelectionEvent(n, this)); + .map(NameDeclaration::getNode); } @@ -133,15 +132,14 @@ public class NodeInfoPanelController extends AbstractController displayAttributes(node)); - Platform.runLater(() -> displayMetrics(node)); + displayAttributes(node); + displayMetrics(node); displayScopes(node); } private void displayAttributes(Node node) { - ObservableList atts = getAttributes(node); - xpathAttributesListView.setItems(atts); + xpathAttributesListView.setItems(getAttributes(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 336c56e80f..be8dfa2495 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 @@ -23,8 +23,6 @@ import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.util.ClasspathClassLoader; import net.sourceforge.pmd.util.fxdesigner.app.AbstractController; -import net.sourceforge.pmd.util.fxdesigner.app.CompositeSelectionSource; -import net.sourceforge.pmd.util.fxdesigner.app.NodeSelectionSource; import net.sourceforge.pmd.util.fxdesigner.model.ASTManager; import net.sourceforge.pmd.util.fxdesigner.model.ParseAbortedException; import net.sourceforge.pmd.util.fxdesigner.popups.AuxclasspathSetupController; @@ -37,8 +35,6 @@ import net.sourceforge.pmd.util.fxdesigner.util.controls.NodeEditionCodeArea; import net.sourceforge.pmd.util.fxdesigner.util.controls.ToolbarTitledPane; import javafx.application.Platform; -import javafx.collections.FXCollections; -import javafx.collections.ObservableSet; import javafx.fxml.FXML; import javafx.scene.control.MenuButton; import javafx.scene.control.RadioMenuItem; @@ -47,13 +43,13 @@ import javafx.scene.control.ToggleGroup; /** * One editor, i.e. source editor and ast tree view. The {@link NodeEditionCodeArea} handles the - * presentation of different types of nodes in separate layers. This class aggregates the event - * streams of its controls and handles configuration, language selection and such. + * presentation of different types of nodes in separate layers. This class handles configuration, + * language selection and such. * * @author Clément Fournier * @since 6.0.0 */ -public class SourceEditorController extends AbstractController implements CompositeSelectionSource { +public class SourceEditorController extends AbstractController { private static final Duration AST_REFRESH_DELAY = Duration.ofMillis(100); private final ASTManager astManager; @@ -116,8 +112,6 @@ public class SourceEditorController extends AbstractController astTreeView.setRoot(null)); Platform.runLater(parent::refreshAST); }); - - } @@ -148,13 +142,6 @@ public class SourceEditorController extends AbstractController getSubSelectionSources() { - return FXCollections.observableSet(nodeEditionCodeArea, astTreeView); - } - - /** * Refreshes the AST and returns the new compilation unit if the parse didn't fail. */ 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 106257c8f1..47edd94f1a 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 @@ -132,6 +132,8 @@ public class XPathPanelController extends AbstractController parent.refreshXPathResults()); selectionEvents = EventStreams.valuesOf(xpathResultListView.getSelectionModel().selectedItemProperty()).suppressible(); + + initNodeSelectionHandling(); } @@ -222,10 +224,9 @@ public class XPathPanelController extends AbstractController getSelectionEvents() { + public EventStream getSelectionEvents() { return selectionEvents.filter(Objects::nonNull) - .map(TextAwareNodeWrapper::getNode) - .map(n -> new NodeSelectionEvent(n, this)); + .map(TextAwareNodeWrapper::getNode); } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/AbstractController.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/AbstractController.java index 08fa047abb..18fe76a941 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/AbstractController.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/AbstractController.java @@ -24,10 +24,11 @@ import javafx.fxml.Initializable; * tree too. * *

For now controllers mostly communicate by sending messages to their parent - * and letting it forward the message to the rest of the app. TODO I'm more and more - * convinced we should avoid that and stop having the controllers hold a reference - * to their parent. They should only communicate by exposing properties their parent - * binds to, but they shouldn't know about their parent. + * and letting it forward the message to the rest of the app. + * TODO I'm more and more convinced we should avoid that and stop having the controllers + * hold a reference to their parent. They should only communicate by exposing properties + * their parent binds to, but they shouldn't know about their parent. + * {@link MessageChannel}s can allow us to decouple them event more. * *

This class mainly to make the initialization cycle of JavaFX clearer. Children controllers * are initialized before their parent, but sometimes they should only diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/ApplicationComponent.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/ApplicationComponent.java index 0eb91e1ee0..f655812c7f 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/ApplicationComponent.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/ApplicationComponent.java @@ -8,7 +8,7 @@ import java.util.function.Supplier; import net.sourceforge.pmd.util.fxdesigner.SourceEditorController; import net.sourceforge.pmd.util.fxdesigner.app.LogEntry.Category; -import net.sourceforge.pmd.util.fxdesigner.app.NodeSelectionSource.NodeSelectionEvent; +import net.sourceforge.pmd.util.fxdesigner.app.MessageChannel.Message; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner; import net.sourceforge.pmd.util.fxdesigner.util.controls.AstTreeView; @@ -24,8 +24,7 @@ import javafx.stage.Stage; * root at initialization time, eg what {@link SourceEditorController} does with {@link AstTreeView}. * *

Some more specific cross-cutting structures for the internals of the app are the {@link SettingsOwner} - * tree, which is more or less identical to the {@link AbstractController} tree. {@link NodeSelectionSource}s - * form yet another similar tree of related components. + * tree, which is more or less identical to the {@link AbstractController} tree. * * @author Clément Fournier */ @@ -120,10 +119,10 @@ public interface ApplicationComponent { } - /** Logs a tracing event pushed by a {@link NodeSelectionSource}. */ - default void logSelectionEventTrace(NodeSelectionEvent event, Supplier details) { + /** Traces a message. */ + default void logMessageTrace(Message event, Supplier details) { if (isDeveloperMode()) { - getLogger().logEvent(LogEntry.createNodeSelectionEventTraceEntry(event, details.get())); + getLogger().logEvent(LogEntry.createDataEntry(event, event.getCategory(), details.get())); } } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/CompositeSelectionSource.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/CompositeSelectionSource.java deleted file mode 100644 index a621642701..0000000000 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/CompositeSelectionSource.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd.util.fxdesigner.app; - -import org.reactfx.EventStream; -import org.reactfx.EventStreams; - -import net.sourceforge.pmd.lang.ast.Node; - -import javafx.collections.ObservableSet; - - -/** - * A {@link NodeSelectionSource} that merges the events of several sub-components. Such a source - * can also handle events itself via {@link #setFocusNode(Node)}. - * - * @author Clément Fournier - */ -public interface CompositeSelectionSource extends NodeSelectionSource { - - /** Returns the sources to forward to when bubbling down, and from which to merge events when bubbling up. */ - ObservableSet getSubSelectionSources(); - - - @Override - default EventStream getSelectionEvents() { - return EventStreams.merge(getSubSelectionSources(), NodeSelectionSource::getSelectionEvents); - } - - - default boolean isRoot() { - return false; - } - - - @Override - default void setFocusNode(Node node) { - // by default do nothing, - // maybe it should only be handled by the components - } - - - @Override - default void bubbleDown(NodeSelectionEvent selectionEvent) { - NodeSelectionSource.super.bubbleDown(selectionEvent); - - for (NodeSelectionSource source : getSubSelectionSources()) { - logSelectionEventTrace(selectionEvent, () -> getDebugName() + " forwards to " + source.getDebugName()); - source.bubbleDown(selectionEvent); - } - } -} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/EventLogger.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/EventLogger.java index 5014c7c209..3896f2aef8 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/EventLogger.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/EventLogger.java @@ -10,7 +10,6 @@ import static net.sourceforge.pmd.util.fxdesigner.app.LogEntry.Category.SELECTIO import static net.sourceforge.pmd.util.fxdesigner.app.LogEntry.Category.XPATH_EVALUATION_EXCEPTION; import static net.sourceforge.pmd.util.fxdesigner.app.LogEntry.Category.XPATH_OK; import static net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil.countNotMatching; -import static net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil.reduceEntangledIfPossible; import java.time.Duration; import java.util.EnumSet; @@ -25,7 +24,6 @@ import org.reactfx.value.Val; import net.sourceforge.pmd.util.fxdesigner.app.LogEntry.Category; import net.sourceforge.pmd.util.fxdesigner.app.LogEntry.LogEntryWithData; -import net.sourceforge.pmd.util.fxdesigner.app.NodeSelectionSource.NodeSelectionEvent; import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; @@ -51,16 +49,6 @@ public class EventLogger implements ApplicationComponent { public EventLogger(DesignerRoot designerRoot) { this.designerRoot = designerRoot; // we have to be careful with initialization order here - // none of this is done if developer mode isn't enabled because then those events aren't even pushed in the first place - EventStream> eventTraces = reduceEntangledIfPossible( - filterOnCategory(latestEvent, false, SELECTION_EVENT_TRACING).map(t -> (LogEntryWithData) t), - // the user data for those is the event - // if they're the same event we reduce them together - (lastEv, newEv) -> Objects.equals(lastEv.getUserData(), newEv.getUserData()), - LogEntryWithData::reduceEventTrace, - EVENT_TRACING_REDUCTION_DELAY - ); - EventStream onlyParseException = deleteOnSignal(latestEvent, PARSE_EXCEPTION, PARSE_OK); EventStream onlyXPathException = deleteOnSignal(latestEvent, XPATH_EVALUATION_EXCEPTION, XPATH_OK); @@ -68,7 +56,19 @@ public class EventLogger implements ApplicationComponent { filterOnCategory(latestEvent, true, PARSE_EXCEPTION, XPATH_EVALUATION_EXCEPTION, SELECTION_EVENT_TRACING) .filter(it -> isDeveloperMode() || !it.getCategory().isInternal()); - EventStreams.merge(eventTraces, onlyParseException, otherExceptions, onlyXPathException) + // none of this is done if developer mode isn't enabled because then those events aren't even pushed in the first place + @SuppressWarnings("unchecked") + EventStream> traces = latestEvent.filter(e -> e.getCategory().isTrace()).map(t -> (LogEntryWithData) t); + EventStream> reducedTraces = DesignerUtil.reduceEntangledIfPossible( + traces, + // the user data for those is the event + // if they're the same event we reduce them together + (lastEv, newEv) -> Objects.equals(lastEv.getUserData(), newEv.getUserData()), + LogEntryWithData::reduceEventTrace, + EVENT_TRACING_REDUCTION_DELAY + ); + + EventStreams.merge(reducedTraces, onlyParseException, otherExceptions, onlyXPathException) .distinct() .subscribe(fullLog::add); } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/LogEntry.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/LogEntry.java index b658897633..b35226a1fc 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/LogEntry.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/LogEntry.java @@ -5,12 +5,11 @@ package net.sourceforge.pmd.util.fxdesigner.app; import java.util.Date; +import java.util.Objects; import org.apache.commons.lang3.exception.ExceptionUtils; import org.reactfx.value.Var; -import net.sourceforge.pmd.util.fxdesigner.app.NodeSelectionSource.NodeSelectionEvent; - /** * Log entry of an {@link EventLogger}. @@ -76,11 +75,6 @@ public class LogEntry implements Comparable { } - public boolean isInternal() { - return category == Category.INTERNAL; - } - - public static LogEntry createUserExceptionEntry(Throwable thrown, Category cat) { return new LogEntry(ExceptionUtils.getStackTrace(thrown), thrown.getMessage(), cat); } @@ -105,8 +99,8 @@ public class LogEntry implements Comparable { } - public static LogEntryWithData createNodeSelectionEventTraceEntry(NodeSelectionEvent event, String details) { - return new LogEntryWithData<>(details, event.toString(), Category.SELECTION_EVENT_TRACING, event); + public static LogEntryWithData createDataEntry(T data, Category category, String details) { + return new LogEntryWithData<>(details, Objects.toString(data), category, data); } @@ -128,7 +122,7 @@ public class LogEntry implements Comparable { // These are used for events that occurred internally to the app and are // only relevant to a developer of the app. INTERNAL("Internal event", CategoryType.INTERNAL), - SELECTION_EVENT_TRACING("Selection event tracing", CategoryType.INTERNAL); + SELECTION_EVENT_TRACING("Selection event tracing", CategoryType.TRACE); public final String name; private final CategoryType type; @@ -151,8 +145,9 @@ public class LogEntry implements Comparable { } + /** Internal categories are only logged if the app is in developer mode. */ public boolean isInternal() { - return type == CategoryType.INTERNAL; + return type != CategoryType.USER_EXCEPTION; } @@ -161,9 +156,15 @@ public class LogEntry implements Comparable { } + public boolean isTrace() { + return type == CategoryType.TRACE; + } + enum CategoryType { USER_EXCEPTION, - INTERNAL + INTERNAL, + /** Trace events are aggregated. */ + TRACE } } @@ -183,8 +184,8 @@ public class LogEntry implements Comparable { } - static LogEntryWithData reduceEventTrace(LogEntryWithData prev, LogEntryWithData next) { - return createNodeSelectionEventTraceEntry(prev.getUserData(), prev.getDetails() + "\n" + next.getDetails()); + static LogEntryWithData reduceEventTrace(LogEntryWithData prev, LogEntryWithData next) { + return createDataEntry(prev.getUserData(), prev.getCategory(), prev.getDetails() + "\n" + next.getDetails()); } } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/MessageChannel.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/MessageChannel.java new file mode 100644 index 0000000000..8328ec16c3 --- /dev/null +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/MessageChannel.java @@ -0,0 +1,145 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.util.fxdesigner.app; + +import java.util.Objects; +import java.util.function.Consumer; + +import org.reactfx.EventSource; +import org.reactfx.EventStream; +import org.reactfx.Subscription; + +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.util.fxdesigner.MainDesignerController; +import net.sourceforge.pmd.util.fxdesigner.app.LogEntry.Category; + + +/** + * Implements some kind of messenger pattern. Conceptually just a globally accessible + * {@link EventSource} with some logging logic. + * + *

This patterns allows us to reduce coupling between controllers. The mediator pattern + * implemented by {@link MainDesignerController} was starting to become very obnoxious, + * every controller had to keep a reference to the main controller, and we had to implement + * several levels of delegation for deeply nested controllers. Centralising message passing + * into a few message channels also improves debug logging. + * + *

This abstraction is not sufficient to remove the mediator. The missing pieces are the + * following: + *

    + *
  • Global state of the app: in particular the current language version of the editor, and + * the current compilation unit, should be exposed globally.
  • + *
  • Transformation requests: e.g. {@link MainDesignerController#wrapNode(Node)} allows to + * associate a node with its rich-text representation by delegating to the code area. This would + * be a "two-way" channel.
  • + *
+ * + *

Channels are static for ease of access for now. + * + * @param Type of the messages of this channel + * + * @author Clément Fournier + * @since 6.12.0 + */ +public class MessageChannel { + + private final EventSource> channel = new EventSource<>(); + private final EventStream> distinct = channel.distinct(); + private final Category logCategory; + + + MessageChannel(Category logCategory) { + this.logCategory = logCategory; + } + + + /** + * Returns a stream of messages to be processed by the given component. + * + * @param component Component listening to the channel + * + * @return A stream of messages + */ + public EventStream messageStream(ApplicationComponent component) { + return distinct.hook(message -> component.logMessageTrace(message, () -> component.getDebugName() + " is handling message " + message)) + .map(Message::getContent); + } + + + /** + * Notifies the listeners of this channel with the given payload. + * In developer mode, all messages are logged. The content may be + * null. + * + * @param origin Origin of the message + * @param content Message to transmit + */ + public void pushEvent(ApplicationComponent origin, T content) { + channel.push(new Message<>(origin, logCategory, content)); + } + + + /** + * A message transmitted through a {@link MessageChannel}. + * It's a pure data class. + */ + public static final class Message { + + private final T content; + private final Category category; + private final ApplicationComponent origin; + + + Message(ApplicationComponent origin, Category category, T content) { + this.content = content; + this.category = category; + this.origin = origin; + } + + + public Category getCategory() { + return category; + } + + + /** Payload of the message. */ + public T getContent() { + return content; + } + + + /** Component that pushed the message. */ + public ApplicationComponent getOrigin() { + return origin; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Message that = (Message) o; + return Objects.equals(content, that.content) + && Objects.equals(origin, that.origin); + } + + + @Override + public int hashCode() { + return Objects.hash(content, origin); + } + + + @Override + public String toString() { + return getContent() + "(" + Objects.hashCode(getContent()) + ") from " + getOrigin().getClass().getSimpleName(); + } + } +} diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/NodeSelectionSource.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/NodeSelectionSource.java index 068dfc04fe..9af0ef6f2d 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/NodeSelectionSource.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/app/NodeSelectionSource.java @@ -4,34 +4,29 @@ package net.sourceforge.pmd.util.fxdesigner.app; -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.XPathPanelController; -import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner; +import net.sourceforge.pmd.util.fxdesigner.app.LogEntry.Category; import net.sourceforge.pmd.util.fxdesigner.util.controls.AstTreeView; -import javafx.application.Platform; - /** * A control or controller that somehow displays nodes in a form that the user can select. * When a node is selected by the user (e.g. {@link AstTreeView}, {@link XPathPanelController}, etc), * the whole UI is synchronized to reflect information about the node. This includes scrolling - * the TreeView, the editor, etc. To achieve that uniformly, node selection events are merged - * into a global stream for the whole app. Events from that stream are handled by {@link MainDesignerController}. - * - *

Node selection sources form a tree parallel to {@link AbstractController} and {@link SettingsOwner}. - * This interface implements behaviour for leaves of the tree. Inner nodes are handled by - * {@link CompositeSelectionSource}. + * the TreeView, the editor, etc. To achieve that uniformly, node selection events are forwarded + * as forwarded as messages on a {@link MessageChannel}. * * @author Clément Fournier */ public interface NodeSelectionSource extends ApplicationComponent { + /** Channel used to transmit events to all interested components. */ + MessageChannel CHANNEL = new MessageChannel<>(Category.SELECTION_EVENT_TRACING); + + /** * Returns a stream of events that should push an event each time * this source or one of its sub components records a change in node @@ -42,90 +37,22 @@ public interface NodeSelectionSource extends ApplicationComponent { * That's why you can't abstract the suppressible behaviour here. * You'd need Scala traits. */ - EventStream getSelectionEvents(); - - - /** - * Bubbles a selection event down the tree. First, {@link #setFocusNode(Node)} is called to - * handle the event (if the event didn't originate from here). If this is not a leaf of the tree, - * then the event is forwarded to the children nodes as well. - * - * @param selectionEvent Event to handle - */ - default void bubbleDown(NodeSelectionEvent selectionEvent) { - if (alwaysHandleSelection() || selectionEvent.getOrigin() != this) { - logSelectionEventTrace(selectionEvent, () -> "\t" + this.getDebugName() + " is handling event"); - // roam the tree synchronously, execute handlers some time later - Platform.runLater(() -> setFocusNode(selectionEvent.getSelection())); - } - } + EventStream getSelectionEvents(); /** * Updates the UI to react to a change in focus node. This is called whenever some selection source - * in the tree records a change. The event is not forwarded to its origin unless {@link #alwaysHandleSelection()} - * is overridden to return true. + * in the tree records a change. */ void setFocusNode(Node node); - /** Whether to also handle events which originated from this controller. */ - default boolean alwaysHandleSelection() { - return false; - } - - /** - * An event fired when the user selects a node somewhere in the UI - * and bubbled up to the {@link MainDesignerController}. It's a pure - * data class. + * Initialises this component. Must be called by the component somewhere. */ - 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() + ") from " + getOrigin().getClass().getSimpleName(); - } + default void initNodeSelectionHandling() { + getSelectionEvents().subscribe(n -> CHANNEL.pushEvent(this, n)); + CHANNEL.messageStream(this).subscribe(this::setFocusNode); } } 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 5b777a62a8..3da105c138 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 @@ -12,6 +12,7 @@ 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.SuspendableEventStream; import org.reactfx.value.Var; @@ -58,12 +59,13 @@ public class AstTreeView extends TreeView implements NodeSelectionSource { } })); + initNodeSelectionHandling(); } @Override - public SuspendableEventStream getSelectionEvents() { - return selectionEvents.map(n -> new NodeSelectionEvent(n, this)).suppressible(); + public EventStream getSelectionEvents() { + return selectionEvents; } diff --git a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeEditionCodeArea.java b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeEditionCodeArea.java index 95f2feb00f..0691b2defe 100644 --- a/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeEditionCodeArea.java +++ b/pmd-ui/src/main/java/net/sourceforge/pmd/util/fxdesigner/util/controls/NodeEditionCodeArea.java @@ -59,9 +59,9 @@ public class NodeEditionCodeArea extends HighlightLayerCodeArea i currentRuleResultsProperty().values().subscribe(this::highlightXPathResults); currentErrorNodesProperty().values().subscribe(this::highlightErrorNodes); currentNameOccurrences.values().subscribe(this::highlightNameOccurrences); + initNodeSelectionHandling(); } - /** Scroll the editor to a node and makes it visible. */ private void scrollToNode(Node node) { @@ -149,7 +149,7 @@ public class NodeEditionCodeArea extends HighlightLayerCodeArea i @Override - public EventStream getSelectionEvents() { + public EventStream getSelectionEvents() { // never emits selection events itself for now return EventStreams.never(); }