diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/event/EventUtil.java b/modules/javafx.base/src/main/java/com/sun/javafx/event/EventUtil.java index 10f9fa05a2b..26d8d83cbc2 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/event/EventUtil.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/event/EventUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,41 @@ package com.sun.javafx.event; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; - import javafx.event.Event; import javafx.event.EventDispatchChain; import javafx.event.EventTarget; public final class EventUtil { + + static { + try { + Class.forName(Event.class.getName(), true, Event.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new AssertionError(e); + } + } + + public interface Accessor { + List getUnconsumedEventHandlers(Event event); + void markDeliveryCompleted(Event event); + } + + public static void setAccessor(Accessor accessor) { + EventUtil.accessor = accessor; + } + + public static List getUnconsumedEventHandlers(Event event) { + return accessor.getUnconsumedEventHandlers(event); + } + + public static void markDeliveryCompleted(Event event) { + accessor.markDeliveryCompleted(event); + } + + private static Accessor accessor; + private static final EventDispatchChainImpl eventDispatchChain = new EventDispatchChainImpl(); @@ -71,6 +99,21 @@ private static Event fireEventImpl(EventDispatchChain eventDispatchChain, Event event) { final EventDispatchChain targetDispatchChain = eventTarget.buildEventDispatchChain(eventDispatchChain); - return targetDispatchChain.dispatchEvent(event); + Event resultEvent = targetDispatchChain.dispatchEvent(event); + + if (resultEvent != null) { + markDeliveryCompleted(resultEvent); + + List handlers = getUnconsumedEventHandlers(resultEvent); + if (handlers != null) { + for (UnconsumedEventHandler handler : handlers) { + if (handler.handle()) { + return null; + } + } + } + } + + return resultEvent; } } diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/event/UnconsumedEventHandler.java b/modules/javafx.base/src/main/java/com/sun/javafx/event/UnconsumedEventHandler.java new file mode 100644 index 00000000000..c806f0e35c0 --- /dev/null +++ b/modules/javafx.base/src/main/java/com/sun/javafx/event/UnconsumedEventHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.event; + +import javafx.event.Event; +import javafx.event.EventHandler; +import java.util.Objects; + +/** + * Captures an {@code EventHandler} that will handle an unconsumed event, as well as the + * event instance as it existed at the time the handler was captured. + * + * @param originalEvent the original event + * @param handler the event handler + */ +public record UnconsumedEventHandler(Event originalEvent, EventHandler handler) { + + public UnconsumedEventHandler { + Objects.requireNonNull(originalEvent, "originalEvent cannot be null"); + Objects.requireNonNull(handler, "handler cannot be null"); + } + + /** + * Invokes the handler with the original event. + * + * @return {@code true} if the event was consumed, {@code false} otherwise + */ + public boolean handle() { + EventUtil.markDeliveryCompleted(originalEvent); + handler.handle(originalEvent); + return originalEvent.isConsumed(); + } +} diff --git a/modules/javafx.base/src/main/java/javafx/event/Event.java b/modules/javafx.base/src/main/java/javafx/event/Event.java index 03d898ccb5e..c43a4a37c6d 100644 --- a/modules/javafx.base/src/main/java/javafx/event/Event.java +++ b/modules/javafx.base/src/main/java/javafx/event/Event.java @@ -25,10 +25,13 @@ package javafx.event; -import java.util.EventObject; - import com.sun.javafx.event.EventUtil; +import com.sun.javafx.event.UnconsumedEventHandler; +import java.util.ArrayList; +import java.util.EventObject; import java.io.IOException; +import java.io.Serial; +import java.util.List; import javafx.beans.NamedArg; // PENDING_DOC_REVIEW @@ -43,7 +46,23 @@ */ public class Event extends EventObject implements Cloneable { - private static final long serialVersionUID = 20121107L; + static { + EventUtil.setAccessor(new EventUtil.Accessor() { + @Override + public List getUnconsumedEventHandlers(Event event) { + return event.unconsumedEventHandlers; + } + + @Override + public void markDeliveryCompleted(Event event) { + event.completed = true; + } + }); + } + + @Serial + private static final long serialVersionUID = 20241110L; + /** * The constant which represents an unknown event source / target. */ @@ -65,11 +84,23 @@ public class Event extends EventObject implements Cloneable { */ protected transient EventTarget target; + /** + * The list of handlers that have expressed their interest in handling the event + * if it is still unconsumed at the end of the bubble phase of event delivery. + */ + private transient List unconsumedEventHandlers; + /** * Whether this event has been consumed by any filter or handler. */ protected boolean consumed; + /** + * Indicates whether this event has completed both delivery phases and can no + * longer accept registrations of unconsumed event handlers. + */ + private boolean completed; + /** * Construct a new {@code Event} with the specified event type. The source * and target of the event is set to {@code NULL_SOURCE_TARGET}. @@ -155,6 +186,44 @@ public void consume() { consumed = true; } + /** + * Specifies an event handler that will handle this event if it is still unconsumed after both phases + * of event delivery have completed. The unconsumed event handlers are invoked in the order they were + * registered. As soon as an event handler consumes the event, further propagation is stopped. + *

+ * This method can only be called from an event filter or event handler during event delivery; any + * attempt to call it after event delivery is complete will fail with {@link IllegalStateException}. + * + * @param handler the event handler + * @param the type of the event + * @throws IllegalStateException when event delivery has already been completed + * @since 24 + */ + public final void ifUnconsumed(EventHandler handler) { + if (completed) { + throw new IllegalStateException("Event delivery is not in progress"); + } + + if (unconsumedEventHandlers == null) { + unconsumedEventHandlers = new ArrayList<>(2); // most of the time we only expect a single handler + } + + @SuppressWarnings("unchecked") + EventHandler untypedHandler = (EventHandler)handler; + unconsumedEventHandlers.add(new UnconsumedEventHandler(this, untypedHandler)); + } + + /** + * Discards all event handlers that were added with {@link #ifUnconsumed(EventHandler)}. + * + * @since 24 + */ + public final void discardUnconsumedEventHandlers() { + if (!completed && unconsumedEventHandlers != null) { + unconsumedEventHandlers.clear(); + } + } + /** * Creates and returns a copy of this {@code Event}. * @return a new instance of {@code Event} with all values copied from diff --git a/modules/javafx.base/src/test/java/test/com/sun/javafx/event/UnconsumedEventsTest.java b/modules/javafx.base/src/test/java/test/com/sun/javafx/event/UnconsumedEventsTest.java new file mode 100644 index 00000000000..aab4f686fb3 --- /dev/null +++ b/modules/javafx.base/src/test/java/test/com/sun/javafx/event/UnconsumedEventsTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package test.com.sun.javafx.event; + +import com.sun.javafx.event.EventHandlerManager; +import com.sun.javafx.event.EventUtil; +import javafx.event.Event; +import javafx.event.EventDispatchChain; +import javafx.event.EventHandler; +import javafx.event.EventTarget; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class UnconsumedEventsTest { + + private List trace; + private EventTargetImpl target0; + private EventTargetImpl target1; + private EventTargetImpl target2; + + @BeforeEach + void setup() { + trace = new ArrayList<>(); + target0 = new EventTargetImpl("target0", null, trace); + target1 = new EventTargetImpl("target1", target0, trace); + target2 = new EventTargetImpl("target2", target1, trace); + } + + @Test + void unconsumedEventHandlerIsCalledAtEndOfDelivery() { + target2.handlerManager.addEventFilter(EmptyEvent.ANY, e -> { + e.ifUnconsumed(_ -> trace.add("unconsumed:" + e.getSource())); + }); + + EventUtil.fireEvent(target2, new EmptyEvent()); + + assertEquals( + List.of( + "filter:target0", + "filter:target1", + "filter:target2", + "handler:target2", + "handler:target1", + "handler:target0", + "unconsumed:target2"), + trace); + } + + @Test + void multipleUnconsumedEventHandlersAreCalledInSequence() { + EventHandler unconsumedHandler = e -> trace.add("unconsumed:" + e.getSource()); + target0.handlerManager.addEventFilter(EmptyEvent.ANY, e -> e.ifUnconsumed(unconsumedHandler)); + target1.handlerManager.addEventFilter(EmptyEvent.ANY, e -> e.ifUnconsumed(unconsumedHandler)); + target2.handlerManager.addEventFilter(EmptyEvent.ANY, e -> e.ifUnconsumed(unconsumedHandler)); + + EventUtil.fireEvent(target2, new EmptyEvent()); + + assertEquals( + List.of( + "filter:target0", + "filter:target1", + "filter:target2", + "handler:target2", + "handler:target1", + "handler:target0", + "unconsumed:target0", + "unconsumed:target1", + "unconsumed:target2"), + trace); + } + + @Test + void consumingAnUnconsumedEventStopsFurtherPropagation() { + EventHandler unconsumedHandler = e -> trace.add("unconsumed:" + e.getSource()); + + // The first unconsumed event handler in the chain is called. + target0.handlerManager.addEventFilter(EmptyEvent.ANY, e -> e.ifUnconsumed(unconsumedHandler)); + + // The next unconsumed handler in the chain consumes the event. + target1.handlerManager.addEventFilter(EmptyEvent.ANY, e -> e.ifUnconsumed(Event::consume)); + + // The last unconsumed event handler will not be called, as the event was consumed. + target2.handlerManager.addEventFilter(EmptyEvent.ANY, e -> e.ifUnconsumed(unconsumedHandler)); + + EventUtil.fireEvent(target2, new EmptyEvent()); + + assertEquals( + List.of( + "filter:target0", + "filter:target1", + "filter:target2", + "handler:target2", + "handler:target1", + "handler:target0", + "unconsumed:target0" + // "unconsumed:target1" <-- no trace output + // "unconsumed:target2" <-- not called + ), trace); + } + + @Test + void cannotAddUnconsumedHandlerAfterDeliveryIsComplete() { + target2.handlerManager.addEventFilter(EmptyEvent.ANY, e -> { + e.ifUnconsumed(e2 -> { + // This call will fail with IllegalStateException: + e2.ifUnconsumed(_ -> {}); + }); + }); + + assertThrows(IllegalStateException.class, () -> EventUtil.fireEvent(target2, new EmptyEvent())); + } + + @Test + void discardUnconsumedHandlers() { + EventHandler unconsumedHandler = e -> trace.add("unconsumed:" + e.getSource()); + + // Register an unconsumed event handler. + target0.handlerManager.addEventFilter(EmptyEvent.ANY, e -> e.ifUnconsumed(unconsumedHandler)); + + // After the unconsumed event handler was registered, the next handler in the chain discards it. + target1.handlerManager.addEventFilter(EmptyEvent.ANY, Event::discardUnconsumedEventHandlers); + + EventUtil.fireEvent(target2, new EmptyEvent()); + + assertEquals( + List.of( + "filter:target0", + "filter:target1", + "filter:target2", + "handler:target2", + "handler:target1", + "handler:target0"), + trace); + } + + @Test + void addUnconsumedEventHandlerAfterDiscarding() { + EventHandler unconsumedHandler = e -> trace.add("unconsumed:" + e.getSource()); + + // Register an unconsumed event handler. + target2.handlerManager.addEventHandler(EmptyEvent.ANY, e -> e.ifUnconsumed(unconsumedHandler)); + + // After the unconsumed event handler was registered, the next handler in the chain discards it + // and adds a new unconsumed event handler. The new handler is now the only handler. + target1.handlerManager.addEventHandler(EmptyEvent.ANY, e -> { + e.discardUnconsumedEventHandlers(); + e.ifUnconsumed(unconsumedHandler); + }); + + EventUtil.fireEvent(target2, new EmptyEvent()); + + assertEquals( + List.of( + "filter:target0", + "filter:target1", + "filter:target2", + "handler:target2", + "handler:target1", + "handler:target0", + "unconsumed:target1"), + trace); + } + + private static class EventTargetImpl implements EventTarget { + final String name; + final EventTargetImpl parentTarget; + final EventHandlerManager handlerManager = new EventHandlerManager(this); + + EventTargetImpl(String name, EventTargetImpl parentTarget, List trace) { + this.name = name; + this.parentTarget = parentTarget; + + handlerManager.addEventFilter(EmptyEvent.ANY, e -> trace.add("filter:" + e.getSource())); + handlerManager.addEventHandler(EmptyEvent.ANY, e -> trace.add("handler:" + e.getSource())); + } + + @Override + public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { + EventTargetImpl eventTarget = this; + while (eventTarget != null) { + tail = tail.prepend(eventTarget.handlerManager); + eventTarget = eventTarget.parentTarget; + } + + return tail; + } + + @Override + public String toString() { + return name; + } + } +}