Fix duplicate events
This commit is contained in:
parent
0b52a94c99
commit
8759db46c4
@ -19,7 +19,7 @@ public final class DesignerRoot {
|
||||
|
||||
|
||||
private final Stage mainStage;
|
||||
private final EventLogger logger = new EventLogger();
|
||||
private final EventLogger logger = new EventLogger(this);
|
||||
private final boolean developerMode;
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -157,7 +158,12 @@ public class MainDesignerController extends AbstractController implements Compos
|
||||
refreshAST(); // initial refreshing
|
||||
sourceEditorController.moveCaret(0, 0);
|
||||
|
||||
getSelectionEvents().subscribe(n -> CompositeSelectionSource.super.select(n));
|
||||
// ignore selection events produced in very short delay
|
||||
// this avoids event handling loops, e.g. an event from
|
||||
// the xpath panel is forwarded to the treeView, which
|
||||
// forwards back an event, etc.
|
||||
getSelectionEvents().thenIgnoreFor(Duration.ofMillis(20))
|
||||
.subscribe(n -> CompositeSelectionSource.super.select(n));
|
||||
}
|
||||
|
||||
|
||||
@ -166,7 +172,6 @@ public class MainDesignerController extends AbstractController implements Compos
|
||||
return FXCollections.observableSet(nodeInfoPanelController, sourceEditorController, xpathPanelController);
|
||||
}
|
||||
|
||||
|
||||
public void shutdown() {
|
||||
try {
|
||||
SettingsPersistenceUtil.persistProperties(this, DesignerUtil.getSettingsFile());
|
||||
@ -418,4 +423,11 @@ public class MainDesignerController extends AbstractController implements Compos
|
||||
public List<AbstractController> getChildren() {
|
||||
return Arrays.asList(xpathPanelController, sourceEditorController, nodeInfoPanelController);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDebugName() {
|
||||
return "MAIN";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -298,4 +298,10 @@ public class NodeInfoPanelController extends AbstractController implements NodeS
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDebugName() {
|
||||
return "info-panel";
|
||||
}
|
||||
}
|
||||
|
@ -427,4 +427,9 @@ public class SourceEditorController extends AbstractController implements Compos
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDebugName() {
|
||||
return "editor";
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.SELECT
|
||||
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.XPATH_EVALUATION_EXCEPTION;
|
||||
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.XPATH_OK;
|
||||
import static net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil.countNotMatching;
|
||||
import static net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil.reduceIfPossible;
|
||||
import static net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil.reduceEntangledIfPossible;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.EnumSet;
|
||||
@ -23,8 +23,10 @@ import org.reactfx.collection.LiveArrayList;
|
||||
import org.reactfx.collection.LiveList;
|
||||
import org.reactfx.value.Val;
|
||||
|
||||
import net.sourceforge.pmd.util.fxdesigner.DesignerRoot;
|
||||
import net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category;
|
||||
import net.sourceforge.pmd.util.fxdesigner.model.LogEntry.LogEntryWithData;
|
||||
import net.sourceforge.pmd.util.fxdesigner.util.ApplicationComponent;
|
||||
import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil;
|
||||
import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource.NodeSelectionEvent;
|
||||
|
||||
@ -35,7 +37,7 @@ import net.sourceforge.pmd.util.fxdesigner.util.NodeSelectionSource.NodeSelectio
|
||||
* @author Clément Fournier
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public class EventLogger {
|
||||
public class EventLogger implements ApplicationComponent {
|
||||
|
||||
/**
|
||||
* Exceptions from XPath evaluation or parsing are never emitted
|
||||
@ -45,26 +47,29 @@ public class EventLogger {
|
||||
private static final Duration EVENT_TRACING_REDUCTION_DELAY = Duration.ofMillis(200);
|
||||
private final EventSource<LogEntry> latestEvent = new EventSource<>();
|
||||
private final LiveList<LogEntry> fullLog = new LiveArrayList<>();
|
||||
private final DesignerRoot designerRoot;
|
||||
|
||||
|
||||
public EventLogger() {
|
||||
public EventLogger(DesignerRoot designerRoot) {
|
||||
this.designerRoot = designerRoot;
|
||||
|
||||
EventStream<LogEntryWithData<NodeSelectionEvent>> eventTraces =
|
||||
reduceIfPossible(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);
|
||||
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);
|
||||
|
||||
EventStream<LogEntry> otherExceptions =
|
||||
filterOnCategory(latestEvent, true, PARSE_EXCEPTION, XPATH_EVALUATION_EXCEPTION, SELECTION_EVENT_TRACING)
|
||||
.filter(it -> !it.getCategory().isFlag());
|
||||
.filter(it -> isDeveloperMode() || !it.getCategory().isInternal());
|
||||
|
||||
EventStreams.merge(eventTraces, onlyParseException, otherExceptions, onlyXPathException)
|
||||
.distinct()
|
||||
.subscribe(fullLog::add);
|
||||
}
|
||||
|
||||
@ -75,6 +80,12 @@ public class EventLogger {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DesignerRoot getDesignerRoot() {
|
||||
return designerRoot;
|
||||
}
|
||||
|
||||
|
||||
private static EventStream<LogEntry> deleteOnSignal(EventStream<LogEntry> input, Category normal, Category deleteSignal) {
|
||||
return DesignerUtil.deleteOnSignal(filterOnCategory(input, false, normal, deleteSignal),
|
||||
x -> x.getCategory() == deleteSignal,
|
||||
|
@ -6,6 +6,7 @@ package net.sourceforge.pmd.util.fxdesigner.model;
|
||||
|
||||
import static net.sourceforge.pmd.util.fxdesigner.model.LogEntry.Category.CategoryType.FLAG;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
@ -123,8 +124,8 @@ public class LogEntry implements Comparable<LogEntry> {
|
||||
|
||||
// These are "flag" categories that signal that previous exceptions
|
||||
// thrown during code or XPath edition may be discarded as uninteresting
|
||||
PARSE_OK("Parsing success", FLAG),
|
||||
XPATH_OK("XPath evaluation success", FLAG),
|
||||
PARSE_OK("Parsing success", CategoryType.INTERNAL),
|
||||
XPATH_OK("XPath evaluation success", CategoryType.INTERNAL),
|
||||
|
||||
/**
|
||||
* Used for events that occurred internally to the app and are only relevant to a developer of the app.
|
||||
|
@ -129,6 +129,7 @@ public final class EventLogController extends AbstractController {
|
||||
}
|
||||
});
|
||||
|
||||
logCategoryColumn.setResizable(true);
|
||||
logCategoryColumn.setCellValueFactory(new PropertyValueFactory<>("category"));
|
||||
logMessageColumn.setCellValueFactory(new PropertyValueFactory<>("message"));
|
||||
logMessageColumn.setSortable(false);
|
||||
@ -149,7 +150,7 @@ public final class EventLogController extends AbstractController {
|
||||
|
||||
logMessageColumn.prefWidthProperty()
|
||||
.bind(eventLogTableView.widthProperty()
|
||||
.subtract(logCategoryColumn.getPrefWidth())
|
||||
.subtract(logCategoryColumn.getWidth())
|
||||
.subtract(logDateColumn.getPrefWidth())
|
||||
.subtract(2)); // makes it work
|
||||
|
||||
@ -235,6 +236,7 @@ public final class EventLogController extends AbstractController {
|
||||
public void showPopup() {
|
||||
myPopupStage.show();
|
||||
popupBinding = bindPopupToThisController();
|
||||
eventLogTableView.refresh();
|
||||
}
|
||||
|
||||
|
||||
@ -242,7 +244,6 @@ public final class EventLogController extends AbstractController {
|
||||
myPopupStage.hide();
|
||||
popupBinding.unsubscribe();
|
||||
popupBinding = () -> {};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -36,16 +36,11 @@ public interface CompositeSelectionSource extends NodeSelectionSource {
|
||||
|
||||
@Override
|
||||
default void select(NodeSelectionEvent selectionEvent) {
|
||||
logSelectionEventTrace(selectionEvent, () -> getDebugName() + " received event");
|
||||
for (NodeSelectionSource source : getComponents()) {
|
||||
if (!selectionEvent.getOrigin().equals(source)) {
|
||||
logSelectionEventTrace(selectionEvent, () -> getDebugName() + " forwards to " + source.getDebugName());
|
||||
source.select(selectionEvent);
|
||||
}
|
||||
}
|
||||
NodeSelectionSource.super.select(selectionEvent);
|
||||
|
||||
if (!this.equals(selectionEvent.getOrigin())) {
|
||||
setFocusNode(selectionEvent.getSelection());
|
||||
for (NodeSelectionSource source : getComponents()) {
|
||||
logSelectionEventTrace(selectionEvent, () -> getDebugName() + " forwards to " + source.getDebugName());
|
||||
source.select(selectionEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -361,13 +361,48 @@ public final class DesignerUtil {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Like reduce if possible, but can be used if the events to reduce are emitted in extremely close
|
||||
* succession, so close that some unrelated events may be mixed up. This reduces each new event
|
||||
* with a related event in the pending notification chain instead of just considering the last one
|
||||
* as a possible reduction target.
|
||||
*/
|
||||
public static <T> EventStream<T> reduceEntangledIfPossible(EventStream<T> input, BiPredicate<T, T> canReduce, BinaryOperator<T> reduction, Duration duration) {
|
||||
EventSource<T> source = new EventSource<>();
|
||||
|
||||
input.reduceSuccessions(
|
||||
() -> new ArrayList<>(),
|
||||
(List<T> pending, T t) -> {
|
||||
|
||||
for (int i = 0; i < pending.size(); i++) {
|
||||
if (canReduce.test(pending.get(i), t)) {
|
||||
pending.set(i, reduction.apply(pending.get(i), t));
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
pending.add(t);
|
||||
|
||||
return pending;
|
||||
},
|
||||
duration
|
||||
)
|
||||
.subscribe(pending -> {
|
||||
for (T t : pending) {
|
||||
source.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an event stream that reduces successions of the input stream, and deletes the latest
|
||||
* event if a new event that matches the isCancelSignal predicate is recorded during a reduction
|
||||
* period. Cancel events are never emitted.
|
||||
* period. Cancel events are also emitted.
|
||||
*/
|
||||
public static <T> EventStream<T> deleteOnSignal(EventStream<T> input, Predicate<T> isCancelSignal, Duration duration) {
|
||||
return reduceIfPossible(input, (last, t) -> isCancelSignal.test(t), (last, t) -> t, duration).filter(isCancelSignal.negate());
|
||||
return reduceIfPossible(input, (last, t) -> isCancelSignal.test(t), (last, t) -> t, duration);
|
||||
}
|
||||
|
||||
|
||||
|
@ -32,8 +32,8 @@ public interface NodeSelectionSource extends ApplicationComponent {
|
||||
|
||||
|
||||
default void select(NodeSelectionEvent selectionEvent) {
|
||||
if (selectionEvent.getOrigin() != this) {
|
||||
logSelectionEventTrace(selectionEvent, () -> this.getDebugName() + " handling");
|
||||
if (alwaysHandleSelection() || selectionEvent.getOrigin() != this) {
|
||||
logSelectionEventTrace(selectionEvent, () -> "\t" + this.getDebugName() + " is handling event");
|
||||
setFocusNode(selectionEvent.getSelection());
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,11 @@ public interface NodeSelectionSource extends ApplicationComponent {
|
||||
void setFocusNode(Node node);
|
||||
|
||||
|
||||
default boolean alwaysHandleSelection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
final class NodeSelectionEvent {
|
||||
|
||||
private final Node selection;
|
||||
|
@ -86,6 +86,15 @@ public class NodeParentageCrumbBar extends BreadCrumbBar<Node> implements NodeSe
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean alwaysHandleSelection() {
|
||||
// We need to reset the pseudo class we artificially added.
|
||||
// if the event originated from here, then we know the crumb is displayed
|
||||
// and we won't reset the displayed nodes
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public EventStream<NodeSelectionEvent> getSelectionEvents() {
|
||||
return selectionEvents.map(n -> new NodeSelectionEvent(n, this));
|
||||
@ -219,4 +228,10 @@ public class NodeParentageCrumbBar extends BreadCrumbBar<Node> implements NodeSe
|
||||
public void setDesignerRoot(DesignerRoot designerRoot) {
|
||||
this.designerRoot = designerRoot;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDebugName() {
|
||||
return "crumb-bar";
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user