Skip to content

Commit

Permalink
Merge pull request #262 from jmhofer/swing
Browse files Browse the repository at this point in the history
A few basic Swing observables
  • Loading branch information
benjchristensen committed May 7, 2013
2 parents 4e21ee7 + cf02e71 commit 7ffc515
Show file tree
Hide file tree
Showing 4 changed files with 491 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Copyright 2013 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.observables;

import static rx.Observable.filter;

import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Set;

import javax.swing.AbstractButton;
import javax.swing.JComponent;

import rx.Observable;
import rx.swing.sources.AbstractButtonSource;
import rx.swing.sources.KeyEventSource;
import rx.swing.sources.MouseEventSource;
import rx.util.functions.Func1;

/**
* Allows creating observables from various sources specific to Swing.
*/
public enum SwingObservable { ; // no instances

/**
* Creates an observable corresponding to a Swing button action.
*
* @param button
* The button to register the observable for.
* @return Observable of action events.
*/
public static Observable<ActionEvent> fromButtonAction(AbstractButton button) {
return AbstractButtonSource.fromActionOf(button);
}

/**
* Creates an observable corresponding to raw key events.
*
* @param component
* The component to register the observable for.
* @return Observable of key events.
*/
public static Observable<KeyEvent> fromKeyEvents(JComponent component) {
return KeyEventSource.fromKeyEventsOf(component);
}

/**
* Creates an observable corresponding to raw key events, restricted a set of given key codes.
*
* @param component
* The component to register the observable for.
* @return Observable of key events.
*/
public static Observable<KeyEvent> fromKeyEvents(JComponent component, final Set<Integer> keyCodes) {
return filter(fromKeyEvents(component), new Func1<KeyEvent, Boolean>() {
@Override
public Boolean call(KeyEvent event) {
return keyCodes.contains(event.getKeyCode());
}
});
}

/**
* Creates an observable that emits the set of all currently pressed keys each time
* this set changes.
* @param component
* The component to register the observable for.
* @return Observable of currently pressed keys.
*/
public static Observable<Set<Integer>> currentlyPressedKeys(JComponent component) {
return KeyEventSource.currentlyPressedKeysOf(component);
}

/**
* Creates an observable corresponding to raw mouse events (excluding mouse motion events).
*
* @param component
* The component to register the observable for.
* @return Observable of mouse events.
*/
public static Observable<MouseEvent> fromMouseEvents(JComponent component) {
return MouseEventSource.fromMouseEventsOf(component);
}

/**
* Creates an observable corresponding to raw mouse motion events.
*
* @param component
* The component to register the observable for.
* @return Observable of mouse motion events.
*/
public static Observable<MouseEvent> fromMouseMotionEvents(JComponent component) {
return MouseEventSource.fromMouseMotionEventsOf(component);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Copyright 2013 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 static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.AbstractButton;

import org.junit.Test;
import org.mockito.Matchers;

import rx.Observable;
import rx.Observer;
import rx.Subscription;
import rx.subscriptions.Subscriptions;
import rx.util.functions.Action0;
import rx.util.functions.Action1;
import rx.util.functions.Func1;

public enum AbstractButtonSource { ; // no instances

public static Observable<ActionEvent> fromActionOf(final AbstractButton button) {
return Observable.create(new Func1<Observer<ActionEvent>, Subscription>() {
@Override
public Subscription call(final Observer<ActionEvent> observer) {
final ActionListener listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
observer.onNext(e);
}
};
button.addActionListener(listener);

return Subscriptions.create(new Action0() {
@Override
public void call() {
button.removeActionListener(listener);
}
});
}
});
}

public static class UnitTest {
@Test
public void testObservingActionEvents() {
@SuppressWarnings("unchecked")
Action1<ActionEvent> action = mock(Action1.class);
@SuppressWarnings("unchecked")
Action1<Exception> error = mock(Action1.class);
Action0 complete = mock(Action0.class);

final ActionEvent event = new ActionEvent(this, 1, "command");

@SuppressWarnings("serial")
class TestButton extends AbstractButton {
void testAction() {
fireActionPerformed(event);
}
}

TestButton button = new TestButton();
Subscription sub = fromActionOf(button).subscribe(action, error, complete);

verify(action, never()).call(Matchers.<ActionEvent>any());
verify(error, never()).call(Matchers.<Exception>any());
verify(complete, never()).call();

button.testAction();
verify(action, times(1)).call(Matchers.<ActionEvent>any());

button.testAction();
verify(action, times(2)).call(Matchers.<ActionEvent>any());

sub.unsubscribe();
button.testAction();
verify(action, times(2)).call(Matchers.<ActionEvent>any());
verify(error, never()).call(Matchers.<Exception>any());
verify(complete, never()).call();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* Copyright 2013 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 java.util.Arrays.asList;
import static org.mockito.Mockito.*;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.swing.JComponent;
import javax.swing.JPanel;

import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Matchers;

import rx.Observable;
import rx.Observer;
import rx.Subscription;
import rx.subscriptions.Subscriptions;
import rx.util.functions.Action0;
import rx.util.functions.Action1;
import rx.util.functions.Func1;
import rx.util.functions.Func2;

public enum KeyEventSource { ; // no instances

public static Observable<KeyEvent> fromKeyEventsOf(final JComponent component) {
return Observable.create(new Func1<Observer<KeyEvent>, Subscription>() {
@Override
public Subscription call(final Observer<KeyEvent> observer) {
final KeyListener listener = new KeyListener() {
@Override
public void keyPressed(KeyEvent event) {
observer.onNext(event);
}

@Override
public void keyReleased(KeyEvent event) {
observer.onNext(event);
}

@Override
public void keyTyped(KeyEvent event) {
observer.onNext(event);
}
};
component.addKeyListener(listener);

return Subscriptions.create(new Action0() {
@Override
public void call() {
component.removeKeyListener(listener);
}
});
}
});
}

public static Observable<Set<Integer>> currentlyPressedKeysOf(JComponent component) {
return Observable.<KeyEvent, Set<Integer>>scan(fromKeyEventsOf(component), new HashSet<Integer>(), new Func2<Set<Integer>, KeyEvent, Set<Integer>>() {
@Override
public Set<Integer> call(Set<Integer> pressedKeys, KeyEvent event) {
Set<Integer> afterEvent = new HashSet<Integer>(pressedKeys);
switch (event.getID()) {
case KeyEvent.KEY_PRESSED:
afterEvent.add(event.getKeyCode());
break;

case KeyEvent.KEY_RELEASED:
afterEvent.remove(event.getKeyCode());
break;

default: // nothing to do
}
return afterEvent;
}
});
}

public static class UnitTest {
private JComponent comp = new JPanel();

@Test
public void testObservingKeyEvents() {
@SuppressWarnings("unchecked")
Action1<KeyEvent> action = mock(Action1.class);
@SuppressWarnings("unchecked")
Action1<Exception> error = mock(Action1.class);
Action0 complete = mock(Action0.class);

final KeyEvent event = mock(KeyEvent.class);

Subscription sub = fromKeyEventsOf(comp).subscribe(action, error, complete);

verify(action, never()).call(Matchers.<KeyEvent>any());
verify(error, never()).call(Matchers.<Exception>any());
verify(complete, never()).call();

fireKeyEvent(event);
verify(action, times(1)).call(Matchers.<KeyEvent>any());

fireKeyEvent(event);
verify(action, times(2)).call(Matchers.<KeyEvent>any());

sub.unsubscribe();
fireKeyEvent(event);
verify(action, times(2)).call(Matchers.<KeyEvent>any());
verify(error, never()).call(Matchers.<Exception>any());
verify(complete, never()).call();
}

@Test
public void testObservingPressedKeys() {
@SuppressWarnings("unchecked")
Action1<Set<Integer>> action = mock(Action1.class);
@SuppressWarnings("unchecked")
Action1<Exception> error = mock(Action1.class);
Action0 complete = mock(Action0.class);

Subscription sub = currentlyPressedKeysOf(comp).subscribe(action, error, complete);

InOrder inOrder = inOrder(action);
inOrder.verify(action, times(1)).call(Collections.<Integer>emptySet());
verify(error, never()).call(Matchers.<Exception>any());
verify(complete, never()).call();

fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED));
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1)));
verify(error, never()).call(Matchers.<Exception>any());
verify(complete, never()).call();

fireKeyEvent(keyEvent(2, KeyEvent.KEY_PRESSED));
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1, 2)));

fireKeyEvent(keyEvent(2, KeyEvent.KEY_RELEASED));
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1)));

fireKeyEvent(keyEvent(3, KeyEvent.KEY_RELEASED));
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1)));

fireKeyEvent(keyEvent(1, KeyEvent.KEY_RELEASED));
inOrder.verify(action, times(1)).call(Collections.<Integer>emptySet());

sub.unsubscribe();

fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED));
inOrder.verify(action, never()).call(Matchers.<Set<Integer>>any());
verify(error, never()).call(Matchers.<Exception>any());
verify(complete, never()).call();
}

private KeyEvent keyEvent(int keyCode, int id) {
return new KeyEvent(comp, id, -1L, 0, keyCode, ' ');
}

private void fireKeyEvent(KeyEvent event) {
for (KeyListener listener: comp.getKeyListeners()) {
listener.keyTyped(event);
}
}
}
}
Loading

0 comments on commit 7ffc515

Please sign in to comment.