diff --git a/.gitignore b/.gitignore
index 32858aa..ca5d5c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,71 @@
+ompiled source #
+###################
+*.com
*.class
+*.dll
+*.exe
+*.o
+*.so
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
+# Packages #
+############
+# it's better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
*.jar
-*.war
-*.ear
+*.rar
+*.tar
+*.zip
+
+# Logs and databases #
+######################
+*.log
+
+# OS generated files #
+######################
+.DS_Store*
+ehthumbs.db
+Icon?
+Thumbs.db
+
+# Editor Files #
+################
+*~
+*.swp
+
+# Gradle Files #
+################
+.gradle
+.gradletasknamecache
+.m2
+
+# Build output directies
+target/
+build/
+
+# IntelliJ specific files/directories
+out
+.idea
+*.ipr
+*.iws
+*.iml
+atlassian-ide-plugin.xml
+
+# Eclipse specific files/directories
+.classpath
+.project
+.settings
+.metadata
+bin/
+
+# NetBeans specific files/directories
+.nbattrs
+/.nb-gradle/profiles/private/
+.nb-gradle-properties
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+# Scala build
+*.cache
+/.nb-gradle/private/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..514a71b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,21 @@
+language: java
+jdk:
+jdk:
+ - oraclejdk8
+# force upgrade Java8 as per https://github.com/travis-ci/travis-ci/issues/4042 (fixes compilation issue)
+addons:
+ apt:
+ packages:
+ - oracle-java8-installer
+sudo: false
+script: gradle/buildViaTravis.sh
+cache:
+ directories:
+ - "$HOME/.m2"
+ - "$HOME/.gradle"
+env:
+ global:
+ - secure: i92CdoDDqeIiCGbhVc4AM4nQ4RORRZy1s9dEKMl6yQtXeD6Y5kZXg+SzRJSlAM9TNM7O24VmFHJn0o34jD8AJx4irk6JuFJQ23yGipGIsVUUetqpckF9hcsJl5/9D+0B6DpxQR4wiy2W4mSZeYSryXKWLbx4YC84ICEmiMIF0tc=
+ - secure: Q4TcIw8otYdvhPUgVcKMsoUm4ab277+bA9CMvcQSopSl9PRZgQBXG8GTI41LrPnlWHoocAqA8lhj4dS1ACHb7ulPBNVGOTbJwgSAP62WZ7Ka7T07idef2i8/s/HOKAYGJO2tC7OCX5Ynfd9seVm4zXbyLJkR5FqGhFDPhF5zDxc=
+ - secure: fx61a7A8lZNqGAArBcigjPtsxfj7wJOT2Tq+vbfdZouiPBZ68yUHKMmebssWDun62hrwJoFO3rSM6Qeg/t592AsRn/y+MF1l/IWG5VXkggaC0mYAYVH3cBjbVnWeAmPILva9YHeCsLHPyq8v8Z7bIhRz2f2tWwAPbUeezQUtxk4=
+ - secure: bIv6R9czUONhAOIpw/ustrmea5AMnLaUqLN93ASXzX1jqH8enx0uNj7HI5rRnmRSliKlh3klWofiMnuQnQQeREGlOYHJnkXx2Dy5RAQ2ps7gZDB/LCeBoyi6LajS5gEvNOvK7p2FuPtXZSGGDE9iEyLP/zswWeNJkjDY/Gi3WNo=
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..96f81c4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,31 @@
+# Contributing to RxJava
+
+If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request (on a branch other than `master` or `gh-pages`).
+
+When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible.
+
+## License
+
+By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/ReactiveX/RxJava/blob/master/LICENSE
+
+All files are released with the Apache 2.0 license.
+
+If you are adding a new file it should have a header like this:
+
+```
+/**
+ * Copyright 2014 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.
+ */
+ ```
diff --git a/LICENSE b/LICENSE
index 8dada3e..7f8ced0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,4 @@
+
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -178,7 +179,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
+ boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@@ -186,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright {yyyy} {name of copyright owner}
+ Copyright 2012 Netflix, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8ae09c3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,67 @@
+# RxSwing2: Swing bindings for RxJava
+
+Learn more about RxJava on the Wiki Home and the Netflix TechBlog post where RxJava was introduced.
+
+## Master Build Status
+
+
+
+## Communication
+RxSwing:
+- [GitHub Issues](https://github.com/ReactiveX/RxSwing/issues)
+
+General RxJava:
+- Google Group: [RxJava](http://groups.google.com/d/forum/rxjava)
+- Twitter: [@RxJava](http://twitter.com/RxJava)
+
+## Binaries
+
+Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7C%22rxswing%22%20AND%20g%3A%22io.reactivex%22).
+
+Example for Maven:
+
+```xml
+
+ io.reactivex.rxjava2
+ rxswing
+ xxx
+
+```
+and for Ivy:
+
+```xml
+
+```
+
+## Build
+
+To build:
+
+```
+$ git clone git@github.com:ReactiveX/RxSwing.git
+$ cd RxSwing/
+$ ./gradlew build
+```
+
+## Bugs and Feedback
+
+For bugs, questions and discussions please use the [Github Issues](https://github.com/ReactiveX/RxSwing/issues).
+
+## Learning Resources:
+
+Architecture tutorial and examples from @Petikoch: https://github.com/Petikoch/Java_MVVM_with_Swing_and_RxJava_Examples
+
+
+## LICENSE
+
+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
+
+
+
+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.
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..dbf6464
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+group = 'io.reactivex.rxjava2'
+
+buildscript {
+ repositories { jcenter() }
+ dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:4.0.0' }
+}
+
+apply plugin: 'nebula.rxjava-project'
+apply plugin: 'java'
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
+
+dependencies {
+ compile 'io.reactivex.rxjava2:rxjava:2.0.0'
+ testCompile 'junit:junit:4.12'
+ testCompile 'org.mockito:mockito-core:1.10.19'
+}
+
+// support for snapshot/final releases with the various branches RxJava uses
+nebulaRelease {
+ addReleaseBranchPattern(/\d+\.\d+\.\d+/)
+ addReleaseBranchPattern('HEAD')
+}
+
+if (project.hasProperty('release.useLastTag')) {
+ tasks.prepare.enabled = false
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..f0d1ddd
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+version=2.0.0-RC1-SNAPSHOT
diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh
new file mode 100755
index 0000000..d98e5eb
--- /dev/null
+++ b/gradle/buildViaTravis.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# This script will build the project.
+
+if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
+ echo -e "Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]"
+ ./gradlew -Prelease.useLastTag=true build
+elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then
+ echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']'
+ ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace
+elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then
+ echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']'
+ ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final --stacktrace
+else
+ echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']'
+ ./gradlew -Prelease.useLastTag=true build
+fi
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..d3b8398
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a6c755f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Aug 09 15:44:13 CEST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..27309d9
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..832fdb6
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..6393b26
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name='rxswing'
diff --git a/src/main/java/rx/observables/SwingObservable.java b/src/main/java/rx/observables/SwingObservable.java
new file mode 100644
index 0000000..6a66a48
--- /dev/null
+++ b/src/main/java/rx/observables/SwingObservable.java
@@ -0,0 +1,482 @@
+/**
+ * Copyright 2014 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 java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.ItemSelectable;
+import java.awt.Point;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ContainerEvent;
+import java.awt.event.FocusEvent;
+import java.awt.event.HierarchyEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.WindowEvent;
+import java.beans.PropertyChangeEvent;
+import java.util.Set;
+
+import javax.swing.AbstractButton;
+import javax.swing.BoundedRangeModel;
+import javax.swing.ButtonModel;
+import javax.swing.JProgressBar;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JViewport;
+import javax.swing.ListSelectionModel;
+import javax.swing.SpinnerModel;
+import javax.swing.SwingUtilities;
+import javax.swing.colorchooser.ColorSelectionModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.text.Document;
+
+import io.reactivex.Observable;
+import rx.swing.sources.AbstractButtonSource;
+import rx.swing.sources.ChangeEventSource;
+import rx.swing.sources.ComponentEventSource;
+import rx.swing.sources.ContainerEventSource;
+import rx.swing.sources.DocumentEventSource;
+import rx.swing.sources.FocusEventSource;
+import rx.swing.sources.HierarchyEventSource;
+import rx.swing.sources.ItemEventSource;
+import rx.swing.sources.KeyEventSource;
+import rx.swing.sources.ListSelectionEventSource;
+import rx.swing.sources.MouseEventSource;
+import rx.swing.sources.PropertyChangeEventSource;
+import rx.swing.sources.WindowEventSource;
+
+/**
+ * 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(Component 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(Component component, final Set keyCodes) {
+ return fromKeyEvents(component).filter(event -> 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> fromPressedKeys(Component 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(Component 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(Component component) {
+ return MouseEventSource.fromMouseMotionEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to relative mouse motion.
+ * @param component
+ * The component to register the observable for.
+ * @return A point whose x and y coordinate represent the relative horizontal and vertical mouse motion.
+ */
+ public static Observable fromRelativeMouseMotion(Component component) {
+ return MouseEventSource.fromRelativeMouseMotion(component);
+ }
+
+ /**
+ * Creates an observable corresponding to raw mouse wheel events.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return The component to register the observable for.
+ */
+ public static Observable fromMouseWheelEvents(Component component) {
+ return MouseEventSource.fromMouseWheelEvents(component);
+ }
+
+ /**
+ * Creates an observable corresponding to raw component events.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable of component events.
+ */
+ public static Observable fromComponentEvents(Component component) {
+ return ComponentEventSource.fromComponentEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to focus events.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable of focus events.
+ */
+ public static Observable fromFocusEvents(Component component) {
+ return FocusEventSource.fromFocusEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to component resize events.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable emitting the current size of the given component after each resize event.
+ */
+ public static Observable fromResizing(Component component) {
+ return ComponentEventSource.fromResizing(component);
+ }
+
+ /**
+ * Creates an observable corresponding to item events.
+ *
+ * @param itemSelectable
+ * The ItemSelectable to register the observable for.
+ * @return Observable emitting the item events for the given itemSelectable.
+ */
+ public static Observable fromItemEvents(ItemSelectable itemSelectable) {
+ return ItemEventSource.fromItemEventsOf(itemSelectable);
+ }
+
+ /**
+ * Creates an observable corresponding to item selection events.
+ *
+ * @param itemSelectable
+ * The ItemSelectable to register the observable for.
+ * @return Observable emitting the an item event whenever the given itemSelectable is selected.
+ */
+ public static Observable fromItemSelectionEvents(ItemSelectable itemSelectable) {
+ return ItemEventSource.fromItemEventsOf(itemSelectable).filter(event -> event.getStateChange() == ItemEvent.SELECTED);
+ }
+
+ /**
+ * Creates an observable corresponding to item deselection events.
+ *
+ * @param itemSelectable
+ * The ItemSelectable to register the observable for.
+ * @return Observable emitting the an item event whenever the given itemSelectable is deselected.
+ */
+ public static Observable fromItemDeselectionEvents(ItemSelectable itemSelectable) {
+ return ItemEventSource.fromItemEventsOf(itemSelectable).filter(event -> event.getStateChange() == ItemEvent.DESELECTED);
+ }
+
+ /**
+ * Creates an observable corresponding to list selection events (e.g. from a JList or a JTable row / column selection).
+ *
+ * For more info to swing list selection see
+ * How to Write a List Selection Listener.
+ *
+ * @param listSelectionModel
+ * The ListSelectionModel to register the observable for.
+ * @return Observable emitting the list selection events.
+ */
+ public static Observable fromListSelectionEvents(ListSelectionModel listSelectionModel) {
+ return ListSelectionEventSource.fromListSelectionEventsOf(listSelectionModel);
+ }
+
+ /**
+ * Creates an observable corresponding to property change events.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable of property change events for the given component
+ */
+ public static Observable fromPropertyChangeEvents(Component component) {
+ return PropertyChangeEventSource.fromPropertyChangeEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to property change events filtered by property name.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @param propertyName
+ * A property name to filter the property events on.
+ * @return Observable of property change events for the given component, filtered by the provided property name
+ */
+ public static Observable fromPropertyChangeEvents(Component component, final String propertyName) {
+ return fromPropertyChangeEvents(component).filter(event -> event.getPropertyName().equals(propertyName));
+ }
+
+ /**
+ * @param window
+ * The window to register the observable for
+ * @return Observable of window events for the given window
+ */
+ public static Observable fromWindowEventsOf(Window window) {
+ return WindowEventSource.fromWindowEventsOf(window);
+ }
+
+ /**
+ * Creates an observable corresponding to document events.
+ *
+ * @param document The document to register the observable for.
+ * @return Observable of document events.
+ */
+ public static Observable fromDocumentEvents(Document document) {
+ return DocumentEventSource.fromDocumentEventsOf(document);
+ }
+
+ /**
+ * Creates an observable corresponding to document events restricted to a
+ * set of given event types.
+ *
+ * @param document The document to register the observable for.
+ * @param eventTypes The set of event types for which the observable should
+ * emit document events.
+ * @return Observable of document events.
+ */
+ public static Observable fromDocumentEvents(Document document, final Set eventTypes) {
+ return fromDocumentEvents(document).filter(event -> eventTypes.contains(event.getType()));
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. tab selection).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(JTabbedPane component) {
+ return ChangeEventSource.fromChangeEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. value changes).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(JSlider component) {
+ return ChangeEventSource.fromChangeEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. value changes).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener and
+ * How to Use Spinners.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(JSpinner component) {
+ return ChangeEventSource.fromChangeEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. value changes).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener and
+ * How to Use Spinners.
+ *
+ * @param spinnerModel
+ * The spinnerModel to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(SpinnerModel spinnerModel) {
+ return ChangeEventSource.fromChangeEventsOf(spinnerModel);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. button clicks changes).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(AbstractButton component) {
+ return ChangeEventSource.fromChangeEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. button clicks changes).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param buttonModel
+ * The buttonModel to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(ButtonModel buttonModel) {
+ return ChangeEventSource.fromChangeEventsOf(buttonModel);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. scrolling).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(JViewport component) {
+ return ChangeEventSource.fromChangeEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. from a color chooser).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param colorSelectionModel
+ * The colorSelectionModel to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(ColorSelectionModel colorSelectionModel) {
+ return ChangeEventSource.fromChangeEventsOf(colorSelectionModel);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. progressbar value changes).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param component
+ * The component to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(JProgressBar component) {
+ return ChangeEventSource.fromChangeEventsOf(component);
+ }
+
+ /**
+ * Creates an observable corresponding to change events (e.g. progressbar value changes).
+ *
+ * For more info to change listeners and events see
+ * How to Write a Change Listener.
+ *
+ * @param boundedRangeModel
+ * The boundedRangeModel to register the observable for.
+ * @return Observable emitting the change events.
+ */
+ public static Observable fromChangeEvents(BoundedRangeModel boundedRangeModel) {
+ 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 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);
+ }
+
+ /**
+ * Creates an observable corresponding to hierarchy bounds events (e.g. parent resized).
+ *
+ * @param component
+ * The {@link Component} to register the observable for.
+ * @return Observable emitting hierarchy bounds events for the provided component.
+ */
+ public static Observable fromHierachyBoundsEvents(Component component) {
+ return HierarchyEventSource.fromHierarchyBoundsEventsOf(component);
+ }
+
+ /**
+ * Check if the current thead is the event dispatch thread.
+ *
+ * @throws IllegalStateException if the current thread is not the event dispatch thread.
+ */
+ public static void assertEventDispatchThread() {
+ if (!SwingUtilities.isEventDispatchThread()) {
+ throw new IllegalStateException("Need to run in the event dispatch thread, but was " + Thread.currentThread());
+ }
+ }
+}
diff --git a/src/main/java/rx/schedulers/SwingScheduler.java b/src/main/java/rx/schedulers/SwingScheduler.java
new file mode 100644
index 0000000..858aab2
--- /dev/null
+++ b/src/main/java/rx/schedulers/SwingScheduler.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright 2014 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.schedulers;
+
+import java.awt.EventQueue;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+import io.reactivex.Scheduler;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+
+/**
+ * Executes work on the Swing UI thread.
+ * This scheduler should only be used with actions that execute quickly.
+ *
+ * If the calling thread is the Swing UI thread, and no delay parameter is
+ * provided, the action will run immediately. Otherwise, if the calling
+ * thread is NOT the Swing UI thread, the action will be deferred until
+ * all pending UI events have been processed.
+ */
+public final class SwingScheduler extends Scheduler {
+ private static final SwingScheduler INSTANCE = new SwingScheduler();
+
+ public static SwingScheduler getInstance() {
+ return INSTANCE;
+ }
+
+ /* package for unit test */ SwingScheduler() {
+ }
+
+ @Override
+ public Worker createWorker() {
+ return new InnerSwingScheduler();
+ }
+
+ private static class InnerSwingScheduler extends Worker {
+
+ private final CompositeDisposable innerSubscription = new CompositeDisposable();
+
+ @Override
+ public Disposable schedule(final Runnable action, long delayTime, TimeUnit unit) {
+ long delay = Math.max(0, unit.toMillis(delayTime));
+ assertThatTheDelayIsValidForTheSwingTimer(delay);
+
+
+ if(delayTime == 0){
+ return scheduleNow(action);
+ }
+
+ class ExecuteOnceAction implements ActionListener {
+ private Timer timer;
+
+ private void setTimer(Timer timer) {
+ this.timer = timer;
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ timer.stop();
+ if (innerSubscription.isDisposed()) {
+ return;
+ }
+ action.run();
+ }
+ }
+
+ ExecuteOnceAction executeOnce = new ExecuteOnceAction();
+ final Timer timer = new Timer((int) delay, executeOnce);
+ executeOnce.setTimer(timer);
+ timer.start();
+
+ return innerSubscription;
+ }
+
+ @Override
+ public Disposable schedule(final Runnable action) {
+ return scheduleNow(action);
+ }
+
+ private Disposable scheduleNow(final Runnable action) {
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ if (innerSubscription.isDisposed()) {
+ return;
+ }
+ action.run();
+ }
+ };
+
+ if (SwingUtilities.isEventDispatchThread()) {
+ runnable.run();
+ } else {
+ EventQueue.invokeLater(runnable);
+ }
+
+ return innerSubscription;
+ }
+
+ @Override
+ public void dispose() {
+ innerSubscription.dispose();
+
+ }
+
+ @Override
+ public boolean isDisposed() {
+ return innerSubscription.isDisposed();
+ }
+
+ }
+
+ private static void assertThatTheDelayIsValidForTheSwingTimer(long delay) {
+ if (delay < 0 || delay > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException(String.format("The swing timer only accepts non-negative delays up to %d milliseconds.", Integer.MAX_VALUE));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/rx/swing/sources/AbstractButtonSource.java b/src/main/java/rx/swing/sources/AbstractButtonSource.java
new file mode 100644
index 0000000..9fa592f
--- /dev/null
+++ b/src/main/java/rx/swing/sources/AbstractButtonSource.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2014 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.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.AbstractButton;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum AbstractButtonSource { ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromButtonAction
+ */
+ public static Observable fromActionOf(final AbstractButton button) {
+
+ return Observable.create(new ObservableOnSubscribe() {
+
+
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) throws Exception {
+ final ActionListener listener = new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ subscriber.onNext(e);
+ }
+ };
+ button.addActionListener(listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+ button.removeActionListener(listener);
+ }));
+
+
+
+ }
+ }).subscribeOn(SwingScheduler.getInstance())
+ .unsubscribeOn(SwingScheduler.getInstance());
+ }
+}
diff --git a/src/main/java/rx/swing/sources/ChangeEventSource.java b/src/main/java/rx/swing/sources/ChangeEventSource.java
new file mode 100644
index 0000000..68c32c7
--- /dev/null
+++ b/src/main/java/rx/swing/sources/ChangeEventSource.java
@@ -0,0 +1,116 @@
+/**
+ * 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.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum ChangeEventSource {
+ ; // no instances
+
+ private static final String ADD_CHANGE_LISTENER_METHOD_NAME = "addChangeListener";
+ private static final String REMOVE_CHANGE_LISTENER_METHOD_NAME = "removeChangeListener";
+
+ /**
+ * Creates an observable corresponding to change events (e.g. progressbar value changes).
+ *
+ * Due to the lack of a common interface in Java (up to at least version 8), the implementation is generic and uses internally reflection to add and remove
+ * it's {@link ChangeListener}'s. The contract is therefor that the given parameter object MUST have the typical two public methods "addChangeListener"
+ * (like {@link javax.swing.JProgressBar#addChangeListener(ChangeListener)}) and "removeChangeListener" (like
+ * {@link javax.swing.JProgressBar#removeChangeListener(ChangeListener)}).
+ *
+ * For more info to change listeners and events see How to Write a
+ * Change Listener.
+ *
+ * @param changeEventSource
+ * The object to register the observable for.
+ * @return Observable emitting the change events.
+ * @throws IllegalArgumentException
+ * if the given parameter object has not the needed signature
+ */
+ public static Observable fromChangeEventsOf(final Object changeEventSource) {
+ checkHasChangeListenerSupport(changeEventSource);
+ return Observable.create(new ObservableOnSubscribe() {
+
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) throws Exception {
+ final ChangeListener listener = new ChangeListener() {
+ @Override
+ public void stateChanged(final ChangeEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ addChangeListener(changeEventSource, listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ removeChangeListener(changeEventSource, listener);
+
+ }));
+
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ private static void checkHasChangeListenerSupport(Object object) {
+ checkPublicMethodExists(object, ADD_CHANGE_LISTENER_METHOD_NAME, ChangeListener.class);
+ checkPublicMethodExists(object, REMOVE_CHANGE_LISTENER_METHOD_NAME, ChangeListener.class);
+ }
+
+ private static void checkPublicMethodExists(Object object, String methodName, Class>... parameterTypes) {
+ try {
+ Method method = object.getClass().getMethod(methodName, parameterTypes);
+ if (!Modifier.isPublic(method.getModifiers())) {
+ throw new IllegalArgumentException("Class '" + object.getClass().getName() + "' has not the expected signature to support change listeners in "
+ + ChangeEventSource.class.getName() + ". " + methodName + " is not accessible.");
+ }
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Class '" + object.getClass().getName() + "' has not the expected signature to support change listeners in "
+ + ChangeEventSource.class.getName(), e);
+ }
+ }
+
+ private static void addChangeListener(Object object, ChangeListener changeListener) {
+ callChangeListenerMethodViaReflection(object, ADD_CHANGE_LISTENER_METHOD_NAME, changeListener);
+ }
+
+ private static void removeChangeListener(Object object, ChangeListener changeListener) {
+ callChangeListenerMethodViaReflection(object, REMOVE_CHANGE_LISTENER_METHOD_NAME, changeListener);
+ }
+
+ private static void callChangeListenerMethodViaReflection(Object object, String methodName, ChangeListener changeListener) {
+ try {
+ object.getClass().getMethod(methodName, ChangeListener.class).invoke(object, changeListener);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(
+ "Call of " + methodName + " via reflection failed. Does class " + object.getClass().getName() + " support change listeners?", e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalArgumentException("Call of " + methodName + " via reflection failed.", e);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(
+ "Call of " + methodName + " via reflection failed. Does class " + object.getClass().getName() + " support change listeners?", e);
+ }
+ }
+}
diff --git a/src/main/java/rx/swing/sources/ComponentEventSource.java b/src/main/java/rx/swing/sources/ComponentEventSource.java
new file mode 100644
index 0000000..8810baa
--- /dev/null
+++ b/src/main/java/rx/swing/sources/ComponentEventSource.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2014 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.Dimension;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import io.reactivex.functions.Predicate;
+import rx.observables.SwingObservable;
+import rx.schedulers.SwingScheduler;
+
+public enum ComponentEventSource {
+ ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromComponentEvents
+ */
+ public static Observable fromComponentEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+
+ @Override
+ public void subscribe(ObservableEmitter subscriber) throws Exception {
+ final ComponentListener listener = new ComponentListener() {
+ @Override
+ public void componentHidden(ComponentEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void componentMoved(ComponentEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void componentResized(ComponentEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void componentShown(ComponentEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ component.addComponentListener(listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+ component.removeComponentListener(listener);
+ }));
+
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ /**
+ * @see SwingObservable#fromResizing
+ */
+ public static Observable fromResizing(final Component component) {
+ return fromComponentEventsOf(component).filter(Predicates.RESIZED).map(event -> event.getComponent().getSize());
+ }
+
+ /**
+ * Predicates that help with filtering observables for specific component events.
+ */
+ public enum Predicates implements Predicate {
+ RESIZED(ComponentEvent.COMPONENT_RESIZED), HIDDEN(ComponentEvent.COMPONENT_HIDDEN), MOVED(ComponentEvent.COMPONENT_MOVED), SHOWN(
+ ComponentEvent.COMPONENT_SHOWN);
+
+ private final int id;
+
+ private Predicates(int id) {
+ this.id = id;
+ }
+
+
+ @Override
+ public boolean test(ComponentEvent event) throws Exception {
+ return event.getID() == id;
+ }
+ }
+}
diff --git a/src/main/java/rx/swing/sources/ContainerEventSource.java b/src/main/java/rx/swing/sources/ContainerEventSource.java
new file mode 100644
index 0000000..2fbb101
--- /dev/null
+++ b/src/main/java/rx/swing/sources/ContainerEventSource.java
@@ -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 io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import io.reactivex.functions.Predicate;
+import rx.schedulers.SwingScheduler;
+
+public enum ContainerEventSource {
+ ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromContainerEvents
+ */
+ public static Observable fromContainerEventsOf(final Container container) {
+ return Observable.create(new ObservableOnSubscribe() {
+
+ @Override
+ public void subscribe(ObservableEmitter subscriber) throws Exception {
+ 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.setDisposable(Disposables.fromAction(() -> {
+
+ container.removeContainerListener(listener);
+
+ }));
+
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).observeOn(SwingScheduler.getInstance());
+ }
+
+ public static enum Predicates implements Predicate {
+ COMPONENT_ADDED(ContainerEvent.COMPONENT_ADDED), COMPONENT_REMOVED(ContainerEvent.COMPONENT_REMOVED);
+
+ private final int id;
+
+ private Predicates(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public boolean test(ContainerEvent event) {
+ return event.getID() == id;
+ }
+ }
+}
diff --git a/src/main/java/rx/swing/sources/DocumentEventSource.java b/src/main/java/rx/swing/sources/DocumentEventSource.java
new file mode 100644
index 0000000..0745b7c
--- /dev/null
+++ b/src/main/java/rx/swing/sources/DocumentEventSource.java
@@ -0,0 +1,64 @@
+/**
+ * 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 javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.Document;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum DocumentEventSource {
+ ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromDocumentEvents(Document)
+ */
+ public static Observable fromDocumentEventsOf(final Document document) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(ObservableEmitter subscriber) throws Exception {
+
+ final DocumentListener listener = new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ document.addDocumentListener(listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+ document.removeDocumentListener(listener);
+ }));
+
+ }
+
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+}
diff --git a/src/main/java/rx/swing/sources/FocusEventSource.java b/src/main/java/rx/swing/sources/FocusEventSource.java
new file mode 100644
index 0000000..0b1435f
--- /dev/null
+++ b/src/main/java/rx/swing/sources/FocusEventSource.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2014 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.FocusEvent;
+import java.awt.event.FocusListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import io.reactivex.functions.Predicate;
+import rx.schedulers.SwingScheduler;
+
+public enum FocusEventSource { ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromFocusEvents
+ */
+ public static Observable fromFocusEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(ObservableEmitter subscriber) {
+ final FocusListener listener = new FocusListener() {
+
+ @Override
+ public void focusGained(FocusEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void focusLost(FocusEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ component.addFocusListener(listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ component.removeFocusListener(listener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance())
+ .unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ /**
+ * Predicates that help with filtering observables for specific focus events.
+ */
+ public enum Predicates implements Predicate {
+ FOCUS_GAINED(FocusEvent.FOCUS_GAINED),
+ FOCUS_LOST(FocusEvent.FOCUS_LOST);
+
+ private final int id;
+
+ private Predicates(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public boolean test(FocusEvent event) {
+ return event.getID() == id;
+ }
+ }
+}
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..a3a9a15
--- /dev/null
+++ b/src/main/java/rx/swing/sources/HierarchyEventSource.java
@@ -0,0 +1,98 @@
+/**
+ * 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.HierarchyBoundsListener;
+import java.awt.event.HierarchyEvent;
+import java.awt.event.HierarchyListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import io.reactivex.functions.Predicate;
+import rx.schedulers.SwingScheduler;
+
+public enum HierarchyEventSource {
+ ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromHierachyEvents
+ */
+ public static Observable fromHierarchyEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final HierarchyListener hiearchyListener = new HierarchyListener() {
+ @Override
+ public void hierarchyChanged(HierarchyEvent e) {
+ subscriber.onNext(e);
+ }
+ };
+ component.addHierarchyListener(hiearchyListener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ component.removeHierarchyListener(hiearchyListener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ /**
+ * @see rx.observables.SwingObservable#fromHierachyBoundsEvents
+ */
+ public static Observable fromHierarchyBoundsEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final HierarchyBoundsListener hiearchyBoundsListener = new HierarchyBoundsListener() {
+ @Override
+ public void ancestorMoved(HierarchyEvent e) {
+ subscriber.onNext(e);
+ }
+
+ @Override
+ public void ancestorResized(HierarchyEvent e) {
+ subscriber.onNext(e);
+ }
+ };
+ component.addHierarchyBoundsListener(hiearchyBoundsListener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ component.removeHierarchyBoundsListener(hiearchyBoundsListener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ public static enum Predicates implements Predicate {
+ ANCESTOR_RESIZED(HierarchyEvent.ANCESTOR_RESIZED), ANCESTOR_MOVED(HierarchyEvent.ANCESTOR_MOVED);
+
+ private final int id;
+
+ private Predicates(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public boolean test(HierarchyEvent event) {
+ return event.getID() == id;
+ }
+ }
+}
diff --git a/src/main/java/rx/swing/sources/ItemEventSource.java b/src/main/java/rx/swing/sources/ItemEventSource.java
new file mode 100644
index 0000000..127d5c2
--- /dev/null
+++ b/src/main/java/rx/swing/sources/ItemEventSource.java
@@ -0,0 +1,48 @@
+/**
+ * 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.ItemSelectable;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum ItemEventSource {
+ ; // no instances
+
+ public static Observable fromItemEventsOf(final ItemSelectable itemSelectable) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final ItemListener listener = new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ itemSelectable.addItemListener(listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+ itemSelectable.removeItemListener(listener);
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+}
diff --git a/src/main/java/rx/swing/sources/KeyEventSource.java b/src/main/java/rx/swing/sources/KeyEventSource.java
new file mode 100644
index 0000000..db8ce1f
--- /dev/null
+++ b/src/main/java/rx/swing/sources/KeyEventSource.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2014 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.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import io.reactivex.functions.BiFunction;
+import rx.schedulers.SwingScheduler;
+
+public enum KeyEventSource { ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromKeyEvents(Component)
+ */
+ public static Observable fromKeyEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final KeyListener listener = new KeyListener() {
+ @Override
+ public void keyPressed(KeyEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void keyReleased(KeyEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void keyTyped(KeyEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ component.addKeyListener(listener);
+
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ component.removeKeyListener(listener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance())
+ .unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ /**
+ * @see rx.observables.SwingObservable#fromPressedKeys(Component)
+ */
+ public static Observable> currentlyPressedKeysOf(Component component) {
+ class CollectKeys implements BiFunction, KeyEvent, Set>{
+
+
+ @Override
+ public Set apply(Set t1, KeyEvent event) throws Exception {
+ Set afterEvent = new HashSet(t1);
+ 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;
+ }
+ }
+
+ Observable filteredKeyEvents = fromKeyEventsOf(component).filter(event -> event.getID() == KeyEvent.KEY_PRESSED || event.getID() == KeyEvent.KEY_RELEASED);
+
+ return filteredKeyEvents.scan(Collections.emptySet(), new CollectKeys());
+ }
+
+}
diff --git a/src/main/java/rx/swing/sources/ListSelectionEventSource.java b/src/main/java/rx/swing/sources/ListSelectionEventSource.java
new file mode 100644
index 0000000..694555e
--- /dev/null
+++ b/src/main/java/rx/swing/sources/ListSelectionEventSource.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 javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum ListSelectionEventSource { ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromListSelectionEvents(ListSelectionModel)
+ */
+ public static Observable fromListSelectionEventsOf(final ListSelectionModel listSelectionModel) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final ListSelectionListener listener = new ListSelectionListener() {
+ @Override
+ public void valueChanged(final ListSelectionEvent event) {
+ subscriber.onNext(event);
+ }
+
+ };
+ listSelectionModel.addListSelectionListener(listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ listSelectionModel.removeListSelectionListener(listener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance())
+ .unsubscribeOn(SwingScheduler.getInstance());
+ }
+}
diff --git a/src/main/java/rx/swing/sources/MouseEventSource.java b/src/main/java/rx/swing/sources/MouseEventSource.java
new file mode 100644
index 0000000..cae6e21
--- /dev/null
+++ b/src/main/java/rx/swing/sources/MouseEventSource.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright 2014 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.Point;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum MouseEventSource {
+ ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromMouseEvents
+ */
+ public static Observable fromMouseEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final MouseListener listener = new MouseListener() {
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void mousePressed(MouseEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void mouseExited(MouseEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ component.addMouseListener(listener);
+
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ component.removeMouseListener(listener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ /**
+ * @see rx.observables.SwingObservable#fromMouseMotionEvents
+ */
+ public static Observable fromMouseMotionEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final MouseMotionListener listener = new MouseMotionListener() {
+ @Override
+ public void mouseDragged(MouseEvent event) {
+ subscriber.onNext(event);
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ component.addMouseMotionListener(listener);
+
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ component.removeMouseMotionListener(listener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ public static Observable fromMouseWheelEvents(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final MouseWheelListener listener = new MouseWheelListener() {
+ @Override
+ public void mouseWheelMoved(MouseWheelEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ component.addMouseWheelListener(listener);
+
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ component.removeMouseWheelListener(listener);
+
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+ /**
+ * @see rx.observables.SwingObservable#fromRelativeMouseMotion
+ */
+ public static Observable fromRelativeMouseMotion(final Component component) {
+ final Observable events = fromMouseMotionEventsOf(component);
+ return Observable.zip(events, events.skip(1), (ev1, ev2) -> new Point(ev2.getX() - ev1.getX(), ev2.getY() - ev1.getY()))
+ .subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+
+}
diff --git a/src/main/java/rx/swing/sources/PropertyChangeEventSource.java b/src/main/java/rx/swing/sources/PropertyChangeEventSource.java
new file mode 100644
index 0000000..8f4196f
--- /dev/null
+++ b/src/main/java/rx/swing/sources/PropertyChangeEventSource.java
@@ -0,0 +1,48 @@
+/**
+ * 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.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum PropertyChangeEventSource { ; // no instances
+
+ public static Observable fromPropertyChangeEventsOf(final Component component) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(final ObservableEmitter subscriber) {
+ final PropertyChangeListener listener = new PropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent event) {
+ subscriber.onNext(event);
+ }
+ };
+ component.addPropertyChangeListener(listener);
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+ component.removePropertyChangeListener(listener);
+ }));
+ }
+ }).subscribeOn(SwingScheduler.getInstance())
+ .unsubscribeOn(SwingScheduler.getInstance());
+ }
+}
diff --git a/src/main/java/rx/swing/sources/SwingTestHelper.java b/src/main/java/rx/swing/sources/SwingTestHelper.java
new file mode 100644
index 0000000..b2ab0ba
--- /dev/null
+++ b/src/main/java/rx/swing/sources/SwingTestHelper.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright 2014 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.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Scheduler.Worker;
+import io.reactivex.functions.Action;
+import rx.schedulers.SwingScheduler;
+
+/* package-private */final class SwingTestHelper { // only for test
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private volatile Throwable error;
+
+ private SwingTestHelper() {
+ }
+
+ public static SwingTestHelper create() {
+ return new SwingTestHelper();
+ }
+
+ public SwingTestHelper runInEventDispatchThread(final Action action) {
+ Worker inner = SwingScheduler.getInstance().createWorker();
+ inner.schedule(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ action.run();
+ } catch (Throwable e) {
+ error = e;
+ }
+ latch.countDown();
+ }
+ });
+ return this;
+ }
+
+ public void awaitTerminal() throws Throwable {
+ latch.await();
+ if (error != null) {
+ throw error;
+ }
+ }
+
+ public void awaitTerminal(long timeout, TimeUnit unit) throws Throwable {
+ latch.await(timeout, unit);
+ if (error != null) {
+ throw error;
+ }
+ }
+
+}
diff --git a/src/main/java/rx/swing/sources/WindowEventSource.java b/src/main/java/rx/swing/sources/WindowEventSource.java
new file mode 100644
index 0000000..f29942a
--- /dev/null
+++ b/src/main/java/rx/swing/sources/WindowEventSource.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright 2015 Netflix
+ *
+ * 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.Window;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.disposables.Disposables;
+import rx.schedulers.SwingScheduler;
+
+public enum WindowEventSource {
+ ; // no instances
+
+ /**
+ * @see rx.observables.SwingObservable#fromWindowEventsOf(Window)
+ */
+ public static Observable fromWindowEventsOf(final Window window) {
+ return Observable.create(new ObservableOnSubscribe() {
+
+ @Override
+ public void subscribe(ObservableEmitter subscriber) throws Exception {
+ final WindowListener windowListener = new WindowListener() {
+ @Override
+ public void windowOpened(WindowEvent windowEvent) {
+ subscriber.onNext(windowEvent);
+ }
+
+ @Override
+ public void windowClosing(WindowEvent windowEvent) {
+ subscriber.onNext(windowEvent);
+ }
+
+ @Override
+ public void windowClosed(WindowEvent windowEvent) {
+ subscriber.onNext(windowEvent);
+ }
+
+ @Override
+ public void windowIconified(WindowEvent windowEvent) {
+ subscriber.onNext(windowEvent);
+ }
+
+ @Override
+ public void windowDeiconified(WindowEvent windowEvent) {
+ subscriber.onNext(windowEvent);
+ }
+
+ @Override
+ public void windowActivated(WindowEvent windowEvent) {
+ subscriber.onNext(windowEvent);
+ }
+
+ @Override
+ public void windowDeactivated(WindowEvent windowEvent) {
+ subscriber.onNext(windowEvent);
+ }
+ };
+
+ window.addWindowListener(windowListener);
+
+ subscriber.setDisposable(Disposables.fromAction(() -> {
+
+ window.removeWindowListener(windowListener);
+
+ }));
+
+ }
+ }).subscribeOn(SwingScheduler.getInstance()).unsubscribeOn(SwingScheduler.getInstance());
+ }
+}
diff --git a/src/test/java/rx/schedulers/SwingSchedulerTest.java b/src/test/java/rx/schedulers/SwingSchedulerTest.java
new file mode 100644
index 0000000..fbf6992
--- /dev/null
+++ b/src/test/java/rx/schedulers/SwingSchedulerTest.java
@@ -0,0 +1,164 @@
+/**
+ * Copyright 2014 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.schedulers;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.awt.EventQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.SwingUtilities;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.InOrder;
+
+import io.reactivex.Scheduler.Worker;
+import io.reactivex.functions.Action;
+
+/**
+ * Executes work on the Swing UI thread.
+ * This scheduler should only be used with actions that execute quickly.
+ */
+public final class SwingSchedulerTest {
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void testInvalidDelayValues() {
+ final SwingScheduler scheduler = new SwingScheduler();
+ final Worker inner = scheduler.createWorker();
+ final Runnable action = mock(Runnable.class);
+
+ inner.schedulePeriodically(action, -1L, 100L, TimeUnit.SECONDS);
+
+ inner.schedulePeriodically(action, 100L, -1L, TimeUnit.SECONDS);
+
+ exception.expect(IllegalArgumentException.class);
+ inner.schedulePeriodically(action, 1L + Integer.MAX_VALUE, 100L, TimeUnit.MILLISECONDS);
+
+ exception.expect(IllegalArgumentException.class);
+ inner.schedulePeriodically(action, 100L, 1L + Integer.MAX_VALUE / 1000, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void testPeriodicScheduling() throws Exception {
+ final SwingScheduler scheduler = new SwingScheduler();
+ final Worker inner = scheduler.createWorker();
+
+ final CountDownLatch latch = new CountDownLatch(4);
+
+ final Action innerAction = mock(Action.class);
+ final Runnable action = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ try {
+ innerAction.run();
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ assertTrue(SwingUtilities.isEventDispatchThread());
+ } finally {
+ latch.countDown();
+ }
+ }
+ };
+
+ inner.schedulePeriodically(action, 50, 200, TimeUnit.MILLISECONDS);
+
+ if (!latch.await(5000, TimeUnit.MILLISECONDS)) {
+ fail("timed out waiting for tasks to execute");
+ }
+
+ inner.dispose();
+ waitForEmptyEventQueue();
+ verify(innerAction, times(4)).run();
+ }
+
+ @Test
+ public void testNestedActions() throws Exception {
+ final SwingScheduler scheduler = new SwingScheduler();
+ final Worker inner = scheduler.createWorker();
+
+ final Runnable firstStepStart = mock(Runnable.class);
+ final Runnable firstStepEnd = mock(Runnable.class);
+
+ final Runnable secondStepStart = mock(Runnable.class);
+ final Runnable secondStepEnd = mock(Runnable.class);
+
+ final Runnable thirdStepStart = mock(Runnable.class);
+ final Runnable thirdStepEnd = mock(Runnable.class);
+
+ final Runnable firstAction = new Runnable() {
+ @Override
+ public void run() {
+ assertTrue(SwingUtilities.isEventDispatchThread());
+ firstStepStart.run();
+ firstStepEnd.run();
+ }
+ };
+ final Runnable secondAction = new Runnable() {
+ @Override
+ public void run() {
+ assertTrue(SwingUtilities.isEventDispatchThread());
+ secondStepStart.run();
+ inner.schedule(firstAction);
+ secondStepEnd.run();
+ }
+ };
+ final Runnable thirdAction = new Runnable() {
+ @Override
+ public void run() {
+ assertTrue(SwingUtilities.isEventDispatchThread());
+ thirdStepStart.run();
+ inner.schedule(secondAction);
+ thirdStepEnd.run();
+ }
+ };
+
+ InOrder inOrder = inOrder(firstStepStart, firstStepEnd, secondStepStart, secondStepEnd, thirdStepStart, thirdStepEnd);
+
+ inner.schedule(thirdAction);
+ waitForEmptyEventQueue();
+
+ inOrder.verify(thirdStepStart, times(1)).run();
+ inOrder.verify(secondStepStart, times(1)).run();
+ inOrder.verify(firstStepStart, times(1)).run();
+ inOrder.verify(firstStepEnd, times(1)).run();
+ inOrder.verify(secondStepEnd, times(1)).run();
+ inOrder.verify(thirdStepEnd, times(1)).run();
+ }
+
+ private static void waitForEmptyEventQueue() throws Exception {
+ EventQueue.invokeAndWait(new Runnable() {
+ @Override
+ public void run() {
+ // nothing to do, we're just waiting here for the event queue to be emptied
+ }
+ });
+ }
+
+}
diff --git a/src/test/java/rx/swing/sources/AbstractButtonSourceTest.java b/src/test/java/rx/swing/sources/AbstractButtonSourceTest.java
new file mode 100644
index 0000000..45721e2
--- /dev/null
+++ b/src/test/java/rx/swing/sources/AbstractButtonSourceTest.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2014 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 javax.swing.AbstractButton;
+
+import org.junit.Test;
+import org.mockito.Matchers;
+
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Action;
+import io.reactivex.functions.Consumer;
+
+public class AbstractButtonSourceTest {
+ @Test
+ public void testObservingActionEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() throws Exception {
+ @SuppressWarnings("unchecked")
+ Consumer action = mock(Consumer.class);
+ @SuppressWarnings("unchecked")
+ Consumer error = mock(Consumer.class);
+ Action complete = mock(Action.class);
+
+ final ActionEvent event = new ActionEvent(this, 1, "command");
+
+ @SuppressWarnings("serial")
+ class TestButton extends AbstractButton {
+ void testAction() {
+ fireActionPerformed(event);
+ }
+ }
+
+ TestButton button = new TestButton();
+ Disposable sub = AbstractButtonSource.fromActionOf(button).subscribe(action,
+ error, complete);
+
+ verify(action, never()).accept(Matchers. any());
+ verify(error, never()).accept(Matchers. any());
+ verify(complete, never()).run();
+
+ button.testAction();
+ verify(action, times(1)).accept(Matchers. any());
+
+ button.testAction();
+ verify(action, times(2)).accept(Matchers. any());
+
+ sub.dispose();
+ button.testAction();
+ verify(action, times(2)).accept(Matchers. any());
+ verify(error, never()).accept(Matchers. any());
+ verify(complete, never()).run();
+ }
+
+ }).awaitTerminal();
+ }
+}
diff --git a/src/test/java/rx/swing/sources/ChangeEventSourceTest.java b/src/test/java/rx/swing/sources/ChangeEventSourceTest.java
new file mode 100644
index 0000000..9637f3b
--- /dev/null
+++ b/src/test/java/rx/swing/sources/ChangeEventSourceTest.java
@@ -0,0 +1,499 @@
+/**
+ * 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.awt.Color;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.AbstractButton;
+import javax.swing.BoundedRangeModel;
+import javax.swing.ButtonModel;
+import javax.swing.JButton;
+import javax.swing.JColorChooser;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JScrollPane;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JTable;
+import javax.swing.JViewport;
+import javax.swing.SpinnerListModel;
+import javax.swing.SpinnerModel;
+import javax.swing.colorchooser.ColorSelectionModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import io.reactivex.functions.Action;
+import io.reactivex.observers.TestObserver;
+
+public class ChangeEventSourceTest {
+
+ @Test
+ public void jTabbedPane_observingSelectionEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JTabbedPane tabbedPane = createTabbedPane();
+ ChangeEventSource.fromChangeEventsOf(tabbedPane)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ tabbedPane.setSelectedIndex(2);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(tabbedPane, ((ChangeEvent)(testSubscriber.getEvents().get(0)).get(0)).getSource());
+
+ tabbedPane.setSelectedIndex(0);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(tabbedPane, ((ChangeEvent)(testSubscriber.getEvents().get(0).get(1))).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void jSlider_observingValueChangeEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JSlider slider = new JSlider();
+ slider.setMinimum(0);
+ slider.setMaximum(10);
+ ChangeEventSource.fromChangeEventsOf(slider)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ slider.setValue(5);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(slider, testSubscriber.values().get(0).getSource());
+
+ slider.setValue(8);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(slider, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void jSpinner_observingValueChangeEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JSpinner spinner = createSpinner();
+ ChangeEventSource.fromChangeEventsOf(spinner)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ spinner.setValue("2015");
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(spinner, testSubscriber.values().get(0).getSource());
+
+ spinner.setValue("2016");
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(spinner, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void spinnerModel_observingValueChangeEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JSpinner spinner = createSpinner();
+ final SpinnerModel spinnerModel = spinner.getModel();
+ ChangeEventSource.fromChangeEventsOf(spinnerModel)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ spinner.setValue("2015");
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(spinnerModel, testSubscriber.values().get(0).getSource());
+
+ spinner.setValue("2016");
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(spinnerModel, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void abstractButton_observingPressedEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ AbstractButton button = new JButton("Click me");
+ ChangeEventSource.fromChangeEventsOf(button)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ button.getModel().setPressed(true);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(button, testSubscriber.values().get(0).getSource());
+
+ button.getModel().setPressed(false);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(button, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void buttonModel_observingPressedEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ AbstractButton button = new JButton("Click me");
+ final ButtonModel buttonModel = button.getModel();
+ ChangeEventSource.fromChangeEventsOf(buttonModel)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ buttonModel.setPressed(true);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(buttonModel, testSubscriber.values().get(0).getSource());
+
+ buttonModel.setPressed(false);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(buttonModel, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void jViewPort_observingScrollEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JTable table = new JTable(1000, 5);
+ JScrollPane scrollPane = new JScrollPane(table);
+ final JViewport viewPort = scrollPane.getViewport();
+ ChangeEventSource.fromChangeEventsOf(viewPort)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ // scoll down
+ table.scrollRectToVisible(table.getCellRect(table.getModel().getRowCount() - 1, 0, false));
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(viewPort, testSubscriber.values().get(0).getSource());
+
+ // scoll up
+ table.scrollRectToVisible(table.getCellRect(0, 0, false));
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(viewPort, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void colorSelectionModel_observingColorChooserEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JColorChooser colorChooser = new JColorChooser();
+ final ColorSelectionModel colorSelectionModel = colorChooser.getSelectionModel();
+ ChangeEventSource.fromChangeEventsOf(colorSelectionModel)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ colorChooser.setColor(Color.BLUE);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(colorSelectionModel, testSubscriber.values().get(0).getSource());
+
+ colorChooser.setColor(Color.GREEN);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(colorSelectionModel, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void jProgressBar_observingProgressEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JProgressBar progressBar = new JProgressBar();
+ progressBar.setMinimum(0);
+ progressBar.setMaximum(10);
+ ChangeEventSource.fromChangeEventsOf(progressBar)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ progressBar.setValue(1);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(progressBar, testSubscriber.values().get(0).getSource());
+
+ progressBar.setValue(2);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(progressBar, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void boundedRangeModel_observingProgressEvents() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JProgressBar progressBar = new JProgressBar();
+ progressBar.setMinimum(0);
+ progressBar.setMaximum(10);
+ final BoundedRangeModel boundedRangeModel = progressBar.getModel();
+ ChangeEventSource.fromChangeEventsOf(boundedRangeModel)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ progressBar.setValue(1);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(1);
+
+ assertEquals(boundedRangeModel, testSubscriber.values().get(0).getSource());
+
+ progressBar.setValue(2);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertValueCount(2);
+
+ assertEquals(boundedRangeModel, testSubscriber.values().get(1).getSource());
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void unsubscribeRemovesRowSelectionListener() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JTabbedPane tabbedPane = createTabbedPane();
+ int numberOfListenersBefore = tabbedPane.getChangeListeners().length;
+
+ ChangeEventSource.fromChangeEventsOf(tabbedPane)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ testSubscriber.dispose();
+
+ Assert.assertTrue(testSubscriber.isDisposed());
+
+ tabbedPane.setSelectedIndex(2);
+
+ testSubscriber.assertNoErrors();
+ testSubscriber.assertNoValues();
+
+ assertEquals(numberOfListenersBefore, tabbedPane.getChangeListeners().length);
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void fromChangeEventsOf_usingObjectWithoutExpectedChangeListenerSupport_failsFastWithException() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+ @Override
+ public void run() {
+ try {
+ ChangeEventSource.fromChangeEventsOf("doesNotSupportChangeListeners").subscribe(TestObserver.create());
+ fail(IllegalArgumentException.class.getSimpleName() + " expected");
+ } catch (IllegalArgumentException ex) {
+ assertEquals("Class 'java.lang.String' has not the expected signature to support change listeners in rx.swing.sources.ChangeEventSource",
+ ex.getMessage());
+ }
+
+ Object changeEventSource = null;
+ try {
+ changeEventSource = new Object() {
+ private void addChangeListener(ChangeListener changeListener) {/* no-op */ }
+
+ private void removeChangeListener(ChangeListener changeListener) {/* no-op */ }
+
+ @Override
+ public String toString() {
+ return "hasWrongMethodModifiers";
+ }
+ };
+ ChangeEventSource.fromChangeEventsOf(changeEventSource).subscribe(TestObserver.create());
+ fail(IllegalArgumentException.class.getSimpleName() + " expected");
+ } catch (IllegalArgumentException ex) {
+ assertEquals("Class '" + changeEventSource.getClass().getName() + "' has not the expected signature to support change listeners in rx.swing.sources.ChangeEventSource",
+ ex.getMessage());
+ }
+ }
+ }).awaitTerminal();
+ }
+
+ @Test
+ public void issuesWithAddingChangeListenerOnSubscriptionArePropagatedAsError() throws Throwable {
+ SwingTestHelper.create().runInEventDispatchThread(new Action() {
+
+
+ @Override
+ public void run() {
+ TestObserver testSubscriber = TestObserver.create();
+
+ JProgressBar brokenProgressBarSubClass = new JProgressBar() {
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ if (listener.getClass().getName().contains(ChangeEventSource.class.getSimpleName())) {
+ throw new RuntimeException("Totally broken");
+ }
+ }
+ };
+ ChangeEventSource.fromChangeEventsOf(brokenProgressBarSubClass)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoValues();
+
+ List onErrorEvents = testSubscriber.errors();
+ assertEquals(1, onErrorEvents.size());
+ assertTrue(onErrorEvents.get(0) instanceof RuntimeException);
+ assertEquals("Call of addChangeListener via reflection failed.", onErrorEvents.get(0).getMessage());
+ assertEquals(InvocationTargetException.class, onErrorEvents.get(0).getCause().getClass());
+ }
+ }).awaitTerminal();
+ }
+
+ private static JTabbedPane createTabbedPane() {
+ final JTabbedPane tabbedPane = new JTabbedPane();
+ tabbedPane.addTab("tab1", new JPanel());
+ tabbedPane.addTab("tab2", new JPanel());
+ tabbedPane.addTab("tab3", new JPanel());
+ return tabbedPane;
+ }
+
+ private static JSpinner createSpinner() {
+ List yearStrings = Arrays.asList("2014", "2015", "2016");
+ SpinnerListModel spinnerListModel = new SpinnerListModel(yearStrings);
+ return new JSpinner(spinnerListModel);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/rx/swing/sources/ContainerEventSourceTest.java b/src/test/java/rx/swing/sources/ContainerEventSourceTest.java
new file mode 100644
index 0000000..bf99d19
--- /dev/null
+++ b/src/test/java/rx/swing/sources/ContainerEventSourceTest.java
@@ -0,0 +1,178 @@
+/**
+ * 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 io.reactivex.Observable;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Action;
+import io.reactivex.functions.Consumer;
+import io.reactivex.functions.Function;
+import rx.observables.SwingObservable;
+
+@RunWith(Parameterized.class)
+public class ContainerEventSourceTest {
+
+ private final Function> observableFactory;
+
+ private JPanel panel;
+ private Consumer action;
+ private Consumer error;
+ private Action complete;
+
+ public ContainerEventSourceTest(Function> observableFactory) {
+ this.observableFactory = observableFactory;
+ }
+
+ @Parameters
+ public static Collection