Simplify and abstract selection logic with messenger pattern

This commit is contained in:
Clément Fournier
2019-02-04 02:21:42 +01:00
parent 1daa3382ae
commit e2ed8447e6
13 changed files with 218 additions and 223 deletions

View File

@ -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<AbstractController<?>> implements CompositeSelectionSource {
public class MainDesignerController extends AbstractController<AbstractController<?>> {
/* Menu bar */
@ -117,7 +113,7 @@ public class MainDesignerController extends AbstractController<AbstractControlle
} catch (Exception e) {
// shouldn't prevent the app from opening
// in case the file is corrupted, it will be overwritten on shutdown
e.printStackTrace();
logInternalException(e);
}
licenseMenuItem.setOnAction(e -> showLicensePopup());
@ -146,17 +142,9 @@ public class MainDesignerController extends AbstractController<AbstractControlle
refreshAST(); // initial refreshing
sourceEditorController.currentRuleResultsProperty().bind(xpathPanelController.currentResultsProperty());
// this is the only place where getSelectionEvents is called
getSelectionEvents().distinct().subscribe(n -> CompositeSelectionSource.super.bubbleDown(n));
}
@Override
public ObservableSet<? extends NodeSelectionSource> getSubSelectionSources() {
return FXCollections.observableSet(nodeInfoPanelController, sourceEditorController, xpathPanelController);
}
public void shutdown() {
try {
@ -290,7 +278,7 @@ public class MainDesignerController extends AbstractController<AbstractControlle
public void invalidateAst() {
nodeInfoPanelController.setFocusNode(null);
xpathPanelController.invalidateResults(false);
sourceEditorController.setFocusNode(null);
NodeSelectionSource.CHANNEL.pushEvent(this, null);
}

View File

@ -32,7 +32,6 @@ import net.sourceforge.pmd.util.fxdesigner.util.controls.ScopeHierarchyTreeCell;
import net.sourceforge.pmd.util.fxdesigner.util.controls.ScopeHierarchyTreeItem;
import net.sourceforge.pmd.util.fxdesigner.util.controls.ToolbarTitledPane;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
@ -102,16 +101,16 @@ public class NodeInfoPanelController extends AbstractController<MainDesignerCont
// suppress as early as possible in the pipeline
myScopeItemSelectionEvents = EventStreams.valuesOf(scopeHierarchyTreeView.getSelectionModel().selectedItemProperty()).suppressible();
initNodeSelectionHandling();
}
@Override
public EventStream<NodeSelectionEvent> getSelectionEvents() {
public EventStream<Node> 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<MainDesignerCont
}
selectedNode = node;
Platform.runLater(() -> displayAttributes(node));
Platform.runLater(() -> displayMetrics(node));
displayAttributes(node);
displayMetrics(node);
displayScopes(node);
}
private void displayAttributes(Node node) {
ObservableList<String> atts = getAttributes(node);
xpathAttributesListView.setItems(atts);
xpathAttributesListView.setItems(getAttributes(node));
}

View File

@ -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<MainDesignerController> implements CompositeSelectionSource {
public class SourceEditorController extends AbstractController<MainDesignerController> {
private static final Duration AST_REFRESH_DELAY = Duration.ofMillis(100);
private final ASTManager astManager;
@ -116,8 +112,6 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
tick.ifRight(c -> astTreeView.setRoot(null));
Platform.runLater(parent::refreshAST);
});
}
@ -148,13 +142,6 @@ public class SourceEditorController extends AbstractController<MainDesignerContr
languageVersionUIProperty = DesignerUtil.mapToggleGroupToUserData(languageToggleGroup, DesignerUtil::defaultLanguageVersion);
}
@Override
public ObservableSet<? extends NodeSelectionSource> getSubSelectionSources() {
return FXCollections.observableSet(nodeEditionCodeArea, astTreeView);
}
/**
* Refreshes the AST and returns the new compilation unit if the parse didn't fail.
*/

View File

@ -132,6 +132,8 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
.subscribe(tick -> parent.refreshXPathResults());
selectionEvents = EventStreams.valuesOf(xpathResultListView.getSelectionModel().selectedItemProperty()).suppressible();
initNodeSelectionHandling();
}
@ -222,10 +224,9 @@ public class XPathPanelController extends AbstractController<MainDesignerControl
@Override
public EventStream<NodeSelectionEvent> getSelectionEvents() {
public EventStream<Node> getSelectionEvents() {
return selectionEvents.filter(Objects::nonNull)
.map(TextAwareNodeWrapper::getNode)
.map(n -> new NodeSelectionEvent(n, this));
.map(TextAwareNodeWrapper::getNode);
}

View File

@ -24,10 +24,11 @@ import javafx.fxml.Initializable;
* tree too.
*
* <p>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.
*
* <p>This class mainly to make the initialization cycle of JavaFX clearer. Children controllers
* are initialized before their parent, but sometimes they should only

View File

@ -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}.
*
* <p>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<String> details) {
/** Traces a message. */
default <T> void logMessageTrace(Message<T> event, Supplier<String> details) {
if (isDeveloperMode()) {
getLogger().logEvent(LogEntry.createNodeSelectionEventTraceEntry(event, details.get()));
getLogger().logEvent(LogEntry.createDataEntry(event, event.getCategory(), details.get()));
}
}

View File

@ -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<? extends NodeSelectionSource> getSubSelectionSources();
@Override
default EventStream<NodeSelectionEvent> 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);
}
}
}

View File

@ -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<LogEntryWithData<NodeSelectionEvent>> eventTraces = reduceEntangledIfPossible(
filterOnCategory(latestEvent, false, SELECTION_EVENT_TRACING).map(t -> (LogEntryWithData<NodeSelectionEvent>) 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<LogEntry> onlyParseException = deleteOnSignal(latestEvent, PARSE_EXCEPTION, PARSE_OK);
EventStream<LogEntry> 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<LogEntryWithData<Object>> traces = latestEvent.filter(e -> e.getCategory().isTrace()).map(t -> (LogEntryWithData<Object>) t);
EventStream<LogEntryWithData<Object>> 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);
}

View File

@ -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<LogEntry> {
}
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<LogEntry> {
}
public static LogEntryWithData<NodeSelectionEvent> createNodeSelectionEventTraceEntry(NodeSelectionEvent event, String details) {
return new LogEntryWithData<>(details, event.toString(), Category.SELECTION_EVENT_TRACING, event);
public static <T> LogEntryWithData<T> 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<LogEntry> {
// 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<LogEntry> {
}
/** 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<LogEntry> {
}
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<LogEntry> {
}
static LogEntryWithData<NodeSelectionEvent> reduceEventTrace(LogEntryWithData<NodeSelectionEvent> prev, LogEntryWithData<NodeSelectionEvent> next) {
return createNodeSelectionEventTraceEntry(prev.getUserData(), prev.getDetails() + "\n" + next.getDetails());
static <T> LogEntryWithData<T> reduceEventTrace(LogEntryWithData<T> prev, LogEntryWithData<T> next) {
return createDataEntry(prev.getUserData(), prev.getCategory(), prev.getDetails() + "\n" + next.getDetails());
}
}

View File

@ -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.
*
* <p>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.
*
* <p>This abstraction is not sufficient to remove the mediator. The missing pieces are the
* following:
* <ul>
* <li>Global state of the app: in particular the current language version of the editor, and
* the current compilation unit, should be exposed globally.</li>
* <li>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.</li>
* </ul>
*
* <p>Channels are static for ease of access for now.
*
* @param <T> Type of the messages of this channel
*
* @author Clément Fournier
* @since 6.12.0
*/
public class MessageChannel<T> {
private final EventSource<Message<T>> channel = new EventSource<>();
private final EventStream<Message<T>> 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<T> 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<T> {
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();
}
}
}

View File

@ -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}.
*
* <p>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<Node> 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<NodeSelectionEvent> 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<Node> 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);
}
}

View File

@ -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<Node> implements NodeSelectionSource {
}
}));
initNodeSelectionHandling();
}
@Override
public SuspendableEventStream<NodeSelectionEvent> getSelectionEvents() {
return selectionEvents.map(n -> new NodeSelectionEvent(n, this)).suppressible();
public EventStream<Node> getSelectionEvents() {
return selectionEvents;
}

View File

@ -59,9 +59,9 @@ public class NodeEditionCodeArea extends HighlightLayerCodeArea<StyleLayerIds> 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<StyleLayerIds> i
@Override
public EventStream<NodeSelectionEvent> getSelectionEvents() {
public EventStream<Node> getSelectionEvents() {
// never emits selection events itself for now
return EventStreams.never();
}