Skip to content

Commit

Permalink
Merge pull request #56 from mikebaum/addContainerListenerSupport
Browse files Browse the repository at this point in the history
Add container listener support
  • Loading branch information
mikebaum committed Feb 1, 2016
2 parents a7be147 + 07f71cc commit c175908
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/main/java/rx/observables/SwingObservable.java
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,17 @@ public static Observable<ChangeEvent> 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<ContainerEvent> fromContainerEvents(Container container) {
return ContainerEventSource.fromContainerEventsOf(container);
}

/**
* Check if the current thead is the event dispatch thread.
*
Expand Down
76 changes: 76 additions & 0 deletions src/main/java/rx/swing/sources/ContainerEventSource.java
Original file line number Diff line number Diff line change
@@ -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<ContainerEvent> fromContainerEventsOf(final Container container) {
return Observable.create(new OnSubscribe<ContainerEvent>() {
@Override
public void call(final Subscriber<? super ContainerEvent> 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<ContainerEvent, Boolean> {
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;
}
}
}
179 changes: 179 additions & 0 deletions src/test/java/rx/swing/sources/ContainerEventSourceTest.java
Original file line number Diff line number Diff line change
@@ -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<Container, Observable<ContainerEvent>> observableFactory;

private JPanel panel;
private Action1<ContainerEvent> action;
private Action1<Throwable> error;
private Action0 complete;

public ContainerEventSourceTest(Func1<Container, Observable<ContainerEvent>> observableFactory) {
this.observableFactory = observableFactory;
}

@Parameters
public static Collection<Object[]> 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<ContainerEvent> containerEventMatcher(final Container container, final Component child, final int id) {
return new ArgumentMatcher<ContainerEvent>() {
@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<Container, Observable<ContainerEvent>> observableFromContainerEventSource()
{
return new Func1<Container, Observable<ContainerEvent>>(){
@Override
public Observable<ContainerEvent> call(Container container) {
return ContainerEventSource.fromContainerEventsOf(container);
}
};
}

private static Func1<Container, Observable<ContainerEvent>> observableFromSwingObservable()
{
return new Func1<Container, Observable<ContainerEvent>>(){
@Override
public Observable<ContainerEvent> call(Container container) {
return SwingObservable.fromContainerEvents(container);
}
};
}
}

0 comments on commit c175908

Please sign in to comment.