From b36ecb51116cb3910b0643666f1ecb5865965c50 Mon Sep 17 00:00:00 2001 From: Mike Baum Date: Sat, 31 Oct 2015 08:36:18 -0400 Subject: [PATCH] Added support for HierarchyEvent / HierarchyListener --- .../java/rx/observables/SwingObservable.java | 11 ++ .../swing/sources/HierarchyEventSource.java | 54 +++++++ .../sources/HierarchyEventSourceTest.java | 149 ++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 src/main/java/rx/swing/sources/HierarchyEventSource.java create mode 100644 src/test/java/rx/swing/sources/HierarchyEventSourceTest.java diff --git a/src/main/java/rx/observables/SwingObservable.java b/src/main/java/rx/observables/SwingObservable.java index 4e82e38..0c2d0ae 100644 --- a/src/main/java/rx/observables/SwingObservable.java +++ b/src/main/java/rx/observables/SwingObservable.java @@ -437,6 +437,17 @@ public static Observable fromContainerEvents(Container container return ContainerEventSource.fromContainerEventsOf(container); } + /** + * Creates an observable corresponding to hierarchy events (e.g. parent added). + * + * @param component + * The {@link Component} to register the observable for. + * @return Observable emitting hierarchy events for the provided component. + */ + public static Observable fromHierachyEvents(Component component) { + return HierarchyEventSource.fromHierarchyEventsOf(component); + } + /** * Check if the current thead is the event dispatch thread. * diff --git a/src/main/java/rx/swing/sources/HierarchyEventSource.java b/src/main/java/rx/swing/sources/HierarchyEventSource.java new file mode 100644 index 0000000..479aa2c --- /dev/null +++ b/src/main/java/rx/swing/sources/HierarchyEventSource.java @@ -0,0 +1,54 @@ +/** + * 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.event.HierarchyEvent; +import java.awt.event.HierarchyListener; + +import rx.*; +import rx.Observable.OnSubscribe; +import rx.functions.Action0; +import rx.schedulers.SwingScheduler; +import rx.subscriptions.Subscriptions; + +public enum HierarchyEventSource { ; // no instances + + /** + * @see rx.observables.SwingObservable#fromHierachyEvents + */ + public static Observable fromHierarchyEventsOf(final Component component) { + return Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + final HierarchyListener hiearchyListener = new HierarchyListener() { + @Override + public void hierarchyChanged(HierarchyEvent e) { + subscriber.onNext(e); + } + }; + component.addHierarchyListener(hiearchyListener); + subscriber.add(Subscriptions.create(new Action0(){ + @Override + public void call() { + component.removeHierarchyListener(hiearchyListener); + } + })); + } + }).subscribeOn(SwingScheduler.getInstance()) + .unsubscribeOn(SwingScheduler.getInstance()); + } +} diff --git a/src/test/java/rx/swing/sources/HierarchyEventSourceTest.java b/src/test/java/rx/swing/sources/HierarchyEventSourceTest.java new file mode 100644 index 0000000..c13e8b1 --- /dev/null +++ b/src/test/java/rx/swing/sources/HierarchyEventSourceTest.java @@ -0,0 +1,149 @@ +/** + * 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 static org.mockito.Mockito.mock; + +import java.awt.Component; +import java.awt.Container; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +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.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; + +@RunWith(Parameterized.class) +public class HierarchyEventSourceTest { + + private JPanel rootPanel; + private JPanel parentPanel; + private Action1 action; + private Action1 error; + private Action0 complete; + private final Func1> observableFactory; + + public HierarchyEventSourceTest( Func1> observableFactory ) { + this.observableFactory = observableFactory; + } + + @Parameters + public static Collection data() { + return Arrays.asList( new Object[][]{ { ObservablefromEventSource() }, + { ObservablefromSwingObservable() } }); + } + + @SuppressWarnings("unchecked") + @Before + public void setup() { + rootPanel = new JPanel(); + parentPanel = new JPanel(); + + action = mock(Action1.class); + error = mock(Action1.class); + complete = mock(Action0.class); + } + + @Test + public void testObservingHierarchyEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action0() { + @Override + public void call() { + JPanel childPanel = Mockito.spy(new JPanel()); + parentPanel.add(childPanel); + + Subscription subscription = observableFactory.call(childPanel) + .subscribe(action, error, complete); + + rootPanel.add(parentPanel); + + Mockito.verify(action).call(Matchers.argThat(hierarchyEventMatcher(childPanel, HierarchyEvent.PARENT_CHANGED, parentPanel, rootPanel))); + 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(childPanel).removeHierarchyListener(Mockito.any(HierarchyListener.class)); + Assert.assertEquals(0, childPanel.getHierarchyListeners().length); + + // Sanity check to verify that no more events are emitted after unsubscribing. + rootPanel.remove(parentPanel); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + private Matcher hierarchyEventMatcher(final Component source, final int changeFlags, final Container changed, final Container changedParent) { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if (argument.getClass() != HierarchyEvent.class) + return false; + + HierarchyEvent event = (HierarchyEvent) argument; + + if (source != event.getComponent()) + return false; + + if (changed != event.getChanged()) + return false; + + if (changedParent != event.getChangedParent()) + return false; + + return changeFlags == event.getChangeFlags(); + } + }; + } + + private static Func1> ObservablefromEventSource() + { + return new Func1>() { + @Override + public Observable call(Component component) { + return HierarchyEventSource.fromHierarchyEventsOf(component); + } + }; + } + + private static Func1> ObservablefromSwingObservable() + { + return new Func1>() { + @Override + public Observable call(Component component) { + return SwingObservable.fromHierachyEvents(component); + } + }; + } +}