diff --git a/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/IDisconnectable.java b/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/IDisconnectable.java new file mode 100644 index 00000000000..4ddbd1a3442 --- /dev/null +++ b/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/IDisconnectable.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022, 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. + */ +// Original code is re-licensed to Oracle by the author. +// https://github.com/andy-goryachev/FxTextEditor/blob/master/src/goryachev/common/util/Disconnectable.java +// Copyright © 2021-2022 Andy Goryachev +package com.sun.javafx.scene.control; + +/** + * A functional interface that provides a {@link #disconnect()} method. + */ +@FunctionalInterface +public interface IDisconnectable { + /** + * Disconnects what has been connected. May be called multiple times, only the + * first invocation actually disconnects. + */ + public void disconnect(); +} diff --git a/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/ListenerHelper.java b/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/ListenerHelper.java new file mode 100644 index 00000000000..e7dfb202025 --- /dev/null +++ b/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/ListenerHelper.java @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2022, 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. + */ +// Original code is re-licensed to Oracle by the author. +// https://github.com/andy-goryachev/FxTextEditor/blob/master/src/goryachev/fx/FxDisconnector.java +// Copyright © 2021-2022 Andy Goryachev +package com.sun.javafx.scene.control; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.function.Consumer; +import java.util.function.Function; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; +import javafx.concurrent.Task; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SkinBase; +import javafx.scene.control.TableColumnBase; +import javafx.scene.control.TreeItem; +import javafx.scene.transform.Transform; +import javafx.stage.Window; + +/** + * This class provides convenience methods for adding various listeners, both + * strong and weak, as well as a single {@link #disconnect()} method to remove + * all listeners. + *

+ * There are two usage patterns: + *

+ * + * This class is currently used for clean replacement of {@link Skin}s. + * We should consider making this class a part of the public API in {@code javax.base}, + * since it proved itself useful in removing listeners and handlers in bulk at the application level. + */ +public class ListenerHelper implements IDisconnectable { + private WeakReference ownerRef; + private final ArrayList items = new ArrayList<>(4); + private static Function,ListenerHelper> accessor; + + public ListenerHelper(Object owner) { + ownerRef = new WeakReference<>(owner); + } + + public ListenerHelper() { + } + + public static void setAccessor(Function,ListenerHelper> a) { + accessor = a; + } + + public static ListenerHelper get(SkinBase skin) { + return accessor.apply(skin); + } + + public IDisconnectable addDisconnectable(Runnable r) { + IDisconnectable d = new IDisconnectable() { + @Override + public void disconnect() { + items.remove(this); + r.run(); + } + }; + items.add(d); + return d; + } + + @Override + public void disconnect() { + for (int i = items.size() - 1; i >= 0; i--) { + IDisconnectable d = items.remove(i); + d.disconnect(); + } + } + + private boolean isAliveOrDisconnect() { + if (ownerRef != null) { + if (ownerRef.get() == null) { + disconnect(); + return false; + } + } + return true; + } + + // change listeners + + public IDisconnectable addChangeListener(Runnable callback, ObservableValue... props) { + return addChangeListener(callback, false, props); + } + + public IDisconnectable addChangeListener(Runnable onChange, boolean fireImmediately, ObservableValue... props) { + if (onChange == null) { + throw new NullPointerException("onChange must not be null."); + } + + ChLi li = new ChLi() { + @Override + public void disconnect() { + for (ObservableValue p : props) { + p.removeListener(this); + } + items.remove(this); + } + + @Override + public void changed(ObservableValue p, Object oldValue, Object newValue) { + if (isAliveOrDisconnect()) { + onChange.run(); + } + } + }; + + items.add(li); + + for (ObservableValue p : props) { + p.addListener(li); + } + + if (fireImmediately) { + onChange.run(); + } + + return li; + } + + public IDisconnectable addChangeListener(ObservableValue prop, ChangeListener listener) { + return addChangeListener(prop, false, listener); + } + + public IDisconnectable addChangeListener(ObservableValue prop, boolean fireImmediately, ChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + + ChLi li = new ChLi() { + @Override + public void disconnect() { + prop.removeListener(this); + items.remove(this); + } + + @Override + public void changed(ObservableValue src, T oldValue, T newValue) { + if (isAliveOrDisconnect()) { + listener.changed(src, oldValue, newValue); + } + } + }; + + items.add(li); + prop.addListener(li); + + if (fireImmediately) { + T v = prop.getValue(); + listener.changed(prop, null, v); + } + + return li; + } + + public IDisconnectable addChangeListener(ObservableValue prop, Consumer callback) { + return addChangeListener(prop, false, callback); + } + + public IDisconnectable addChangeListener(ObservableValue prop, boolean fireImmediately, Consumer callback) { + if (callback == null) { + throw new NullPointerException("Callback must be specified."); + } + + ChLi li = new ChLi() { + @Override + public void disconnect() { + prop.removeListener(this); + items.remove(this); + } + + @Override + public void changed(ObservableValue observable, T oldValue, T newValue) { + if (isAliveOrDisconnect()) { + callback.accept(newValue); + } + } + }; + + items.add(li); + prop.addListener(li); + + if (fireImmediately) { + T v = prop.getValue(); + callback.accept(v); + } + + return li; + } + + // invalidation listeners + + public IDisconnectable addInvalidationListener(Runnable callback, ObservableValue... props) { + return addInvalidationListener(callback, false, props); + } + + public IDisconnectable addInvalidationListener(Runnable callback, boolean fireImmediately, ObservableValue... props) { + if (callback == null) { + throw new NullPointerException("Callback must be specified."); + } + + InLi li = new InLi() { + @Override + public void disconnect() { + for (ObservableValue p : props) { + p.removeListener(this); + } + items.remove(this); + } + + @Override + public void invalidated(Observable p) { + if (isAliveOrDisconnect()) { + callback.run(); + } + } + }; + + items.add(li); + + for (ObservableValue p : props) { + p.addListener(li); + } + + if (fireImmediately) { + callback.run(); + } + + return li; + } + + public IDisconnectable addInvalidationListener(ObservableValue prop, InvalidationListener listener) { + return addInvalidationListener(prop, false, listener); + } + + public IDisconnectable addInvalidationListener(ObservableValue prop, boolean fireImmediately, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + + InLi li = new InLi() { + @Override + public void disconnect() { + prop.removeListener(this); + items.remove(this); + } + + @Override + public void invalidated(Observable observable) { + if (isAliveOrDisconnect()) { + listener.invalidated(observable); + } + } + }; + + items.add(li); + prop.addListener(li); + + if (fireImmediately) { + listener.invalidated(prop); + } + + return li; + } + + // list change listeners + + public IDisconnectable addListChangeListener(ObservableList list, ListChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + + LiChLi li = new LiChLi() { + @Override + public void disconnect() { + list.removeListener(this); + items.remove(this); + } + + @Override + public void onChanged(Change ch) { + if (isAliveOrDisconnect()) { + listener.onChanged(ch); + } + } + }; + + items.add(li); + list.addListener(li); + + return li; + } + + // map change listener + + public IDisconnectable addMapChangeListener(ObservableMap list, MapChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + + MaChLi li = new MaChLi() { + @Override + public void disconnect() { + list.removeListener(this); + items.remove(this); + } + + @Override + public void onChanged(Change ch) { + if (isAliveOrDisconnect()) { + listener.onChanged(ch); + } + } + }; + + items.add(li); + list.addListener(li); + + return li; + } + + // set change listeners + + public IDisconnectable addSetChangeListener(ObservableSet set, SetChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + + SeChLi li = new SeChLi() { + @Override + public void disconnect() { + set.removeListener(this); + items.remove(this); + } + + @Override + public void onChanged(Change ch) { + if (isAliveOrDisconnect()) { + listener.onChanged(ch); + } + } + }; + + items.add(li); + set.addListener(li); + + return li; + } + + // event handlers + + public IDisconnectable addEventHandler(Object x, EventType t, EventHandler handler) { + EvHa h = new EvHa<>(handler) { + @Override + public void disconnect() { + if (x instanceof Node n) { + n.removeEventHandler(t, this); + } else if (x instanceof Window y) { + y.removeEventHandler(t, this); + } else if (x instanceof Scene y) { + y.removeEventHandler(t, this); + } else if (x instanceof MenuItem y) { + y.removeEventHandler(t, this); + } else if (x instanceof TreeItem y) { + y.removeEventHandler(t, this); + } else if (x instanceof TableColumnBase y) { + y.removeEventHandler(t, this); + } else if (x instanceof Transform y) { + y.removeEventHandler(t, this); + } else if (x instanceof Task y) { + y.removeEventHandler(t, this); + } + } + }; + + items.add(h); + + // we really need an interface here ... "HasEventHandlers" + if (x instanceof Node y) { + y.addEventHandler(t, h); + } else if (x instanceof Window y) { + y.addEventHandler(t, h); + } else if (x instanceof Scene y) { + y.addEventHandler(t, h); + } else if (x instanceof MenuItem y) { + y.addEventHandler(t, h); + } else if (x instanceof TreeItem y) { + y.addEventHandler(t, h); + } else if (x instanceof TableColumnBase y) { + y.addEventHandler(t, h); + } else if (x instanceof Transform y) { + y.addEventHandler(t, h); + } else if (x instanceof Task y) { + y.addEventHandler(t, h); + } else { + throw new IllegalArgumentException("Cannot add event handler to " + x); + } + + return h; + } + + // event filters + + public IDisconnectable addEventFilter(Object x, EventType t, EventHandler handler) { + EvHa h = new EvHa<>(handler) { + @Override + public void disconnect() { + if (x instanceof Node n) { + n.removeEventFilter(t, this); + } else if (x instanceof Window y) { + y.removeEventFilter(t, this); + } else if (x instanceof Scene y) { + y.removeEventFilter(t, this); + } else if (x instanceof Transform y) { + y.removeEventFilter(t, this); + } else if (x instanceof Task y) { + y.removeEventFilter(t, this); + } + } + }; + + items.add(h); + + // we really need an interface here ... "HasEventFilters" + if (x instanceof Node y) { + y.addEventFilter(t, h); + } else if (x instanceof Window y) { + y.addEventFilter(t, h); + } else if (x instanceof Scene y) { + y.addEventFilter(t, h); + } else if (x instanceof Transform y) { + y.addEventFilter(t, h); + } else if (x instanceof Task y) { + y.addEventFilter(t, h); + } else { + throw new IllegalArgumentException("Cannot add event filter to " + x); + } + + return h; + } + + // + + private static abstract class ChLi implements IDisconnectable, ChangeListener { } + + private static abstract class InLi implements IDisconnectable, InvalidationListener { } + + private static abstract class LiChLi implements IDisconnectable, ListChangeListener { } + + private static abstract class MaChLi implements IDisconnectable, MapChangeListener { } + + private static abstract class SeChLi implements IDisconnectable, SetChangeListener { } + + private abstract class EvHa implements IDisconnectable, EventHandler { + private final EventHandler handler; + + public EvHa(EventHandler h) { + this.handler = h; + } + + @Override + public void handle(T ev) { + if (isAliveOrDisconnect()) { + handler.handle(ev); + } + } + } +} diff --git a/modules/javafx.controls/src/main/java/javafx/scene/control/SkinBase.java b/modules/javafx.controls/src/main/java/javafx/scene/control/SkinBase.java index a07635466d5..a50bd016963 100644 --- a/modules/javafx.controls/src/main/java/javafx/scene/control/SkinBase.java +++ b/modules/javafx.controls/src/main/java/javafx/scene/control/SkinBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2022, 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 @@ -29,8 +29,6 @@ import java.util.List; import java.util.function.Consumer; -import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler; - import javafx.beans.Observable; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener.Change; @@ -48,6 +46,9 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; +import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler; +import com.sun.javafx.scene.control.ListenerHelper; + /** * Base implementation class for defining the visual representation of user * interface controls by defining a scene graph of nodes to represent the @@ -58,6 +59,11 @@ */ public abstract class SkinBase implements Skin { + static { + // must be the first code to execute + ListenerHelper.setAccessor((skin) -> skin.listenerHelper()); + } + /* ************************************************************************* * * * Private fields * @@ -81,10 +87,12 @@ public abstract class SkinBase implements Skin { * This is part of the workaround introduced during delomboking. We probably will * want to adjust the way listeners are added rather than continuing to use this * map (although it doesn't really do much harm). + * + * TODO remove after migration to ListenerHelper */ private LambdaMultiplePropertyChangeListenerHandler lambdaChangeListenerHandler; - + private ListenerHelper listenerHelper; /* ************************************************************************* * * @@ -158,6 +166,10 @@ protected SkinBase(final C control) { lambdaChangeListenerHandler.dispose(); } + if (listenerHelper != null) { + listenerHelper.disconnect(); + } + this.control = null; } @@ -207,6 +219,15 @@ protected final void consumeMouseEvents(boolean value) { } } + /** + * Returns the skin's instance of {@link ListenerHelper}, creating it if necessary. + */ + ListenerHelper listenerHelper() { + if (listenerHelper == null) { + listenerHelper = new ListenerHelper(); + } + return listenerHelper; + } /** * Registers an operation to perform when the given {@code observable} sends a change event. diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/TestListenerHelper.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TestListenerHelper.java new file mode 100644 index 00000000000..530d25c659e --- /dev/null +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/TestListenerHelper.java @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2022, 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.javafx.scene.control; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import java.lang.ref.WeakReference; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import com.sun.javafx.event.EventUtil; +import com.sun.javafx.scene.control.ListenerHelper; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; +import javafx.concurrent.Task; +import javafx.event.EventTarget; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TreeItem; +import javafx.scene.control.skin.LabelSkin; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; +import javafx.scene.transform.Scale; +import javafx.stage.Stage; +import test.com.sun.javafx.scene.control.infrastructure.MouseEventGenerator; +import test.util.memory.JMemoryBuddy; + +/** + * Tests ListenerHelper utility class. + */ +public class TestListenerHelper { + @Test + public void testCheckAlive() { + Object owner = new Object(); + WeakReference ref = new WeakReference<>(owner); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + AtomicInteger disconnected = new AtomicInteger(); + + ListenerHelper h = new ListenerHelper(owner); + + h.addChangeListener(p, (v) -> ct.incrementAndGet()); + h.addDisconnectable(() -> disconnected.incrementAndGet()); + + // check that the listener is working + p.set("1"); + assertEquals(1, ct.get()); + + // collect + owner = null; + JMemoryBuddy.assertCollectable(ref); + + // fire an event that should be ignored + p.set("2"); + assertEquals(1, ct.get()); + + // check that helper has disconnected all its items + assertEquals(1, disconnected.get()); + } + + // change listeners + + @Test + public void testChangeListener_MultipleProperties() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p1 = new SimpleStringProperty(); + SimpleStringProperty p2 = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addChangeListener(() -> ct.incrementAndGet(), p1, p2); + + p1.set("1"); + p2.set("2"); + assertEquals(2, ct.get()); + + h.disconnect(); + + p1.set("3"); + p2.set("4"); + assertEquals(2, ct.get()); + } + + @Test + public void testChangeListener_MultipleProperties_FireImmediately() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p1 = new SimpleStringProperty(); + SimpleStringProperty p2 = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addChangeListener(() -> ct.incrementAndGet(), true, p1, p2); + + p1.set("1"); + p2.set("2"); + assertEquals(3, ct.get()); + + h.disconnect(); + + p1.set("3"); + p2.set("4"); + assertEquals(3, ct.get()); + } + + @Test + public void testChangeListener_Plain() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addChangeListener(p, (s, old, cur) -> ct.incrementAndGet()); + + p.set("1"); + assertEquals(1, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(1, ct.get()); + } + + @Test + public void testChangeListener_Plain_FireImmediately() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addChangeListener(p, true, (s, old, cur) -> ct.incrementAndGet()); + + p.set("1"); + assertEquals(2, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(2, ct.get()); + } + + @Test + public void testChangeListener_Callback() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addChangeListener(p, (cur) -> ct.incrementAndGet()); + + p.set("1"); + assertEquals(1, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(1, ct.get()); + } + + @Test + public void testChangeListener_Callback_FireImmediately() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addChangeListener(p, true, (cur) -> ct.incrementAndGet()); + + p.set("1"); + assertEquals(2, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(2, ct.get()); + } + + // invalidation listeners + + @Test + public void testInvalidationListener_MultipleProperties() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p1 = new SimpleStringProperty(); + SimpleStringProperty p2 = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addInvalidationListener(() -> ct.incrementAndGet(), p1, p2); + + p1.set("1"); + p2.set("2"); + assertEquals(2, ct.get()); + + h.disconnect(); + + p1.set("3"); + p2.set("4"); + assertEquals(2, ct.get()); + } + + @Test + public void testInvalidationListener_MultipleProperties_FireImmediately() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p1 = new SimpleStringProperty(); + SimpleStringProperty p2 = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addInvalidationListener(() -> ct.incrementAndGet(), true, p1, p2); + + p1.set("1"); + p2.set("2"); + assertEquals(3, ct.get()); + + h.disconnect(); + + p1.set("3"); + p2.set("4"); + assertEquals(3, ct.get()); + } + + @Test + public void testInvalidationListener_Plain() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addInvalidationListener(p, (x) -> ct.incrementAndGet()); + + p.set("1"); + assertEquals(1, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(1, ct.get()); + } + + @Test + public void testInvalidationListener_Plain_FireImmediately() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addInvalidationListener(p, true, (x) -> ct.incrementAndGet()); + + p.set("1"); + assertEquals(2, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(2, ct.get()); + } + + @Test + public void testInvalidationListener_Callback() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addInvalidationListener(() -> ct.incrementAndGet(), p); + + p.set("1"); + assertEquals(1, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(1, ct.get()); + } + + @Test + public void testInvalidationListener_Callback_FireImmediately() { + ListenerHelper h = new ListenerHelper(); + SimpleStringProperty p = new SimpleStringProperty(); + AtomicInteger ct = new AtomicInteger(); + + h.addInvalidationListener(() -> ct.incrementAndGet(), true, p); + + p.set("1"); + assertEquals(2, ct.get()); + + h.disconnect(); + + p.set("2"); + assertEquals(2, ct.get()); + } + + // list change listeners + + @Test + public void testListChangeListener() { + ListenerHelper h = new ListenerHelper(); + ObservableList list = FXCollections.observableArrayList(); + AtomicInteger ct = new AtomicInteger(); + ListChangeListener li = (ch) -> ct.incrementAndGet(); + + h.addListChangeListener(list, li); + + list.add("1"); + assertEquals(1, ct.get()); + + h.disconnect(); + + list.add("2"); + assertEquals(1, ct.get()); + } + + // set change listeners + + @Test + public void testSetChangeListener() { + ListenerHelper h = new ListenerHelper(); + ObservableSet list = FXCollections.observableSet(); + AtomicInteger ct = new AtomicInteger(); + SetChangeListener li = (ch) -> ct.incrementAndGet(); + + h.addSetChangeListener(list, li); + + list.add("1"); + assertEquals(1, ct.get()); + + h.disconnect(); + + list.add("2"); + assertEquals(1, ct.get()); + } + + // map change listeners + + @Test + public void testMapChangeListener() { + ListenerHelper h = new ListenerHelper(); + ObservableMap m = FXCollections.observableHashMap(); + AtomicInteger ct = new AtomicInteger(); + MapChangeListener li = (ch) -> ct.incrementAndGet(); + + h.addMapChangeListener(m, li); + + m.put("1", "a"); + assertEquals(1, ct.get()); + + h.disconnect(); + + m.put("2", "b"); + assertEquals(1, ct.get()); + } + + // event handlers + + @Test + public void testEventHandler() { + EventTarget[] items = eventHandlerTargets(); + + for (EventTarget item : items) { + ListenerHelper h = new ListenerHelper(); + AtomicInteger ct = new AtomicInteger(); + + h.addEventHandler(item, MouseEvent.ANY, (ev) -> ct.incrementAndGet()); + + MouseEvent ev = MouseEventGenerator.generateMouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0); + EventUtil.fireEvent(ev, item); + + assertEquals(1, ct.get()); + + h.disconnect(); + + EventUtil.fireEvent(ev, item); + assertEquals(1, ct.get()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testEventHandlerCheck() { + ListenerHelper h = new ListenerHelper(); + h.addEventHandler(new Object(), MouseEvent.ANY, (ev) -> { throw new Error(); }); + } + + // event filters + + @Test + public void testEventFilter() { + EventTarget[] items = eventHandlerFilters(); + + for (EventTarget item : items) { + ListenerHelper h = new ListenerHelper(); + AtomicInteger ct = new AtomicInteger(); + + h.addEventFilter(item, MouseEvent.ANY, (ev) -> ct.incrementAndGet()); + + MouseEvent ev = MouseEventGenerator.generateMouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0); + EventUtil.fireEvent(ev, item); + + assertEquals(1, ct.get()); + + h.disconnect(); + + EventUtil.fireEvent(ev, item); + assertEquals(1, ct.get()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testEventFilterCheck() { + ListenerHelper h = new ListenerHelper(); + h.addEventFilter(new Object(), MouseEvent.ANY, (ev) -> { throw new Error(); }); + } + + // + + protected EventTarget[] eventHandlerTargets() { + return new EventTarget[] { + new Region(), + new Stage(), + new Scene(new Group()), + new MenuItem(), + new TreeItem(), + new TableColumn(), + new Scale(), + new Task() { + @Override + protected Object call() throws Exception { + return null; + } + } + }; + } + + protected EventTarget[] eventHandlerFilters() { + return new EventTarget[] { + new Region(), + new Stage(), + new Scene(new Group()), + new Scale(), + new Task() { + @Override + protected Object call() throws Exception { + return null; + } + } + }; + } +}