Skip to content

Commit 7ffc515

Browse files
Merge pull request ReactiveX#262 from jmhofer/swing
A few basic Swing observables
2 parents 4e21ee7 + cf02e71 commit 7ffc515

File tree

4 files changed

+491
-0
lines changed

4 files changed

+491
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package rx.observables;
17+
18+
import static rx.Observable.filter;
19+
20+
import java.awt.event.ActionEvent;
21+
import java.awt.event.KeyEvent;
22+
import java.awt.event.MouseEvent;
23+
import java.util.Set;
24+
25+
import javax.swing.AbstractButton;
26+
import javax.swing.JComponent;
27+
28+
import rx.Observable;
29+
import rx.swing.sources.AbstractButtonSource;
30+
import rx.swing.sources.KeyEventSource;
31+
import rx.swing.sources.MouseEventSource;
32+
import rx.util.functions.Func1;
33+
34+
/**
35+
* Allows creating observables from various sources specific to Swing.
36+
*/
37+
public enum SwingObservable { ; // no instances
38+
39+
/**
40+
* Creates an observable corresponding to a Swing button action.
41+
*
42+
* @param button
43+
* The button to register the observable for.
44+
* @return Observable of action events.
45+
*/
46+
public static Observable<ActionEvent> fromButtonAction(AbstractButton button) {
47+
return AbstractButtonSource.fromActionOf(button);
48+
}
49+
50+
/**
51+
* Creates an observable corresponding to raw key events.
52+
*
53+
* @param component
54+
* The component to register the observable for.
55+
* @return Observable of key events.
56+
*/
57+
public static Observable<KeyEvent> fromKeyEvents(JComponent component) {
58+
return KeyEventSource.fromKeyEventsOf(component);
59+
}
60+
61+
/**
62+
* Creates an observable corresponding to raw key events, restricted a set of given key codes.
63+
*
64+
* @param component
65+
* The component to register the observable for.
66+
* @return Observable of key events.
67+
*/
68+
public static Observable<KeyEvent> fromKeyEvents(JComponent component, final Set<Integer> keyCodes) {
69+
return filter(fromKeyEvents(component), new Func1<KeyEvent, Boolean>() {
70+
@Override
71+
public Boolean call(KeyEvent event) {
72+
return keyCodes.contains(event.getKeyCode());
73+
}
74+
});
75+
}
76+
77+
/**
78+
* Creates an observable that emits the set of all currently pressed keys each time
79+
* this set changes.
80+
* @param component
81+
* The component to register the observable for.
82+
* @return Observable of currently pressed keys.
83+
*/
84+
public static Observable<Set<Integer>> currentlyPressedKeys(JComponent component) {
85+
return KeyEventSource.currentlyPressedKeysOf(component);
86+
}
87+
88+
/**
89+
* Creates an observable corresponding to raw mouse events (excluding mouse motion events).
90+
*
91+
* @param component
92+
* The component to register the observable for.
93+
* @return Observable of mouse events.
94+
*/
95+
public static Observable<MouseEvent> fromMouseEvents(JComponent component) {
96+
return MouseEventSource.fromMouseEventsOf(component);
97+
}
98+
99+
/**
100+
* Creates an observable corresponding to raw mouse motion events.
101+
*
102+
* @param component
103+
* The component to register the observable for.
104+
* @return Observable of mouse motion events.
105+
*/
106+
public static Observable<MouseEvent> fromMouseMotionEvents(JComponent component) {
107+
return MouseEventSource.fromMouseMotionEventsOf(component);
108+
}
109+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package rx.swing.sources;
17+
18+
import static org.mockito.Mockito.mock;
19+
import static org.mockito.Mockito.never;
20+
import static org.mockito.Mockito.times;
21+
import static org.mockito.Mockito.verify;
22+
23+
import java.awt.event.ActionEvent;
24+
import java.awt.event.ActionListener;
25+
26+
import javax.swing.AbstractButton;
27+
28+
import org.junit.Test;
29+
import org.mockito.Matchers;
30+
31+
import rx.Observable;
32+
import rx.Observer;
33+
import rx.Subscription;
34+
import rx.subscriptions.Subscriptions;
35+
import rx.util.functions.Action0;
36+
import rx.util.functions.Action1;
37+
import rx.util.functions.Func1;
38+
39+
public enum AbstractButtonSource { ; // no instances
40+
41+
public static Observable<ActionEvent> fromActionOf(final AbstractButton button) {
42+
return Observable.create(new Func1<Observer<ActionEvent>, Subscription>() {
43+
@Override
44+
public Subscription call(final Observer<ActionEvent> observer) {
45+
final ActionListener listener = new ActionListener() {
46+
@Override
47+
public void actionPerformed(ActionEvent e) {
48+
observer.onNext(e);
49+
}
50+
};
51+
button.addActionListener(listener);
52+
53+
return Subscriptions.create(new Action0() {
54+
@Override
55+
public void call() {
56+
button.removeActionListener(listener);
57+
}
58+
});
59+
}
60+
});
61+
}
62+
63+
public static class UnitTest {
64+
@Test
65+
public void testObservingActionEvents() {
66+
@SuppressWarnings("unchecked")
67+
Action1<ActionEvent> action = mock(Action1.class);
68+
@SuppressWarnings("unchecked")
69+
Action1<Exception> error = mock(Action1.class);
70+
Action0 complete = mock(Action0.class);
71+
72+
final ActionEvent event = new ActionEvent(this, 1, "command");
73+
74+
@SuppressWarnings("serial")
75+
class TestButton extends AbstractButton {
76+
void testAction() {
77+
fireActionPerformed(event);
78+
}
79+
}
80+
81+
TestButton button = new TestButton();
82+
Subscription sub = fromActionOf(button).subscribe(action, error, complete);
83+
84+
verify(action, never()).call(Matchers.<ActionEvent>any());
85+
verify(error, never()).call(Matchers.<Exception>any());
86+
verify(complete, never()).call();
87+
88+
button.testAction();
89+
verify(action, times(1)).call(Matchers.<ActionEvent>any());
90+
91+
button.testAction();
92+
verify(action, times(2)).call(Matchers.<ActionEvent>any());
93+
94+
sub.unsubscribe();
95+
button.testAction();
96+
verify(action, times(2)).call(Matchers.<ActionEvent>any());
97+
verify(error, never()).call(Matchers.<Exception>any());
98+
verify(complete, never()).call();
99+
}
100+
}
101+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package rx.swing.sources;
17+
18+
import static java.util.Arrays.asList;
19+
import static org.mockito.Mockito.*;
20+
21+
import java.awt.event.KeyEvent;
22+
import java.awt.event.KeyListener;
23+
import java.util.Collections;
24+
import java.util.HashSet;
25+
import java.util.Set;
26+
27+
import javax.swing.JComponent;
28+
import javax.swing.JPanel;
29+
30+
import org.junit.Test;
31+
import org.mockito.InOrder;
32+
import org.mockito.Matchers;
33+
34+
import rx.Observable;
35+
import rx.Observer;
36+
import rx.Subscription;
37+
import rx.subscriptions.Subscriptions;
38+
import rx.util.functions.Action0;
39+
import rx.util.functions.Action1;
40+
import rx.util.functions.Func1;
41+
import rx.util.functions.Func2;
42+
43+
public enum KeyEventSource { ; // no instances
44+
45+
public static Observable<KeyEvent> fromKeyEventsOf(final JComponent component) {
46+
return Observable.create(new Func1<Observer<KeyEvent>, Subscription>() {
47+
@Override
48+
public Subscription call(final Observer<KeyEvent> observer) {
49+
final KeyListener listener = new KeyListener() {
50+
@Override
51+
public void keyPressed(KeyEvent event) {
52+
observer.onNext(event);
53+
}
54+
55+
@Override
56+
public void keyReleased(KeyEvent event) {
57+
observer.onNext(event);
58+
}
59+
60+
@Override
61+
public void keyTyped(KeyEvent event) {
62+
observer.onNext(event);
63+
}
64+
};
65+
component.addKeyListener(listener);
66+
67+
return Subscriptions.create(new Action0() {
68+
@Override
69+
public void call() {
70+
component.removeKeyListener(listener);
71+
}
72+
});
73+
}
74+
});
75+
}
76+
77+
public static Observable<Set<Integer>> currentlyPressedKeysOf(JComponent component) {
78+
return Observable.<KeyEvent, Set<Integer>>scan(fromKeyEventsOf(component), new HashSet<Integer>(), new Func2<Set<Integer>, KeyEvent, Set<Integer>>() {
79+
@Override
80+
public Set<Integer> call(Set<Integer> pressedKeys, KeyEvent event) {
81+
Set<Integer> afterEvent = new HashSet<Integer>(pressedKeys);
82+
switch (event.getID()) {
83+
case KeyEvent.KEY_PRESSED:
84+
afterEvent.add(event.getKeyCode());
85+
break;
86+
87+
case KeyEvent.KEY_RELEASED:
88+
afterEvent.remove(event.getKeyCode());
89+
break;
90+
91+
default: // nothing to do
92+
}
93+
return afterEvent;
94+
}
95+
});
96+
}
97+
98+
public static class UnitTest {
99+
private JComponent comp = new JPanel();
100+
101+
@Test
102+
public void testObservingKeyEvents() {
103+
@SuppressWarnings("unchecked")
104+
Action1<KeyEvent> action = mock(Action1.class);
105+
@SuppressWarnings("unchecked")
106+
Action1<Exception> error = mock(Action1.class);
107+
Action0 complete = mock(Action0.class);
108+
109+
final KeyEvent event = mock(KeyEvent.class);
110+
111+
Subscription sub = fromKeyEventsOf(comp).subscribe(action, error, complete);
112+
113+
verify(action, never()).call(Matchers.<KeyEvent>any());
114+
verify(error, never()).call(Matchers.<Exception>any());
115+
verify(complete, never()).call();
116+
117+
fireKeyEvent(event);
118+
verify(action, times(1)).call(Matchers.<KeyEvent>any());
119+
120+
fireKeyEvent(event);
121+
verify(action, times(2)).call(Matchers.<KeyEvent>any());
122+
123+
sub.unsubscribe();
124+
fireKeyEvent(event);
125+
verify(action, times(2)).call(Matchers.<KeyEvent>any());
126+
verify(error, never()).call(Matchers.<Exception>any());
127+
verify(complete, never()).call();
128+
}
129+
130+
@Test
131+
public void testObservingPressedKeys() {
132+
@SuppressWarnings("unchecked")
133+
Action1<Set<Integer>> action = mock(Action1.class);
134+
@SuppressWarnings("unchecked")
135+
Action1<Exception> error = mock(Action1.class);
136+
Action0 complete = mock(Action0.class);
137+
138+
Subscription sub = currentlyPressedKeysOf(comp).subscribe(action, error, complete);
139+
140+
InOrder inOrder = inOrder(action);
141+
inOrder.verify(action, times(1)).call(Collections.<Integer>emptySet());
142+
verify(error, never()).call(Matchers.<Exception>any());
143+
verify(complete, never()).call();
144+
145+
fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED));
146+
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1)));
147+
verify(error, never()).call(Matchers.<Exception>any());
148+
verify(complete, never()).call();
149+
150+
fireKeyEvent(keyEvent(2, KeyEvent.KEY_PRESSED));
151+
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1, 2)));
152+
153+
fireKeyEvent(keyEvent(2, KeyEvent.KEY_RELEASED));
154+
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1)));
155+
156+
fireKeyEvent(keyEvent(3, KeyEvent.KEY_RELEASED));
157+
inOrder.verify(action, times(1)).call(new HashSet<Integer>(asList(1)));
158+
159+
fireKeyEvent(keyEvent(1, KeyEvent.KEY_RELEASED));
160+
inOrder.verify(action, times(1)).call(Collections.<Integer>emptySet());
161+
162+
sub.unsubscribe();
163+
164+
fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED));
165+
inOrder.verify(action, never()).call(Matchers.<Set<Integer>>any());
166+
verify(error, never()).call(Matchers.<Exception>any());
167+
verify(complete, never()).call();
168+
}
169+
170+
private KeyEvent keyEvent(int keyCode, int id) {
171+
return new KeyEvent(comp, id, -1L, 0, keyCode, ' ');
172+
}
173+
174+
private void fireKeyEvent(KeyEvent event) {
175+
for (KeyListener listener: comp.getKeyListeners()) {
176+
listener.keyTyped(event);
177+
}
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)