forked from phoedos/pmd
Simplify and abstract selection logic with messenger pattern
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user