From 07f71ccf5598e02bce21acf9bb222656839346b9 Mon Sep 17 00:00:00 2001 From: Mike Baum Date: Sat, 31 Oct 2015 14:52:56 -0400 Subject: [PATCH] Added support for Container Event Sources. Implementation for issue #34. --- .../java/rx/observables/SwingObservable.java | 11 ++ .../swing/sources/ContainerEventSource.java | 76 ++++++++ .../sources/ContainerEventSourceTest.java | 179 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 src/main/java/rx/swing/sources/ContainerEventSource.java create mode 100644 src/test/java/rx/swing/sources/ContainerEventSourceTest.java diff --git a/src/main/java/rx/observables/SwingObservable.java b/src/main/java/rx/observables/SwingObservable.java index 02c544b..4e82e38 100644 --- a/src/main/java/rx/observables/SwingObservable.java +++ b/src/main/java/rx/observables/SwingObservable.java @@ -426,6 +426,17 @@ public static Observable fromChangeEvents(BoundedRangeModel bounded return ChangeEventSource.fromChangeEventsOf(boundedRangeModel); } + /** + * Creates an observable corresponding to container events (e.g. component added). + * + * @param container + * The container to register the observable for. + * @return Observable emitting the container events. + */ + public static Observable fromContainerEvents(Container container) { + return ContainerEventSource.fromContainerEventsOf(container); + } + /** * Check if the current thead is the event dispatch thread. * diff --git a/src/main/java/rx/swing/sources/ContainerEventSource.java b/src/main/java/rx/swing/sources/ContainerEventSource.java new file mode 100644 index 0000000..a1fe704 --- /dev/null +++ b/src/main/java/rx/swing/sources/ContainerEventSource.java @@ -0,0 +1,76 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.swing.sources; + +import java.awt.Container; +import java.awt.event.ContainerEvent; +import java.awt.event.ContainerListener; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Func1; +import rx.schedulers.SwingScheduler; +import rx.subscriptions.Subscriptions; + +public enum ContainerEventSource { ; // no instances + + /** + * @see rx.observables.SwingObservable#fromContainerEvents + */ + public static Observable fromContainerEventsOf(final Container container) { + return Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + final ContainerListener listener = new ContainerListener() { + @Override + public void componentRemoved(ContainerEvent event) { + subscriber.onNext(event); + } + @Override + public void componentAdded(ContainerEvent event) { + subscriber.onNext(event); + } + }; + container.addContainerListener(listener); + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + container.removeContainerListener(listener); + } + })); + } + }).subscribeOn(SwingScheduler.getInstance()) + .observeOn(SwingScheduler.getInstance()); + } + + public static enum Predicate implements Func1 { + COMPONENT_ADDED(ContainerEvent.COMPONENT_ADDED), + COMPONENT_REMOVED(ContainerEvent.COMPONENT_REMOVED); + + private final int id; + + private Predicate(int id) { + this.id = id; + } + + @Override + public Boolean call(ContainerEvent event) { + return event.getID() == id; + } + } +} diff --git a/src/test/java/rx/swing/sources/ContainerEventSourceTest.java b/src/test/java/rx/swing/sources/ContainerEventSourceTest.java new file mode 100644 index 0000000..11d1fde --- /dev/null +++ b/src/test/java/rx/swing/sources/ContainerEventSourceTest.java @@ -0,0 +1,179 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.swing.sources; + +import java.awt.Component; +import java.awt.Container; +import java.awt.event.ContainerEvent; +import java.awt.event.ContainerListener; +import java.util.Arrays; +import java.util.Collection; + +import javax.swing.JPanel; + +import org.hamcrest.Matcher; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.ArgumentMatcher; +import org.mockito.InOrder; +import org.mockito.Matchers; +import org.mockito.Mockito; + +import rx.Observable; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.observables.SwingObservable; +import rx.swing.sources.ContainerEventSource.Predicate; + +@RunWith(Parameterized.class) +public class ContainerEventSourceTest { + + private final Func1> observableFactory; + + private JPanel panel; + private Action1 action; + private Action1 error; + private Action0 complete; + + public ContainerEventSourceTest(Func1> observableFactory) { + this.observableFactory = observableFactory; + } + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ { observableFromContainerEventSource() }, + { observableFromSwingObservable() } }); + } + + @SuppressWarnings("unchecked") + @Before + public void setup() { + panel = Mockito.spy(new JPanel()); + action = Mockito.mock(Action1.class); + error = Mockito.mock(Action1.class); + complete = Mockito.mock(Action0.class); + } + + @Test + public void testObservingContainerEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action0() { + @Override + public void call() { + Subscription subscription = observableFactory.call(panel) + .subscribe(action, error, complete); + + JPanel child = new JPanel(); + panel.add(child); + panel.removeAll(); + + InOrder inOrder = Mockito.inOrder(action); + + inOrder.verify(action).call(Matchers.argThat(containerEventMatcher(panel, child, ContainerEvent.COMPONENT_ADDED))); + inOrder.verify(action).call(Matchers.argThat(containerEventMatcher(panel, child, ContainerEvent.COMPONENT_REMOVED))); + inOrder.verifyNoMoreInteractions(); + Mockito.verify(error, Mockito.never()).call(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).call(); + + // Verifies that the underlying listener has been removed. + subscription.unsubscribe(); + Mockito.verify(panel).removeContainerListener(Mockito.any(ContainerListener.class)); + Assert.assertEquals(0, panel.getHierarchyListeners().length); + + // Verifies that after unsubscribing events are not emitted. + panel.add(child); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + @Test + public void testObservingFilteredContainerEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action0() { + @Override + public void call() { + Subscription subscription = observableFactory.call(panel) + .filter(Predicate.COMPONENT_ADDED) + .subscribe(action, error, complete); + + JPanel child = new JPanel(); + panel.add(child); + panel.remove(child); // sanity check to verify that the filtering works. + + Mockito.verify(action).call(Matchers.argThat(containerEventMatcher(panel, child, ContainerEvent.COMPONENT_ADDED))); + Mockito.verify(error, Mockito.never()).call(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).call(); + + // Verifies that the underlying listener has been removed. + subscription.unsubscribe(); + Mockito.verify(panel).removeContainerListener(Mockito.any(ContainerListener.class)); + Assert.assertEquals(0, panel.getHierarchyListeners().length); + + // Verifies that after unsubscribing events are not emitted. + panel.add(child); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + private static Matcher containerEventMatcher(final Container container, final Component child, final int id) { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if ( argument.getClass() != ContainerEvent.class ) + return false; + + ContainerEvent event = (ContainerEvent) argument; + + if (container != event.getContainer()) + return false; + + if (container != event.getSource()) + return false; + + if (child != event.getChild()) + return false; + + return event.getID() == id; + } + }; + } + + private static Func1> observableFromContainerEventSource() + { + return new Func1>(){ + @Override + public Observable call(Container container) { + return ContainerEventSource.fromContainerEventsOf(container); + } + }; + } + + private static Func1> observableFromSwingObservable() + { + return new Func1>(){ + @Override + public Observable call(Container container) { + return SwingObservable.fromContainerEvents(container); + } + }; + } +}