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 data() { + return Arrays.asList(new Object[][]{ { observableFromContainerEventSource() }, + { observableFromSwingObservable() } }); + } + + @SuppressWarnings("unchecked") + @Before + public void setup() { + panel = Mockito.spy(new JPanel()); + action = Mockito.mock(Consumer.class); + error = Mockito.mock(Consumer.class); + complete = Mockito.mock(Action.class); + } + + @Test + public void testObservingContainerEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + @Override + public void run() throws Exception { + Disposable subscription = observableFactory.apply(panel) + .subscribe(action, error, complete); + + JPanel child = new JPanel(); + panel.add(child); + panel.removeAll(); + + InOrder inOrder = Mockito.inOrder(action); + + inOrder.verify(action).accept(Matchers.argThat(containerEventMatcher(panel, child, ContainerEvent.COMPONENT_ADDED))); + inOrder.verify(action).accept(Matchers.argThat(containerEventMatcher(panel, child, ContainerEvent.COMPONENT_REMOVED))); + inOrder.verifyNoMoreInteractions(); + Mockito.verify(error, Mockito.never()).accept(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).run(); + + // Verifies that the underlying listener has been removed. + subscription.dispose(); + Mockito.verify(panel).removeContainerListener(Mockito.any(ContainerListener.class)); + Assert.assertEquals(0, panel.getHierarchyListeners().length); + + // Verifies that after unsubscribing events are not emitted. + panel.add(child); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + @Test + public void testObservingFilteredContainerEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + @Override + public void run() throws Exception{ + Disposable subscription = observableFactory.apply(panel) + .filter(rx.swing.sources.ContainerEventSource.Predicates.COMPONENT_ADDED) + .subscribe(action, error, complete); + + JPanel child = new JPanel(); + panel.add(child); + panel.remove(child); // sanity check to verify that the filtering works. + + Mockito.verify(action).accept(Matchers.argThat(containerEventMatcher(panel, child, ContainerEvent.COMPONENT_ADDED))); + Mockito.verify(error, Mockito.never()).accept(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).run(); + + // Verifies that the underlying listener has been removed. + subscription.dispose(); + Mockito.verify(panel).removeContainerListener(Mockito.any(ContainerListener.class)); + Assert.assertEquals(0, panel.getHierarchyListeners().length); + + // Verifies that after unsubscribing events are not emitted. + panel.add(child); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + private static Matcher containerEventMatcher(final Container container, final Component child, final int id) { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if ( argument.getClass() != ContainerEvent.class ) + return false; + + ContainerEvent event = (ContainerEvent) argument; + + if (container != event.getContainer()) + return false; + + if (container != event.getSource()) + return false; + + if (child != event.getChild()) + return false; + + return event.getID() == id; + } + }; + } + + private static Function> observableFromContainerEventSource() + { + return new Function>(){ + @Override + public Observable apply(Container container) { + return ContainerEventSource.fromContainerEventsOf(container); + } + }; + } + + private static Function> observableFromSwingObservable() + { + return new Function>(){ + @Override + public Observable apply(Container container) { + return SwingObservable.fromContainerEvents(container); + } + }; + } +} diff --git a/src/test/java/rx/swing/sources/DocumentEventSourceTest.java b/src/test/java/rx/swing/sources/DocumentEventSourceTest.java new file mode 100644 index 0000000..66348e3 --- /dev/null +++ b/src/test/java/rx/swing/sources/DocumentEventSourceTest.java @@ -0,0 +1,169 @@ +/** + * 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.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.swing.JEditorPane; +import javax.swing.event.DocumentEvent; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Style; +import javax.swing.text.StyleContext; +import javax.swing.text.html.HTMLDocument; + +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Matchers; +import org.mockito.Mockito; + +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import rx.observables.SwingObservable; + +public class DocumentEventSourceTest { + + @Test + public void testObservingDocumentEvents() 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 JEditorPane pane = new JEditorPane(); + // Document must by StyledDocument to test changeUpdate + pane.setContentType("text/html"); + final Document doc = (HTMLDocument) pane.getDocument(); + + final Disposable subscription = DocumentEventSource.fromDocumentEventsOf(doc) + .subscribe(action, error, complete); + + verify(action, never()).accept(Matchers.any()); + verify(error, never()).accept(Matchers.any()); + verify(complete, never()).run(); + + // test insertUpdate + insertStringToDocument(doc, 0, "test text"); + verify(action).accept(Mockito.argThat(documentEventMatcher(DocumentEvent.EventType.INSERT))); + verifyNoMoreInteractions(action, error, complete); + + // test removeUpdate + removeFromDocument(doc, 0, 5); + verify(action).accept(Mockito.argThat(documentEventMatcher(DocumentEvent.EventType.REMOVE))); + verifyNoMoreInteractions(action, error, complete); + + // test changeUpdate + Style defaultStyle = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE); + ((HTMLDocument) doc).setCharacterAttributes(0, doc.getLength(), defaultStyle, true); + verify(action).accept(Mockito.argThat(documentEventMatcher(DocumentEvent.EventType.CHANGE))); + verifyNoMoreInteractions(action, error, complete); + + // test unsubscribe + subscription.dispose(); + insertStringToDocument(doc, 0, "this should be ignored"); + verifyNoMoreInteractions(action, error, complete); + } + + }).awaitTerminal(); + } + + @Test + public void testObservingFilteredDocumentEvents() 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 Document doc = new JEditorPane().getDocument(); + + // filter only INSERT, others will be ignored + final Set filteredTypes + = new HashSet(Arrays.asList(DocumentEvent.EventType.INSERT)); + final Disposable subscription = SwingObservable.fromDocumentEvents(doc, filteredTypes) + .subscribe(action, error, complete); + + verify(action, never()).accept(Matchers.any()); + verify(error, never()).accept(Matchers.any()); + verify(complete, never()).run(); + + // test insertUpdate + insertStringToDocument(doc, 0, "test text"); + verify(action).accept(Mockito.argThat(documentEventMatcher(DocumentEvent.EventType.INSERT))); + verifyNoMoreInteractions(action, error, complete); + + // test removeUpdate + removeFromDocument(doc, 0, 5); + // removeUpdate should be ignored + verifyNoMoreInteractions(action, error, complete); + + // test unsubscribe + subscription.dispose(); + insertStringToDocument(doc, 0, "this should be ignored"); + verifyNoMoreInteractions(action, error, complete); + } + + }).awaitTerminal(); + } + + private static Matcher documentEventMatcher(final DocumentEvent.EventType eventType) { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if (!(argument instanceof DocumentEvent)) { + return false; + } + + return ((DocumentEvent) argument).getType().equals(eventType); + } + }; + } + + private static void insertStringToDocument(Document doc, int offset, String text) { + try { + doc.insertString(offset, text, null); + } catch (BadLocationException ex) { + throw new RuntimeException(ex); + } + } + + private static void removeFromDocument(Document doc, int offset, int length) { + try { + doc.remove(offset, length); + } catch (BadLocationException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/src/test/java/rx/swing/sources/FocusEventSourceTest.java b/src/test/java/rx/swing/sources/FocusEventSourceTest.java new file mode 100644 index 0000000..9ce3adb --- /dev/null +++ b/src/test/java/rx/swing/sources/FocusEventSourceTest.java @@ -0,0 +1,82 @@ +/** + * 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.Component; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; + +import javax.swing.JPanel; + +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 FocusEventSourceTest { + private Component comp = new JPanel(); + + @Test + public void testObservingFocusEvents() 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 FocusEvent event = mock(FocusEvent.class); + + Disposable sub = FocusEventSource.fromFocusEventsOf(comp) + .subscribe(action, error, complete); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + fireFocusEvent(event); + verify(action, times(1)).accept(Matchers. any()); + + fireFocusEvent(event); + verify(action, times(2)).accept(Matchers. any()); + + sub.dispose(); + fireFocusEvent(event); + verify(action, times(2)).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + + }).awaitTerminal(); + } + + private void fireFocusEvent(FocusEvent event) { + for (FocusListener listener : comp.getFocusListeners()) { + listener.focusGained(event); + } + } +} diff --git a/src/test/java/rx/swing/sources/HierarchyBoundsEventSourceTest.java b/src/test/java/rx/swing/sources/HierarchyBoundsEventSourceTest.java new file mode 100644 index 0000000..485a753 --- /dev/null +++ b/src/test/java/rx/swing/sources/HierarchyBoundsEventSourceTest.java @@ -0,0 +1,219 @@ +/** + * 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.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +import java.awt.Component; +import java.awt.Container; +import java.awt.event.HierarchyBoundsListener; +import java.awt.event.HierarchyEvent; +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; +import rx.swing.sources.HierarchyEventSource.Predicates; + +@RunWith(Parameterized.class) +public class HierarchyBoundsEventSourceTest { + + private JPanel rootPanel; + private JPanel parentPanel; + private Consumer action; + private Consumer error; + private Action complete; + private final Function> observableFactory; + private JPanel childPanel; + + public HierarchyBoundsEventSourceTest( Function> observableFactory ) { + this.observableFactory = observableFactory; + } + + @Parameters + public static Collection data() { + return Arrays.asList( new Object[][]{ { observablefromEventSource() }, + { observablefromSwingObservable() } }); + } + + @SuppressWarnings("unchecked") + @Before + public void setup() { + rootPanel = new JPanel(); + + parentPanel = new JPanel(); + rootPanel.add(parentPanel); + + childPanel = Mockito.spy(new JPanel()); + parentPanel.add(childPanel); + + action = mock(Consumer.class); + error = mock(Consumer.class); + complete = mock(Action.class); + } + + @Test + public void testObservingAnscestorResizedHierarchyEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + @Override + public void run() throws Exception{ + Disposable subscription = observableFactory.apply(childPanel) + .filter(Predicates.ANCESTOR_RESIZED) + .subscribe(action, error, complete); + + parentPanel.setSize(10, 10); + parentPanel.setLocation(10, 10); // verifies that ancestor moved events are ignored. + + Mockito.verify(action).accept(Matchers.argThat(hierarchyEventMatcher(childPanel, HierarchyEvent.ANCESTOR_RESIZED, parentPanel, rootPanel))); + Mockito.verify(error, Mockito.never()).accept(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).run(); + + // Verifies that the underlying listener has been removed. + subscription.dispose(); + Mockito.verify(childPanel).removeHierarchyBoundsListener(Mockito.any(HierarchyBoundsListener.class)); + Assert.assertEquals(0, childPanel.getHierarchyListeners().length); + + // Sanity check to verify that no more events are emitted after unsubscribing. + parentPanel.setSize(20, 20); + parentPanel.setLocation(20, 20); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + @Test + public void testObservingAnscestorMovedHierarchyEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + @Override + public void run() throws Exception { + Disposable subscription = observableFactory.apply(childPanel) + .filter(Predicates.ANCESTOR_MOVED) + .subscribe(action, error, complete); + + parentPanel.setSize(10, 10); // verifies that ancestor resized events are ignored. + parentPanel.setLocation(10, 10); + + Mockito.verify(action).accept(Matchers.argThat(hierarchyEventMatcher(childPanel, HierarchyEvent.ANCESTOR_MOVED, parentPanel, rootPanel))); + Mockito.verify(error, Mockito.never()).accept(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).run(); + + // Verifies that the underlying listener has been removed. + subscription.dispose(); + Mockito.verify(childPanel).removeHierarchyBoundsListener(Mockito.any(HierarchyBoundsListener.class)); + Assert.assertEquals(0, childPanel.getHierarchyListeners().length); + + // Sanity check to verify that no more events are emitted after unsubscribing. + parentPanel.setSize(20, 20); + parentPanel.setLocation(20, 20); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + @Test + public void testObservingAllHierarchyBoundsEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + @Override + public void run() throws Exception{ + Disposable subscription = observableFactory.apply(childPanel) + .subscribe(action, error, complete); + + InOrder inOrder = inOrder(action); + + parentPanel.setSize(10, 10); + parentPanel.setLocation(10, 10); + + inOrder.verify(action).accept(Matchers.argThat(hierarchyEventMatcher(childPanel, HierarchyEvent.ANCESTOR_RESIZED, parentPanel, rootPanel))); + inOrder.verify(action).accept(Matchers.argThat(hierarchyEventMatcher(childPanel, HierarchyEvent.ANCESTOR_MOVED, parentPanel, rootPanel))); + inOrder.verifyNoMoreInteractions(); + Mockito.verify(error, Mockito.never()).accept(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).run(); + + // Verifies that the underlying listener has been removed. + subscription.dispose(); + Mockito.verify(childPanel).removeHierarchyBoundsListener(Mockito.any(HierarchyBoundsListener.class)); + Assert.assertEquals(0, childPanel.getHierarchyListeners().length); + + // Sanity check to verify that no more events are emitted after unsubscribing. + parentPanel.setSize(20, 20); + parentPanel.setLocation(20, 20); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + private Matcher hierarchyEventMatcher(final Component source, final int id, final Container changed, final Container changedParent) { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if (argument.getClass() != HierarchyEvent.class) + return false; + + HierarchyEvent event = (HierarchyEvent) argument; + + if (source != event.getComponent()) + return false; + + if (changed != event.getChanged()) + return false; + + if (changedParent != event.getChangedParent()) + return false; + + return id == event.getID(); + } + }; + } + + private static Function> observablefromEventSource() + { + return new Function>() { + @Override + public Observable apply(Component component) { + return HierarchyEventSource.fromHierarchyBoundsEventsOf(component); + } + }; + } + + private static Function> observablefromSwingObservable() + { + return new Function>() { + @Override + public Observable apply(Component component) { + return SwingObservable.fromHierachyBoundsEvents(component); + } + }; + } +} diff --git a/src/test/java/rx/swing/sources/HierarchyEventSourceTest.java b/src/test/java/rx/swing/sources/HierarchyEventSourceTest.java new file mode 100644 index 0000000..1702f97 --- /dev/null +++ b/src/test/java/rx/swing/sources/HierarchyEventSourceTest.java @@ -0,0 +1,149 @@ +/** + * 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.mockito.Mockito.mock; + +import java.awt.Component; +import java.awt.Container; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +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.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 HierarchyEventSourceTest { + + private JPanel rootPanel; + private JPanel parentPanel; + private Consumer action; + private Consumer error; + private Action complete; + private final Function> observableFactory; + + public HierarchyEventSourceTest( Function> observableFactory ) { + this.observableFactory = observableFactory; + } + + @Parameters + public static Collection data() { + return Arrays.asList( new Object[][]{ { ObservablefromEventSource() }, + { ObservablefromSwingObservable() } }); + } + + @SuppressWarnings("unchecked") + @Before + public void setup() { + rootPanel = new JPanel(); + parentPanel = new JPanel(); + + action = mock(Consumer.class); + error = mock(Consumer.class); + complete = mock(Action.class); + } + + @Test + public void testObservingHierarchyEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + @Override + public void run() throws Exception{ + JPanel childPanel = Mockito.spy(new JPanel()); + parentPanel.add(childPanel); + + Disposable subscription = observableFactory.apply(childPanel) + .subscribe(action, error, complete); + + rootPanel.add(parentPanel); + + Mockito.verify(action).accept(Matchers.argThat(hierarchyEventMatcher(childPanel, HierarchyEvent.PARENT_CHANGED, parentPanel, rootPanel))); + Mockito.verify(error, Mockito.never()).accept(Mockito.any(Throwable.class)); + Mockito.verify(complete, Mockito.never()).run(); + + // Verifies that the underlying listener has been removed. + subscription.dispose(); + Mockito.verify(childPanel).removeHierarchyListener(Mockito.any(HierarchyListener.class)); + Assert.assertEquals(0, childPanel.getHierarchyListeners().length); + + // Sanity check to verify that no more events are emitted after unsubscribing. + rootPanel.remove(parentPanel); + Mockito.verifyNoMoreInteractions(action, error, complete); + } + }).awaitTerminal(); + } + + private Matcher hierarchyEventMatcher(final Component source, final int changeFlags, final Container changed, final Container changedParent) { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if (argument.getClass() != HierarchyEvent.class) + return false; + + HierarchyEvent event = (HierarchyEvent) argument; + + if (source != event.getComponent()) + return false; + + if (changed != event.getChanged()) + return false; + + if (changedParent != event.getChangedParent()) + return false; + + return changeFlags == event.getChangeFlags(); + } + }; + } + + private static Function> ObservablefromEventSource() + { + return new Function>() { + @Override + public Observable apply(Component component) { + return HierarchyEventSource.fromHierarchyEventsOf(component); + } + }; + } + + private static Function> ObservablefromSwingObservable() + { + return new Function>() { + @Override + public Observable apply(Component component) { + return SwingObservable.fromHierachyEvents(component); + } + }; + } +} diff --git a/src/test/java/rx/swing/sources/ItemEventSourceTest.java b/src/test/java/rx/swing/sources/ItemEventSourceTest.java new file mode 100644 index 0000000..055e358 --- /dev/null +++ b/src/test/java/rx/swing/sources/ItemEventSourceTest.java @@ -0,0 +1,217 @@ +/** + * 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 java.awt.event.ItemEvent.DESELECTED; +import static java.awt.event.ItemEvent.SELECTED; +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.ItemEvent; + +import javax.swing.AbstractButton; + +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Matchers; +import org.mockito.Mockito; + +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import rx.observables.SwingObservable; + +public class ItemEventSourceTest +{ + @Test + public void testObservingItemEvents() 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); + + @SuppressWarnings("serial") + class TestButton extends AbstractButton { + + void testSelection() { + fireItemStateChanged(new ItemEvent(this, + ItemEvent.ITEM_STATE_CHANGED, + this, + ItemEvent.SELECTED)); + } + void testDeselection() { + fireItemStateChanged(new ItemEvent(this, + ItemEvent.ITEM_STATE_CHANGED, + this, + ItemEvent.DESELECTED)); + } + } + + TestButton button = new TestButton(); + Disposable sub = ItemEventSource.fromItemEventsOf(button).subscribe(action, + error, complete); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + button.testSelection(); + verify(action, times(1)).accept(Mockito.argThat(itemEventMatcher(SELECTED))); + + button.testSelection(); + verify(action, times(2)).accept(Mockito.argThat(itemEventMatcher(SELECTED))); + + button.testDeselection(); + verify(action, times(1)).accept(Mockito.argThat(itemEventMatcher(DESELECTED))); + + + sub.dispose(); + button.testSelection(); + verify(action, times(2)).accept(Mockito.argThat(itemEventMatcher(SELECTED))); + verify(action, times(1)).accept(Mockito.argThat(itemEventMatcher(DESELECTED))); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + }).awaitTerminal(); + } + + @Test + public void testObservingItemEventsFilteredBySelected() 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); + + @SuppressWarnings("serial") + class TestButton extends AbstractButton { + void testSelection() { + fireItemStateChanged(new ItemEvent(this, + ItemEvent.ITEM_STATE_CHANGED, + this, + ItemEvent.SELECTED)); + } + void testDeselection() { + fireItemStateChanged(new ItemEvent(this, + ItemEvent.ITEM_STATE_CHANGED, + this, + ItemEvent.DESELECTED)); + } + } + + TestButton button = new TestButton(); + Disposable sub = SwingObservable.fromItemSelectionEvents(button) + .subscribe(action, error, complete); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + button.testSelection(); + verify(action, times(1)).accept(Mockito.argThat(itemEventMatcher(SELECTED))); + + button.testDeselection(); + verify(action, never()).accept(Mockito.argThat(itemEventMatcher(DESELECTED))); + + + sub.dispose(); + button.testSelection(); + verify(action, times(1)).accept(Mockito.argThat(itemEventMatcher(SELECTED))); + verify(action, never()).accept(Mockito.argThat(itemEventMatcher(DESELECTED))); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + }).awaitTerminal(); + } + + @Test + public void testObservingItemEventsFilteredByDeSelected() 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); + + @SuppressWarnings("serial") + class TestButton extends AbstractButton { + void testSelection() { + fireItemStateChanged(new ItemEvent(this, + ItemEvent.ITEM_STATE_CHANGED, + this, + ItemEvent.SELECTED)); + } + void testDeselection() { + fireItemStateChanged(new ItemEvent(this, + ItemEvent.ITEM_STATE_CHANGED, + this, + ItemEvent.DESELECTED)); + } + } + + TestButton button = new TestButton(); + Disposable sub = SwingObservable.fromItemDeselectionEvents(button) + .subscribe(action, error, complete); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + button.testSelection(); + verify(action, never()).accept(Mockito.argThat(itemEventMatcher(SELECTED))); + + button.testDeselection(); + verify(action, times(1)).accept(Mockito.argThat(itemEventMatcher(DESELECTED))); + + + sub.dispose(); + button.testSelection(); + verify(action, never()).accept(Mockito.argThat(itemEventMatcher(SELECTED))); + verify(action, times(1)).accept(Mockito.argThat(itemEventMatcher(DESELECTED))); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + }).awaitTerminal(); + } + + private Matcher itemEventMatcher(final int eventType) + { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if (argument.getClass() != ItemEvent.class) + return false; + + return ((ItemEvent) argument).getStateChange() == eventType; + } + }; + } +} diff --git a/src/test/java/rx/swing/sources/KeyEventSourceTest.java b/src/test/java/rx/swing/sources/KeyEventSourceTest.java new file mode 100644 index 0000000..5d0e9a1 --- /dev/null +++ b/src/test/java/rx/swing/sources/KeyEventSourceTest.java @@ -0,0 +1,147 @@ +/** + * 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 java.util.Arrays.asList; +import static org.mockito.Mockito.inOrder; +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.Component; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.swing.JPanel; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Matchers; + +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; + +public class KeyEventSourceTest { + private Component comp = new JPanel(); + + @Test + public void testObservingKeyEvents() 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 KeyEvent event = mock(KeyEvent.class); + + Disposable sub = KeyEventSource.fromKeyEventsOf(comp) + .subscribe(action, error, complete); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + fireKeyEvent(event); + verify(action, times(1)).accept(Matchers. any()); + + fireKeyEvent(event); + verify(action, times(2)).accept(Matchers. any()); + + sub.dispose(); + fireKeyEvent(event); + verify(action, times(2)).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + + }).awaitTerminal(); + } + + @Test + public void testObservingPressedKeys() 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); + + Disposable sub = KeyEventSource.currentlyPressedKeysOf(comp) + .subscribe(action, error, complete); + + InOrder inOrder = inOrder(action); + inOrder.verify(action).accept( + Collections. emptySet()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED)); + inOrder.verify(action, times(1)).accept( + new HashSet(asList(1))); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + fireKeyEvent(keyEvent(2, KeyEvent.KEY_PRESSED)); + fireKeyEvent(keyEvent(KeyEvent.VK_UNDEFINED, KeyEvent.KEY_TYPED)); + inOrder.verify(action, times(1)).accept( + new HashSet(asList(1, 2))); + + fireKeyEvent(keyEvent(2, KeyEvent.KEY_RELEASED)); + inOrder.verify(action, times(1)).accept( + new HashSet(asList(1))); + + fireKeyEvent(keyEvent(3, KeyEvent.KEY_RELEASED)); + inOrder.verify(action, times(1)).accept( + new HashSet(asList(1))); + + fireKeyEvent(keyEvent(1, KeyEvent.KEY_RELEASED)); + inOrder.verify(action, times(1)).accept( + Collections. emptySet()); + + sub.dispose(); + + fireKeyEvent(keyEvent(1, KeyEvent.KEY_PRESSED)); + inOrder.verify(action, never()).accept( + Matchers.> any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + + }).awaitTerminal(); + } + + private KeyEvent keyEvent(int keyCode, int id) { + return new KeyEvent(comp, id, -1L, 0, keyCode, ' '); + } + + private void fireKeyEvent(KeyEvent event) { + for (KeyListener listener : comp.getKeyListeners()) { + listener.keyTyped(event); + } + } +} diff --git a/src/test/java/rx/swing/sources/ListSelectionEventSourceTest.java b/src/test/java/rx/swing/sources/ListSelectionEventSourceTest.java new file mode 100644 index 0000000..8411bc6 --- /dev/null +++ b/src/test/java/rx/swing/sources/ListSelectionEventSourceTest.java @@ -0,0 +1,243 @@ +/** + * 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.DefaultListSelectionModel; +import javax.swing.JList; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; + +import org.junit.Assert; +import org.junit.Test; + +import io.reactivex.functions.Action; +import io.reactivex.observers.TestObserver; + + +public class ListSelectionEventSourceTest { + + @Test + public void jtableRowSelectionObservingSelectionEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + + @Override + public void run() throws Exception { + TestObserver testSubscriber = TestObserver.create(); + + JTable table = createJTable(); + ListSelectionEventSource + .fromListSelectionEventsOf(table.getSelectionModel()) + .subscribe(testSubscriber); + + testSubscriber.assertNoErrors(); + testSubscriber.assertNoValues(); + + table.getSelectionModel().setSelectionInterval(0, 0); + + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(1); + + assertListSelectionEventEquals( + new ListSelectionEvent( + table.getSelectionModel(), + 0 /* start of region with selection changes */, + 0 /* end of region with selection changes */, + false), + testSubscriber.values().get(0)); + + table.getSelectionModel().setSelectionInterval(2, 2); + + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(2); + + assertListSelectionEventEquals( + new ListSelectionEvent( + table.getSelectionModel(), + 0 /* start of region with selection changes */, + 2 /* end of region with selection changes */, + false), + testSubscriber.values().get(1)); + } + }).awaitTerminal(); + } + + @Test + public void jtableColumnSelectionObservingSelectionEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + + @Override + public void run() { + TestObserver testSubscriber = TestObserver.create(); + + JTable table = createJTable(); + table.getColumnModel().getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); + + ListSelectionEventSource + .fromListSelectionEventsOf(table.getColumnModel().getSelectionModel()) + .subscribe(testSubscriber); + + testSubscriber.assertNoErrors(); + testSubscriber.assertNoValues(); + + table.getColumnModel().getSelectionModel().setSelectionInterval(0, 0); + + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(1); + + assertListSelectionEventEquals( + new ListSelectionEvent( + table.getColumnModel().getSelectionModel(), + 0 /* start of region with selection changes */, + 0 /* end of region with selection changes */, + false), + testSubscriber.values().get(0)); + + table.getColumnModel().getSelectionModel().setSelectionInterval(2, 2); + + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(2); + + assertListSelectionEventEquals( + new ListSelectionEvent( + table.getColumnModel().getSelectionModel(), + 0 /* start of region with selection changes */, + 2 /* end of region with selection changes */, + false), + testSubscriber.values().get(1)); + + } + }).awaitTerminal(); + } + + @Test + public void jlistSelectionObservingSelectionEvents() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + + @Override + public void run() throws Exception { + TestObserver testSubscriber = TestObserver.create(); + + JList jList = new JList(new String[]{"a", "b", "c", "d", "e", "f"}); + jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + ListSelectionEventSource + .fromListSelectionEventsOf(jList.getSelectionModel()) + .subscribe(testSubscriber); + + testSubscriber.assertNoErrors(); + testSubscriber.assertNoValues(); + + jList.getSelectionModel().setSelectionInterval(0, 0); + + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(1); + + assertListSelectionEventEquals( + new ListSelectionEvent( + jList.getSelectionModel(), + 0 /* start of region with selection changes */, + 0 /* end of region with selection changes */, + false), + testSubscriber.values().get(0)); + + jList.getSelectionModel().setSelectionInterval(2, 2); + + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(2); + + assertListSelectionEventEquals( + new ListSelectionEvent( + jList.getSelectionModel(), + 0 /* start of region with selection changes */, + 2 /* end of region with selection changes */, + false), + testSubscriber.values().get(1)); + } + }).awaitTerminal(); + } + + @Test + public void jtableRowSelectionUnsubscribeRemovesRowSelectionListener() throws Throwable { + SwingTestHelper.create().runInEventDispatchThread(new Action() { + + @Override + public void run() throws Exception{ + TestObserver testSubscriber = TestObserver.create(); + + JTable table = createJTable(); + int numberOfListenersBefore = getNumberOfRowListSelectionListeners(table); + + ListSelectionEventSource + .fromListSelectionEventsOf(table.getSelectionModel()) + .subscribe(testSubscriber); + + testSubscriber.assertNoErrors(); + testSubscriber.assertNoValues(); + + testSubscriber.dispose(); + + Assert.assertTrue(testSubscriber.isDisposed()); + + table.getSelectionModel().setSelectionInterval(0, 0); + + testSubscriber.assertNoErrors(); + testSubscriber.assertNoValues(); + + Assert.assertEquals(numberOfListenersBefore, getNumberOfRowListSelectionListeners(table)); + } + }).awaitTerminal(); + } + + private static int getNumberOfRowListSelectionListeners(final JTable table) { + return ((DefaultListSelectionModel) table.getSelectionModel()).getListSelectionListeners().length; + } + + private static JTable createJTable() { + return new JTable(new Object[][]{ + {"A1", "B1", "C1"}, + {"A2", "B2", "C2"}, + {"A3", "B3", "C3"}, + }, + new String[]{ + "A", "B", "C" + }); + } + + private static void assertListSelectionEventEquals(ListSelectionEvent expected, ListSelectionEvent actual) { + if (expected == null) { + throw new IllegalArgumentException("missing expected"); + } + + if (actual == null) { + throw new AssertionError("Expected " + expected + ", but was: " + actual); + } + if (!expected.getSource().equals(actual.getSource())) { + throw new AssertionError("Expected " + expected + ", but was: " + actual + ". Different source."); + } + if (expected.getFirstIndex() != actual.getFirstIndex()) { + throw new AssertionError("Expected " + expected + ", but was: " + actual + ". Different first index."); + } + if (expected.getLastIndex() != actual.getLastIndex()) { + throw new AssertionError("Expected " + expected + ", but was: " + actual + ". Different last index."); + } + if (expected.getValueIsAdjusting() != actual.getValueIsAdjusting()) { + throw new AssertionError("Expected " + expected + ", but was: " + actual + ". Different ValueIsAdjusting."); + } + } +} \ No newline at end of file diff --git a/src/test/java/rx/swing/sources/MouseEventSourceTest.java b/src/test/java/rx/swing/sources/MouseEventSourceTest.java new file mode 100644 index 0000000..1f06f1c --- /dev/null +++ b/src/test/java/rx/swing/sources/MouseEventSourceTest.java @@ -0,0 +1,205 @@ +/** + * 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.inOrder; +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.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 javax.swing.JPanel; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Matchers; + +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; + +public class MouseEventSourceTest { + private Component comp = new JPanel(); + + @Test + public void testRelativeMouseMotion() 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); + + Disposable sub = MouseEventSource.fromRelativeMouseMotion(comp).subscribe( + action, error, complete); + + InOrder inOrder = inOrder(action); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + fireMouseMotionEvent(mouseEvent(0, 0, MouseEvent.MOUSE_MOVED)); + verify(action, never()).accept(Matchers. any()); + + fireMouseMotionEvent(mouseEvent(10, -5, MouseEvent.MOUSE_MOVED)); + inOrder.verify(action, times(1)).accept(new Point(10, -5)); + + fireMouseMotionEvent(mouseEvent(6, 10, MouseEvent.MOUSE_MOVED)); + inOrder.verify(action, times(1)).accept(new Point(-4, 15)); + + sub.dispose(); + fireMouseMotionEvent(mouseEvent(0, 0, MouseEvent.MOUSE_MOVED)); + inOrder.verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + + }).awaitTerminal(); + } + + @Test + public void testMouseEvents() 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); + + Disposable sub = MouseEventSource.fromMouseEventsOf(comp) + .subscribe(action, error, complete); + + InOrder inOrder = inOrder(action); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + MouseEvent mouseEvent = + mouseEvent(0, 0, MouseEvent.MOUSE_CLICKED); + fireMouseClickEvent(mouseEvent); + inOrder.verify(action, times(1)).accept(mouseEvent); + + mouseEvent = mouseEvent(300, 200, MouseEvent.MOUSE_CLICKED); + fireMouseClickEvent(mouseEvent); + inOrder.verify(action, times(1)).accept(mouseEvent); + + mouseEvent = mouseEvent(0, 0, MouseEvent.MOUSE_CLICKED); + fireMouseClickEvent(mouseEvent); + inOrder.verify(action, times(1)).accept(mouseEvent); + + sub.dispose(); + fireMouseClickEvent(mouseEvent(0, 0, MouseEvent.MOUSE_CLICKED)); + inOrder.verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + + }).awaitTerminal(); + } + + @Test + public void testMouseWheelEvents() 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); + + Disposable sub = MouseEventSource.fromMouseWheelEvents(comp) + .subscribe(action, error, complete); + + InOrder inOrder = inOrder(action); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + MouseWheelEvent mouseEvent = mouseWheelEvent(0); + fireMouseWheelEvent(mouseEvent); + inOrder.verify(action, times(1)).accept(mouseEvent); + + mouseEvent = mouseWheelEvent(3); + fireMouseWheelEvent(mouseEvent); + inOrder.verify(action, times(1)).accept(mouseEvent); + + mouseEvent = mouseWheelEvent(5); + fireMouseWheelEvent(mouseEvent); + inOrder.verify(action, times(1)).accept(mouseEvent); + + mouseEvent = mouseWheelEvent(1); + fireMouseWheelEvent(mouseEvent); + inOrder.verify(action, times(1)).accept(mouseEvent); + + sub.dispose(); + fireMouseClickEvent(mouseEvent(0, 0, MouseEvent.MOUSE_CLICKED)); + inOrder.verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + + }).awaitTerminal(); + } + + private MouseEvent mouseEvent(int x, int y, int mouseEventType) { + return new MouseEvent(comp, mouseEventType, 1L, 0, x, y, 0, + false); + } + + private void fireMouseMotionEvent(MouseEvent event) { + for (MouseMotionListener listener : comp.getMouseMotionListeners()) { + listener.mouseMoved(event); + } + } + + private void fireMouseClickEvent(MouseEvent event) { + for (MouseListener listener : comp.getMouseListeners()) { + listener.mouseClicked(event); + } + } + + private MouseWheelEvent mouseWheelEvent(int wheelRotationClicks) { + int mouseEventType = MouseEvent.MOUSE_WHEEL; + return new MouseWheelEvent(comp, mouseEventType, 1L, 0, 0, 0, 0, + false, MouseWheelEvent.WHEEL_BLOCK_SCROLL, 0, + wheelRotationClicks); + } + + private void fireMouseWheelEvent(MouseWheelEvent event) { + for (MouseWheelListener listener : comp.getMouseWheelListeners()) { + listener.mouseWheelMoved(event); + } + } +} diff --git a/src/test/java/rx/swing/sources/PropertyChangeEventSourceTest.java b/src/test/java/rx/swing/sources/PropertyChangeEventSourceTest.java new file mode 100644 index 0000000..b3318af --- /dev/null +++ b/src/test/java/rx/swing/sources/PropertyChangeEventSourceTest.java @@ -0,0 +1,146 @@ +/** + * 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 static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.awt.Component; +import java.beans.PropertyChangeEvent; + +import javax.swing.JPanel; + +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Matchers; +import org.mockito.Mockito; + +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import rx.observables.SwingObservable; + +public class PropertyChangeEventSourceTest { + @Test + public void testObservingPropertyEvents() 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); + + Component component = new JPanel(); + + Disposable subscription = PropertyChangeEventSource.fromPropertyChangeEventsOf(component).subscribe(action, error, complete); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + component.setEnabled(false); + verify(action, times(1)).accept(Mockito.argThat(propertyChangeEventMatcher("enabled", true, false))); + verifyNoMoreInteractions(action, error, complete); + + // check that an event is only fired if the value really changes + component.setEnabled(false); + verifyNoMoreInteractions(action, error, complete); + + component.setEnabled(true); + verify(action, times(1)).accept(Mockito.argThat(propertyChangeEventMatcher("enabled", false, true))); + verifyNoMoreInteractions(action, error, complete); + + // check some arbitrary property + component.firePropertyChange("width", 200, 300); + verify(action, times(1)).accept(Mockito.argThat(propertyChangeEventMatcher("width", 200l, 300l))); + verifyNoMoreInteractions(action, error, complete); + + // verify no events sent after unsubscribing + subscription.dispose(); + component.setEnabled(false); + verifyNoMoreInteractions(action, error, complete); + } + + }).awaitTerminal(); + } + + @Test + public void testObservingFilteredPropertyEvents() 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); + + Component component = new JPanel(); + + Disposable subscription = SwingObservable.fromPropertyChangeEvents(component, "enabled").subscribe(action, error, complete); + + verify(action, never()).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + + // trigger a bunch of property change events and verify that only the enbled ones are observed + component.setEnabled(false); + component.setEnabled(false); + component.setEnabled(true); + component.firePropertyChange("width", 200, 300); + component.firePropertyChange("height", 400, 200); + component.firePropertyChange("depth", 100, 300); + verify(action, times(1)).accept(Mockito.argThat(propertyChangeEventMatcher("enabled", true, false))); + verify(action, times(1)).accept(Mockito.argThat(propertyChangeEventMatcher("enabled", false, true))); + verifyNoMoreInteractions(action, error, complete); + + subscription.dispose(); + } + + }).awaitTerminal(); + } + + private static Matcher propertyChangeEventMatcher(final String propertyName, final Object oldValue, final Object newValue) { + return new ArgumentMatcher() { + @Override + public boolean matches(Object argument) { + if (argument.getClass() != PropertyChangeEvent.class) { + return false; + } + + PropertyChangeEvent pcEvent = (PropertyChangeEvent) argument; + + if (!propertyName.equals(pcEvent.getPropertyName())) { + return false; + } + + if (!oldValue.equals(pcEvent.getOldValue())) { + return false; + } + + return newValue.equals(pcEvent.getNewValue()); + } + }; + } +} diff --git a/src/test/java/rx/swing/sources/WindowEventSourceTest.java b/src/test/java/rx/swing/sources/WindowEventSourceTest.java new file mode 100644 index 0000000..ffcc9d4 --- /dev/null +++ b/src/test/java/rx/swing/sources/WindowEventSourceTest.java @@ -0,0 +1,86 @@ +/** + * 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.GraphicsEnvironment; +import java.awt.Window; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +import javax.swing.JFrame; + +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 WindowEventSourceTest { + + @Test + public void testObservingWindowEvents() throws Throwable { + if (GraphicsEnvironment.isHeadless()) + return; + SwingTestHelper.create().runInEventDispatchThread(new Action() { + @Override + public void run() throws Exception{ + JFrame owner = new JFrame(); + Window window = new Window(owner); + + @SuppressWarnings("unchecked") + Consumer action = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer error = mock(Consumer.class); + Action complete = mock(Action.class); + + final WindowEvent event = mock(WindowEvent.class); + + Disposable sub = WindowEventSource.fromWindowEventsOf(window) + .subscribe(action, error, complete); + + verify(action, never()).accept(Matchers.any()); + verify(error, never()).accept(Matchers.any()); + verify(complete, never()).run(); + + fireWindowEvent(window, event); + verify(action, times(1)).accept(Matchers.any()); + + fireWindowEvent(window, event); + verify(action, times(2)).accept(Matchers. any()); + + sub.dispose(); + fireWindowEvent(window, event); + verify(action, times(2)).accept(Matchers. any()); + verify(error, never()).accept(Matchers. any()); + verify(complete, never()).run(); + } + + }).awaitTerminal(); + } + + private void fireWindowEvent(Window window, WindowEvent event) { + for (WindowListener listener : window.getWindowListeners()) { + listener.windowClosed(event); + } + } +} +