diff --git a/rxjava-contrib/rxjava-swing/src/main/java/rx/observables/SwingObservable.java b/rxjava-contrib/rxjava-swing/src/main/java/rx/observables/SwingObservable.java new file mode 100644 index 0000000000..90ddcbcfae --- /dev/null +++ b/rxjava-contrib/rxjava-swing/src/main/java/rx/observables/SwingObservable.java @@ -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 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 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 fromKeyEvents(JComponent component, final Set keyCodes) { + return filter(fromKeyEvents(component), new Func1() { + @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> 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 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 fromMouseMotionEvents(JComponent component) { + return MouseEventSource.fromMouseMotionEventsOf(component); + } +} diff --git a/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/AbstractButtonSource.java b/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/AbstractButtonSource.java new file mode 100644 index 0000000000..9e2d933cbe --- /dev/null +++ b/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/AbstractButtonSource.java @@ -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 fromActionOf(final AbstractButton button) { + return Observable.create(new Func1, Subscription>() { + @Override + public Subscription call(final Observer 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 action = mock(Action1.class); + @SuppressWarnings("unchecked") + Action1 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.any()); + verify(error, never()).call(Matchers.any()); + verify(complete, never()).call(); + + button.testAction(); + verify(action, times(1)).call(Matchers.any()); + + button.testAction(); + verify(action, times(2)).call(Matchers.any()); + + sub.unsubscribe(); + button.testAction(); + verify(action, times(2)).call(Matchers.any()); + verify(error, never()).call(Matchers.any()); + verify(complete, never()).call(); + } + } +} diff --git a/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/KeyEventSource.java b/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/KeyEventSource.java new file mode 100644 index 0000000000..06ea9026f0 --- /dev/null +++ b/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/KeyEventSource.java @@ -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 fromKeyEventsOf(final JComponent component) { + return Observable.create(new Func1, Subscription>() { + @Override + public Subscription call(final Observer 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> currentlyPressedKeysOf(JComponent component) { + return Observable.>scan(fromKeyEventsOf(component), new HashSet(), new Func2, KeyEvent, Set>() { + @Override + public Set call(Set pressedKeys, KeyEvent event) { + Set afterEvent = new HashSet(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 action = mock(Action1.class); + @SuppressWarnings("unchecked") + Action1 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.any()); + verify(error, never()).call(Matchers.any()); + verify(complete, never()).call(); + + fireKeyEvent(event); + verify(action, times(1)).call(Matchers.any()); + + fireKeyEvent(event); + verify(action, times(2)).call(Matchers.any()); + + sub.unsubscribe(); + fireKeyEvent(event); + verify(action, times(2)).call(Matchers.any()); + verify(error, never()).call(Matchers.any()); + verify(complete, never()).call(); + } + + @Test + public void testObservingPressedKeys() { + @SuppressWarnings("unchecked") + Action1> action = mock(Action1.class); + @SuppressWarnings("unchecked") + Action1 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.emptySet()); + verify(error, never()).call(Matchers.any()); + verify(complete, never()).call(); + + fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED)); + inOrder.verify(action, times(1)).call(new HashSet(asList(1))); + verify(error, never()).call(Matchers.any()); + verify(complete, never()).call(); + + fireKeyEvent(keyEvent(2, KeyEvent.KEY_PRESSED)); + inOrder.verify(action, times(1)).call(new HashSet(asList(1, 2))); + + fireKeyEvent(keyEvent(2, KeyEvent.KEY_RELEASED)); + inOrder.verify(action, times(1)).call(new HashSet(asList(1))); + + fireKeyEvent(keyEvent(3, KeyEvent.KEY_RELEASED)); + inOrder.verify(action, times(1)).call(new HashSet(asList(1))); + + fireKeyEvent(keyEvent(1, KeyEvent.KEY_RELEASED)); + inOrder.verify(action, times(1)).call(Collections.emptySet()); + + sub.unsubscribe(); + + fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED)); + inOrder.verify(action, never()).call(Matchers.>any()); + verify(error, never()).call(Matchers.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); + } + } + } +} diff --git a/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/MouseEventSource.java b/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/MouseEventSource.java new file mode 100644 index 0000000000..1e0a9faf19 --- /dev/null +++ b/rxjava-contrib/rxjava-swing/src/main/java/rx/swing/sources/MouseEventSource.java @@ -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 java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; + +import javax.swing.JComponent; + +import rx.Observable; +import rx.Observer; +import rx.Subscription; +import rx.subscriptions.Subscriptions; +import rx.util.functions.Action0; +import rx.util.functions.Func1; + +public enum MouseEventSource { ; // no instances + + public static Observable fromMouseEventsOf(final JComponent component) { + return Observable.create(new Func1, Subscription>() { + @Override + public Subscription call(final Observer observer) { + final MouseListener listener = new MouseListener() { + @Override + public void mouseClicked(MouseEvent event) { + observer.onNext(event); + } + + @Override + public void mousePressed(MouseEvent event) { + observer.onNext(event); + } + + @Override + public void mouseReleased(MouseEvent event) { + observer.onNext(event); + } + + @Override + public void mouseEntered(MouseEvent event) { + observer.onNext(event); + } + + @Override + public void mouseExited(MouseEvent event) { + observer.onNext(event); + } + }; + component.addMouseListener(listener); + + return Subscriptions.create(new Action0() { + @Override + public void call() { + component.removeMouseListener(listener); + } + }); + } + }); + } + + public static Observable fromMouseMotionEventsOf(final JComponent component) { + return Observable.create(new Func1, Subscription>() { + @Override + public Subscription call(final Observer observer) { + final MouseMotionListener listener = new MouseMotionListener() { + @Override + public void mouseDragged(MouseEvent event) { + observer.onNext(event); + } + + @Override + public void mouseMoved(MouseEvent event) { + observer.onNext(event); + } + }; + component.addMouseMotionListener(listener); + + return Subscriptions.create(new Action0() { + @Override + public void call() { + component.removeMouseMotionListener(listener); + } + }); + } + }); + } +}