├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── example-screenshot.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── net │ │ └── christophermerrill │ │ └── FancyFxTree │ │ ├── FancyTreeCell.java │ │ ├── FancyTreeCellEditor.java │ │ ├── FancyTreeItemBuilder.java │ │ ├── FancyTreeItemFacade.java │ │ ├── FancyTreeKeyHandler.java │ │ ├── FancyTreeNodeFacade.java │ │ ├── FancyTreeOperationHandler.java │ │ ├── FancyTreeView.java │ │ ├── FancyTreeViewSkin.java │ │ ├── TextCellEditor.java │ │ └── example │ │ ├── ExampleCustomCellEditor.java │ │ ├── ExampleDataNode.java │ │ ├── ExampleDataNodeBuilder.java │ │ ├── ExampleOperationHandler.java │ │ ├── ExampleTreeNodeFacade.java │ │ └── FancyTreeExample.java └── resources │ └── net │ └── christophermerrill │ └── FancyFxTree │ └── example │ └── FancyTreeExample.css └── test └── java └── net └── christophermerrill └── FancyFxTree └── FancyTreeTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build 3 | /.idea 4 | *.iml 5 | *.class 6 | hs_err_pid* 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FancyFxTree 2 | An extension of JavaFX TreeView with many advanced features already implemented. Instead of 3 | learning the intracacies of the TreeView APIs, simply extend a few base classes and override 4 | the behaviors you need. 5 | 6 | Notably, it provides separation between the TreeView expectations of the model classes and the implementation 7 | of your data tree - so you don't have to design your data model around the expectations of the TreeView authors 8 | (through a facade interface that you must implement). Included 9 | is the ability to change how a tree item is rendered based on dynamic changes in your application 10 | that are not necessarily a reflection of the nodes in your tree. The JavaFX TreeView assumes that this could 11 | only happen if the model object changes identity. This is designed to allow updates to the appearance 12 | of a cell based on external changes (possibly asynchronous). 13 | 14 | Enable these capabilities by implementing the FancyTreeOperationHandler: 15 | 16 | * Act on cut/copy/paste/delete/undo keystrokes (including multiple-selections) 17 | * Drag & drop (including multiple-selections) 18 | * Act when user double-clicks an item 19 | * Show a context menu for a item 20 | * Act when selection changes 21 | 22 | Use these FancyTree APIs to manipulate the tree: 23 | 24 | * Expand all items 25 | * Expand all children of an item 26 | * Collapse all items 27 | * Collapse all children of an item 28 | * Expand tree items as required to make a specific item visible 29 | * Scroll to a specific item (expanding to make it visible, if needed) 30 | * Scroll to an item that is visible (don't scroll if not in tree's viewport) 31 | * Select a specific item (expanding and scrolling to it if needed) 32 | * Get paths to selected items 33 | 34 | Additional features: 35 | 36 | * Hovering over a node during drag will expand it (hover duration is customizable) 37 | * Clicking nodes does not expand/collapse them...it merely selects them. The user 38 | must click the chevron (expander) icon to expand/collapsed (like most other tree 39 | implementations that are not solely for navigating a hierarchy) 40 | * Supply your own CSS for the look of a selected tree cell and the visual effects 41 | for drop before/on/after indicators. 42 | 43 | ## Example 44 | 45 | The included example demonstrates many of the capabilities described above. 46 | 47 | ![Example Screenshot](https://github.com/ChrisLMerrill/FancyFxTree/raw/master/example-screenshot.png) 48 | 49 | ## Usages 50 | 51 | The FancyFxTree is used by the navigation view in the [MuseIDE](http://ide4selenium.com), an 52 | IDE for web test automation using Selenium. Most of the implementation was taken from MuseIDE's 53 | test editor...which will soon be converted to use it as well. 54 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'application' 4 | id 'maven-publish' 5 | id 'com.jfrog.bintray' version '1.8.4' 6 | id 'org.openjfx.javafxplugin' version '0.0.8' 7 | } 8 | 9 | version 3.6 10 | group = 'net.christophermerrill' 11 | 12 | application { 13 | mainClassName = 'net.christophermerrill.FancyFxTree.example.FancyTreeExample' 14 | } 15 | 16 | javafx { 17 | version = "14.0.1" 18 | modules = [ 'javafx.controls' ] 19 | } 20 | 21 | repositories 22 | { 23 | mavenLocal() 24 | jcenter() 25 | } 26 | 27 | dependencies 28 | { 29 | compile 'org.openjfx:javafx-controls:14.0.1' 30 | 31 | testCompile 'org.junit.jupiter:junit-jupiter-api:5.5.1' 32 | testCompile 'org.junit.jupiter:junit-jupiter-engine:5.5.1' 33 | testCompile 'net.christophermerrill:TestFxUtils:2.0' 34 | // testCompile "org.testfx:openjfx-monocle:jdk-12.0.1+2" // For Java 12 35 | } 36 | 37 | task sourcesJar(type: Jar) { 38 | archiveClassifier.set('sources') 39 | from sourceSets.main.allSource 40 | } 41 | 42 | javadoc { 43 | include 'net/christophermerrill/FancyFxTree/*' 44 | options.addBooleanOption('html5', true) 45 | } 46 | 47 | task javadocJar(type: Jar) { 48 | archiveClassifier.set('javadoc') 49 | from javadoc 50 | } 51 | 52 | if (JavaVersion.current().isJava8Compatible()) { 53 | allprojects { 54 | tasks.withType(Javadoc) { 55 | options.addStringOption('Xdoclint:none', '-quiet') 56 | } 57 | } 58 | } 59 | 60 | artifacts { 61 | archives sourcesJar 62 | archives javadocJar 63 | } 64 | 65 | tasks.withType(JavaCompile) { 66 | sourceCompatibility = JavaVersion.VERSION_11 67 | targetCompatibility = JavaVersion.VERSION_11 68 | } 69 | 70 | wrapper { 71 | gradleVersion = '6.3' 72 | } 73 | 74 | test { 75 | useJUnitPlatform() 76 | } 77 | 78 | publishing { 79 | publications { 80 | mavenJava(MavenPublication) { 81 | from components.java 82 | artifact sourcesJar 83 | artifact javadocJar 84 | pom { 85 | name = 'FancyFxTree' // pom.project.name must be same as bintray.pkg.name 86 | description = 'An extension of JavaFX TreeView that makes it easy to implement a sophisticated tree with editing, drag-n-drop, dynamic updates without designing your data model around the TreeView expectations.' 87 | url = 'https://github.com/ChrisLMerrill/FancyFxTree' 88 | packaging = 'jar' 89 | groupId = project.group 90 | artifactId = 'FancyFxTree' 91 | version = project.version 92 | inceptionYear = '2017' // HARDCODED 93 | licenses { 94 | license { // HARDCODED 95 | name = 'Apache-2.0' 96 | url = 'https://www.apache.org/licenses/LICENSE-2.0' 97 | distribution = 'repo' 98 | } 99 | } 100 | developers { 101 | developer { 102 | id = "ChrisLMerrill" 103 | name = "Chris Merrill" 104 | email = "osdev@christophermerrill.net" 105 | } 106 | } 107 | scm { 108 | connection = 'https://github.com/ChrisLMerrill/FancyFxTree.git' 109 | developerConnection = 'https://github.com/ChrisLMerrill' 110 | url = 'https://github.com/ChrisLMerrill/FancyFxTree' 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | bintray { 118 | user = BINTRAY_UPLOAD_USERNAME 119 | key = BINTRAY_UPLOAD_APIKEY 120 | publications = ['mavenJava'] 121 | pkg { 122 | repo = 'maven' 123 | name = 'FancyFxTree' 124 | licenses = ['Apache-2.0'] 125 | vcsUrl = 'https://github.com/ChrisLMerrill/FancyFxTree.git' 126 | version { 127 | name = project.version 128 | released = new Date() 129 | gpg { 130 | sign = true 131 | passphrase = GPG_JARSIGN_PASSPHRASE 132 | } 133 | mavenCentralSync { 134 | sync = true //[Default: true] Determines whether to sync the version to Maven Central. 135 | user = MAVENCENTRAL_USERNAME 136 | password = MAVENCENTRAL_PASSWORD 137 | close = '1' //Optional property. By default the staging repository is closed and artifacts are released to Maven Central. You can optionally turn this behaviour off (by puting 0 as value) and release the version manually. 138 | } 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /example-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisLMerrill/FancyFxTree/9f1f185748fd83d7d4f61c481104cff98b7173a7/example-screenshot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisLMerrill/FancyFxTree/9f1f185748fd83d7d4f61c481104cff98b7173a7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 20 20:04:13 EDT 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This settings file was auto generated by the Gradle buildInit task 3 | * by 'chris' at '10/21/16 4:17 PM' with Gradle 2.13 4 | * 5 | * The settings file is used to specify which projects to include in your build. 6 | * In a single project build this file can be empty or even removed. 7 | * 8 | * Detailed information about configuring a multi-project build in Gradle can be found 9 | * in the user guide at https://docs.gradle.org/2.13/userguide/multi_project_builds.html 10 | */ 11 | 12 | /* 13 | // To declare projects as part of a multi-project build use the 'include' method 14 | include 'shared' 15 | include 'api' 16 | include 'services:webservice' 17 | */ 18 | 19 | rootProject.name = 'FancyFxTree' 20 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeCell.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.collections.*; 4 | import javafx.css.*; 5 | import javafx.event.*; 6 | import javafx.geometry.*; 7 | import javafx.scene.*; 8 | import javafx.scene.control.*; 9 | import javafx.scene.input.*; 10 | 11 | import java.util.*; 12 | 13 | /** 14 | * @author Christopher L Merrill (see LICENSE.txt for license details) 15 | */ 16 | public class FancyTreeCell extends TreeCell 17 | { 18 | FancyTreeCell(FancyTreeOperationHandler handler, boolean enable_dnd) 19 | { 20 | addStyle(CELL_STYLE_NAME); 21 | _handler = handler; 22 | 23 | if (enable_dnd) 24 | setupDragAndDrop(); 25 | } 26 | 27 | private void setupDragAndDrop() 28 | { 29 | setOnDragEntered(e -> 30 | { 31 | resetCursorPosition(); 32 | e.consume(); 33 | }); 34 | 35 | setOnDragDone(Event::consume); 36 | 37 | setOnDragDetected(e -> 38 | { 39 | FancyTreeView tree = (FancyTreeView) getTreeView(); 40 | 41 | FancyTreeOperationHandler.StartDragInfo result = _handler.startDrag(tree.getSelectionPaths(), tree.getSelectionModel().getSelectedItems()); 42 | if (result == null) 43 | return; 44 | 45 | ClipboardContent content = new ClipboardContent(); 46 | Map content_map = result._content; 47 | for (DataFormat format : content_map.keySet()) 48 | content.put(format, result._content.get(format)); 49 | 50 | Dragboard dragboard = startDragAndDrop(result._transfer_modes); 51 | dragboard.setContent(content); 52 | 53 | e.consume(); 54 | }); 55 | 56 | setOnDragDropped(e -> 57 | { 58 | boolean completed = _handler.finishDrag(e.getTransferMode(), e.getDragboard(), getItem(), _drop_location); 59 | e.setDropCompleted(completed); 60 | e.consume(); 61 | }); 62 | 63 | setOnDragOver(e -> 64 | { 65 | removeStyle(DROP_BEFORE_STYLE_NAME); 66 | removeStyle(DROP_ON_STYLE_NAME); 67 | removeStyle(DROP_AFTER_STYLE_NAME); 68 | 69 | updateCursorPositionAndHoverTime(e); 70 | 71 | if (getItem().getChildren().size() > 0 && !(getTreeItem().isExpanded()) && isWaitingForTreeExpand()) 72 | getTreeItem().setExpanded(true); 73 | 74 | Point2D sceneCoordinates = localToScene(0d, 0d); 75 | double cell_height = getHeight(); 76 | double mouse_y = e.getSceneY() - (sceneCoordinates.getY()); // this will be the y-coord within the cell 77 | FancyTreeOperationHandler.DragOverInfo info = _handler.dragOver(e.getDragboard(), getItem()); 78 | DropLocationCalculator calculator = new DropLocationCalculator(cell_height, mouse_y, info); 79 | _drop_location = calculator.getDropLocation(); 80 | if (_drop_location != null) 81 | { 82 | addStyle(DROP_LOCATION_TO_STYLE_MAP.get(_drop_location)); 83 | TransferMode[] modes = new TransferMode[info._transfer_modes.size()]; 84 | info._transfer_modes.toArray(modes); 85 | e.acceptTransferModes(modes); 86 | } 87 | e.consume(); 88 | }); 89 | 90 | setOnDragExited(event -> 91 | { 92 | removeStyle(DROP_BEFORE_STYLE_NAME); 93 | removeStyle(DROP_ON_STYLE_NAME); 94 | removeStyle(DROP_AFTER_STYLE_NAME); 95 | event.consume(); 96 | }); 97 | } 98 | 99 | @Override 100 | protected void updateItem(FancyTreeNodeFacade item, boolean empty) 101 | { 102 | super.updateItem(item, empty); 103 | if (empty) 104 | { 105 | setText(null); 106 | setGraphic(null); 107 | clearStyles(); 108 | } 109 | else 110 | { 111 | if (isEditing()) 112 | cancelEdit(); 113 | else 114 | updateCellUI(item); 115 | } 116 | } 117 | 118 | private void updateCellUI(FancyTreeNodeFacade item) 119 | { 120 | if (item == null) 121 | { 122 | setText(null); 123 | setGraphic(null); 124 | } 125 | else 126 | { 127 | Node node = item.getCustomCellUI(); 128 | if (node == null) 129 | { 130 | setText(item.getLabelText()); 131 | setGraphic(item.getIcon()); 132 | } 133 | else 134 | { 135 | setText(null); 136 | setGraphic(node); 137 | } 138 | updateStyles(item); 139 | } 140 | pseudoClassStateChanged(EDITING_CLASS, isEditing()); 141 | } 142 | 143 | private void updateStyles(FancyTreeNodeFacade item) 144 | { 145 | final ObservableList applied_styles = getStyleClass(); 146 | final List on_demand_styles = new ArrayList(item.getStyles()); 147 | final List styles_to_remove = new ArrayList(); 148 | for (String style : applied_styles) 149 | { 150 | if (!DEFAULT_STYLES.contains(style) 151 | && !DRAG_STYLES.contains(style)) 152 | { 153 | if (!on_demand_styles.remove(style)) 154 | styles_to_remove.add(style); 155 | } 156 | } 157 | if (styles_to_remove.size() > 0) 158 | applied_styles.removeAll(styles_to_remove); 159 | if (on_demand_styles.size() > 0) 160 | applied_styles.addAll(on_demand_styles); 161 | } 162 | 163 | private void clearStyles() 164 | { 165 | final ObservableList applied_styles = getStyleClass(); 166 | final List styles_to_remove = new ArrayList(applied_styles); 167 | styles_to_remove.removeAll(DEFAULT_STYLES); 168 | styles_to_remove.removeAll(DRAG_STYLES); 169 | applied_styles.removeAll(styles_to_remove); 170 | } 171 | 172 | private void addStyle(String new_style) 173 | { 174 | if (!getStyleClass().contains(new_style)) 175 | getStyleClass().add(new_style); 176 | } 177 | 178 | private void removeStyle(String remove_style) 179 | { 180 | getStyleClass().remove(remove_style); 181 | } 182 | 183 | // for hover-to-expand feature 184 | private void updateCursorPositionAndHoverTime(DragEvent e) 185 | { 186 | if (e.getSceneX() == _cursor_x && e.getSceneY() == _cursor_y) 187 | return; 188 | 189 | _cursor_x = (int) e.getSceneX(); 190 | _cursor_y = (int) e.getSceneY(); 191 | _cursor_hover_since = System.currentTimeMillis(); 192 | } 193 | 194 | // for hover-to-expand feature 195 | private boolean isWaitingForTreeExpand() 196 | { 197 | return System.currentTimeMillis() - _cursor_hover_since > _hover_expand_duration; 198 | } 199 | 200 | // for hover-to-expand feature 201 | private void resetCursorPosition() 202 | { 203 | _cursor_x = 0; 204 | _cursor_y = 0; 205 | _cursor_hover_since = 0; 206 | } 207 | 208 | @Override 209 | public void startEdit() 210 | { 211 | if (! isEditable() || ! getTreeView().isEditable()) 212 | return; 213 | 214 | TreeItem item = getTreeItem(); 215 | if (item == null) 216 | return; 217 | 218 | final FancyTreeCellEditor editor = getCellEditor(); 219 | editor.getNode().requestFocus(); 220 | getItem().editStarting(); 221 | 222 | super.startEdit(); 223 | 224 | if (isEditing()) 225 | { 226 | setText(null); 227 | setGraphic(editor.getNode()); 228 | } 229 | } 230 | 231 | @Override 232 | public void cancelEdit() 233 | { 234 | if (getCellEditor() != null) 235 | { 236 | _editor.cancelEdit(); 237 | _editor = null; // if you don't do this, the editor could be re-used at a future time, which is VERY hard to debug. DAMHIKT 238 | } 239 | super.cancelEdit(); 240 | updateCellUI(getItem()); 241 | if (getItem() != null) 242 | getItem().editFinished(); 243 | } 244 | 245 | @Override 246 | public void commitEdit(FancyTreeNodeFacade facade) 247 | { 248 | super.commitEdit(facade); 249 | updateCellUI(getItem()); 250 | facade.editFinished(); 251 | _editor = null; // if you don't do this, the editor could be re-used at a future time, which is VERY hard to debug. DAMHIKT 252 | } 253 | 254 | private FancyTreeCellEditor getCellEditor() 255 | { 256 | if (_editor == null) 257 | { 258 | _editor = getItem().getCustomEditorUI(); 259 | if (_editor == null) 260 | _editor = new TextCellEditor(); 261 | _editor.setCell(this); 262 | } 263 | return _editor; 264 | } 265 | 266 | /** 267 | * Set how long the user must hover (during drag) before a collapsed parent node will expand. 268 | */ 269 | void setHoverExpandDuration(long hover_expand_duration) 270 | { 271 | _hover_expand_duration = hover_expand_duration; 272 | } 273 | 274 | private final FancyTreeOperationHandler _handler; 275 | private FancyTreeOperationHandler.DropLocation _drop_location; 276 | private int _cursor_x; 277 | private int _cursor_y; 278 | private long _cursor_hover_since; 279 | private long _hover_expand_duration = FancyTreeView.DEFAULT_HOVER_EXPAND_DURATION; 280 | private FancyTreeCellEditor _editor; 281 | 282 | // 283 | // Styles for the cells 284 | // 285 | static final String CELL_STYLE_NAME = "fancytreecell"; 286 | static final String DROP_AFTER_STYLE_NAME = "fancytreecell-drop-after"; 287 | static final String DROP_BEFORE_STYLE_NAME = "fancytreecell-drop-before"; 288 | static final String DROP_ON_STYLE_NAME = "fancytreecell-drop-on"; 289 | private static final List DRAG_STYLES = new ArrayList<>(); 290 | static 291 | { 292 | DRAG_STYLES.add(DROP_AFTER_STYLE_NAME); 293 | DRAG_STYLES.add(DROP_BEFORE_STYLE_NAME); 294 | DRAG_STYLES.add(DROP_ON_STYLE_NAME); 295 | } 296 | 297 | // 298 | // Pseudo-styles for the cell 299 | // 300 | private static PseudoClass EDITING_CLASS = PseudoClass.getPseudoClass("editing"); 301 | 302 | // 303 | // Default styles for the cell (should never be removed) 304 | // 305 | private static final List DEFAULT_STYLES = new ArrayList<>(); 306 | static 307 | { 308 | DEFAULT_STYLES.add(CELL_STYLE_NAME); 309 | DEFAULT_STYLES.add("cell"); 310 | DEFAULT_STYLES.add("indexed-cell"); 311 | DEFAULT_STYLES.add("tree-cell"); 312 | } 313 | 314 | private static final Map DROP_LOCATION_TO_STYLE_MAP = new HashMap<>(); 315 | static 316 | { 317 | DROP_LOCATION_TO_STYLE_MAP.put(FancyTreeOperationHandler.DropLocation.BEFORE, DROP_BEFORE_STYLE_NAME); 318 | DROP_LOCATION_TO_STYLE_MAP.put(FancyTreeOperationHandler.DropLocation.ON, DROP_ON_STYLE_NAME); 319 | DROP_LOCATION_TO_STYLE_MAP.put(FancyTreeOperationHandler.DropLocation.AFTER, DROP_AFTER_STYLE_NAME); 320 | } 321 | 322 | private class DropLocationCalculator 323 | { 324 | DropLocationCalculator(double cell_height, double mouse_y, FancyTreeOperationHandler.DragOverInfo info) 325 | { 326 | _cell_height = cell_height; 327 | _mouse_y = mouse_y; 328 | _info = info; 329 | calculate(); 330 | } 331 | 332 | private void calculate() 333 | { 334 | // re-order and count the locations 335 | List _allowed_locations = new ArrayList<>(); 336 | if (_info._drop_locations.contains(FancyTreeOperationHandler.DropLocation.BEFORE)) 337 | _allowed_locations.add(FancyTreeOperationHandler.DropLocation.BEFORE); 338 | if (_info._drop_locations.contains(FancyTreeOperationHandler.DropLocation.ON)) 339 | _allowed_locations.add(FancyTreeOperationHandler.DropLocation.ON); 340 | if (_info._drop_locations.contains(FancyTreeOperationHandler.DropLocation.AFTER)) 341 | _allowed_locations.add(FancyTreeOperationHandler.DropLocation.AFTER); 342 | 343 | if (_allowed_locations.size() == 1) 344 | _drop_location = _allowed_locations.get(0); 345 | else if (_allowed_locations.size() == 2) 346 | { 347 | if (_mouse_y < (_cell_height * 0.5d)) 348 | _drop_location = _allowed_locations.get(0); 349 | else 350 | _drop_location = _allowed_locations.get(1); 351 | } 352 | else if (_allowed_locations.size() == 3) 353 | { 354 | if (_mouse_y < (_cell_height * 0.25d)) 355 | _drop_location = _allowed_locations.get(0); 356 | else if (_mouse_y > (_cell_height * 0.75d)) 357 | _drop_location = _allowed_locations.get(2); 358 | else 359 | _drop_location = _allowed_locations.get(1); 360 | } 361 | } 362 | 363 | FancyTreeOperationHandler.DropLocation getDropLocation() 364 | { 365 | return _drop_location; 366 | } 367 | 368 | double _cell_height; 369 | double _mouse_y; 370 | FancyTreeOperationHandler.DragOverInfo _info; 371 | FancyTreeOperationHandler.DropLocation _drop_location; 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeCellEditor.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.scene.*; 4 | 5 | /** 6 | * @author Christopher L Merrill (see LICENSE.txt for license details) 7 | */ 8 | public interface FancyTreeCellEditor 9 | { 10 | Node getNode(); 11 | void setCell(FancyTreeCell cell); 12 | void cancelEdit(); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeItemBuilder.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.scene.control.*; 4 | 5 | /** 6 | * @author Christopher L Merrill (see LICENSE.txt for license details) 7 | */ 8 | class FancyTreeItemBuilder 9 | { 10 | static TreeItem create(FancyTreeNodeFacade root) 11 | { 12 | TreeItem root_item = new TreeItem<>(root); 13 | root.setTreeItemFacade(new FancyTreeItemFacade(root_item)); 14 | 15 | addChildren(root_item, root); 16 | return root_item; 17 | } 18 | 19 | private static void addChildren(TreeItem item, FancyTreeNodeFacade node) 20 | { 21 | for (Object child_node : node.getChildren()) 22 | addChild(item, (FancyTreeNodeFacade) child_node); 23 | } 24 | 25 | private static void addChild(TreeItem item, FancyTreeNodeFacade child_node) 26 | { 27 | addChild(item, child_node, item.getChildren().size()); 28 | } 29 | 30 | static void addChild(TreeItem item, FancyTreeNodeFacade child_node, int index) 31 | { 32 | TreeItem child_item = new TreeItem<>(child_node); 33 | item.getChildren().add(index, child_item); 34 | addChildren(child_item, child_node); 35 | 36 | child_node.setTreeItemFacade(new FancyTreeItemFacade(child_item)); 37 | } 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeItemFacade.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.scene.control.*; 4 | 5 | /** 6 | * The model-facing API to a tree item. Allows the model to notify the tree of asynchronous changes 7 | * that require updates in the tree. 8 | * 9 | * @author Christopher L Merrill (see LICENSE.txt for license details) 10 | */ 11 | public class FancyTreeItemFacade 12 | { 13 | @SuppressWarnings("WeakerAccess") // part of the public API 14 | public FancyTreeItemFacade(TreeItem item) 15 | { 16 | _item = item; 17 | } 18 | 19 | /** 20 | * Re-render the node. Should be called when non-structural changes require a change to the visual presentation. 21 | */ 22 | public void refreshDisplay() 23 | { 24 | _item.setValue(_item.getValue().copyAndDestroy()); 25 | } 26 | 27 | public void addChild(FancyTreeNodeFacade child, int index) 28 | { 29 | FancyTreeItemBuilder.addChild(_item, child, index); 30 | } 31 | 32 | public void removeChild(int index, FancyTreeNodeFacade child) 33 | { 34 | try 35 | { 36 | FancyTreeNodeFacade node = _item.getChildren().get(index).getValue(); 37 | if (child == null || node.getModelNode().equals(child.getModelNode())) 38 | { 39 | TreeItem remove_item = _item.getChildren().remove(index); 40 | remove_item.getValue().destroy(); 41 | } 42 | else 43 | throw new IllegalArgumentException(String.format("The indexed sub-item (%d) didn't match the node selected for removal: %s", index, child.getModelNode().toString())); 44 | } 45 | catch (Exception e) 46 | { 47 | // index doesn't exist 48 | String child_description = "(unknown)"; 49 | if (child != null) 50 | child_description = child.getModelNode().toString(); 51 | throw new IllegalArgumentException(String.format("Unable to locate the indexed sub-item (%d) for removal: %s", index, child_description)); 52 | } 53 | } 54 | 55 | private final TreeItem _item; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeKeyHandler.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.collections.*; 4 | import javafx.scene.control.*; 5 | import javafx.scene.input.*; 6 | 7 | /** 8 | * @author Christopher L Merrill (see LICENSE.txt for license details) 9 | */ 10 | class FancyTreeKeyHandler 11 | { 12 | FancyTreeKeyHandler(TreeView tree_view, FancyTreeOperationHandler handler) 13 | { 14 | _tree = tree_view; 15 | _handler = handler; 16 | 17 | _tree.setOnKeyPressed(event -> 18 | { 19 | ObservableList selected_items = _tree.getSelectionModel().getSelectedItems(); 20 | boolean handled; 21 | if (event.getCode().equals(KeyCode.DELETE) && !event.isShiftDown()) 22 | handled = _handler.handleDelete(selected_items); 23 | else if ((event.isShortcutDown() && event.getCode().equals(KeyCode.C)) 24 | || (event.isShortcutDown() && event.getCode().equals(KeyCode.INSERT))) 25 | handled = _handler.handleCopy(selected_items); 26 | else if ((event.isShortcutDown() && event.getCode().equals(KeyCode.X)) 27 | || (event.isShiftDown() && event.getCode().equals(KeyCode.DELETE))) 28 | handled = _handler.handleCut(selected_items); 29 | else if ((event.isShortcutDown() && event.getCode().equals(KeyCode.V)) 30 | || (event.isShiftDown() && event.getCode().equals(KeyCode.INSERT))) 31 | handled = _handler.handlePaste(selected_items); 32 | else if (event.isShortcutDown() && event.getCode().equals(KeyCode.Z)) 33 | handled = _handler.handleUndo(); 34 | else 35 | return; 36 | 37 | if (handled) 38 | event.consume(); 39 | }); 40 | } 41 | 42 | private TreeView _tree; 43 | private FancyTreeOperationHandler _handler; 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeNodeFacade.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.scene.*; 4 | 5 | import java.util.*; 6 | 7 | /** 8 | * The tree-facing API of the tree data model. Allows for a complex data structure that is not 9 | * constrained by the expectations of the TreeView or TreeItem. 10 | * 11 | * @author Christopher L Merrill (see LICENSE.txt for license details) 12 | */ 13 | public interface FancyTreeNodeFacade 14 | { 15 | /** 16 | * Due to a design flaw in the TreeView, the only way to force an update to a specific tree node is 17 | * to replace it. The FancyTreeNodeFacade allows this to happen without changing the underlying 18 | * datamodel. This method is necessary to accomplish that. Implementers should make a copy of this 19 | * object, including registering/deregistering any listeners on the underlying data model. 20 | */ 21 | FancyTreeNodeFacade copyAndDestroy(); 22 | 23 | /** 24 | * This node facade will no longer be used. Implementers should deregister listeners and should 25 | * no longer make calls to the item facade. 26 | */ 27 | void destroy(); 28 | 29 | List> getChildren(); 30 | Node getCustomCellUI(); 31 | String getLabelText(); 32 | T getModelNode(); 33 | List getStyles(); 34 | 35 | /** 36 | * Start editing the cell. Return a FancyTreeCellEditor if a custom editor is needed. 37 | * Return null for the default editor (text field) 38 | */ 39 | void editStarting(); 40 | FancyTreeCellEditor getCustomEditorUI(); 41 | void editFinished(); // called when the edit is done 42 | 43 | /** 44 | * Return an icon for the tree item or null if none. 45 | */ 46 | Node getIcon(); 47 | 48 | /** 49 | * Sets the correspnding FancyTreeItemFacade for this tree node. This is needed for adding and 50 | * removing tree nodes dynamically and refreshing the display due to other changes to the node. 51 | */ 52 | void setTreeItemFacade(FancyTreeItemFacade item_facade); 53 | 54 | /** 55 | * Called by the default FancyTreeCellEditor (TextCellEditor) when the node text has changed. 56 | * May or may not be used by a custom FancyTreeCellEditor. 57 | */ 58 | void setLabelText(String new_value); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeOperationHandler.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.collections.*; 4 | import javafx.scene.control.*; 5 | import javafx.scene.input.*; 6 | 7 | import java.util.*; 8 | 9 | /** 10 | * @author Christopher L Merrill (see LICENSE.txt for license details) 11 | */ 12 | public abstract class FancyTreeOperationHandler 13 | { 14 | public void selectionChanged(ObservableList> selected_items) { } 15 | 16 | public boolean handleDelete(ObservableList> selected_items) { return false; } 17 | public boolean handleCut(ObservableList> selected_items) { return false; } 18 | public boolean handleCopy(ObservableList> selected_items) { return false; } 19 | public boolean handlePaste(ObservableList> selected_items) { return false; } 20 | public boolean handleUndo() { return false; } 21 | 22 | public StartDragInfo startDrag(List> selection_paths, ObservableList> selected_items) { return null; } 23 | public boolean finishDrag(TransferMode transfer_mode, Dragboard dragboard, T item, DropLocation location) { return false; } 24 | public void handleDoubleClick(TreeCell cell, boolean control_down, boolean shift_down, boolean alt_down) { } 25 | public ContextMenu getContextMenu(ObservableList> selected_items) { return null; } 26 | 27 | public DragOverInfo dragOver(Dragboard dragboard, T onto_node) 28 | { 29 | DragOverInfo info = new DragOverInfo(); 30 | info.addAllModesAndLocations(); 31 | return info; 32 | } 33 | 34 | @SuppressWarnings("WeakerAccess") // part of public API 35 | public enum DropLocation 36 | { 37 | BEFORE, 38 | AFTER, 39 | ON 40 | } 41 | 42 | public class StartDragInfo 43 | { 44 | public StartDragInfo() 45 | { 46 | _transfer_modes = new TransferMode[] {TransferMode.COPY, TransferMode.MOVE}; 47 | } 48 | 49 | public void addContent(DataFormat format, Object content) 50 | { 51 | _content.put(format, content); 52 | } 53 | 54 | @SuppressWarnings("WeakerAccess") // part of public API 55 | public Map _content = new HashMap<>(); 56 | 57 | @SuppressWarnings("WeakerAccess") // part of public API 58 | public TransferMode[] _transfer_modes; 59 | } 60 | 61 | public class DragOverInfo 62 | { 63 | public void addAllModesAndLocations() 64 | { 65 | addTransferMode(TransferMode.COPY); 66 | addTransferMode(TransferMode.MOVE); 67 | addDropLocation(DropLocation.BEFORE); 68 | addDropLocation(DropLocation.ON); 69 | addDropLocation(DropLocation.AFTER); 70 | } 71 | 72 | @SuppressWarnings("WeakerAccess") // part of public API 73 | public void addTransferMode(TransferMode mode) 74 | { 75 | _transfer_modes.add(mode); 76 | } 77 | 78 | @SuppressWarnings("WeakerAccess,unused") // part of public API 79 | public void removeTransferMode(TransferMode mode) 80 | { 81 | _transfer_modes.remove(mode); 82 | } 83 | 84 | @SuppressWarnings("WeakerAccess") // part of public API 85 | public void addDropLocation(DropLocation location) 86 | { 87 | _drop_locations.add(location); 88 | } 89 | 90 | @SuppressWarnings("WeakerAccess,unused") // part of public API 91 | public void removeDropLocation(DropLocation location) 92 | { 93 | _drop_locations.remove(location); 94 | } 95 | 96 | List _transfer_modes = new ArrayList<>(); 97 | List _drop_locations = new ArrayList<>(); 98 | } 99 | 100 | /** 101 | * Creates context menu items for the EditTypes provided. Call this from the 102 | * #getContextMenu() method to easily add them to the menu. 103 | */ 104 | protected MenuItem[] createEditMenuItems(ObservableList> selected_items, EditType... types) 105 | { 106 | MenuItem[] items = new MenuItem[types.length]; 107 | int index = 0; 108 | for (EditType type : types) 109 | { 110 | MenuItem item = new MenuItem(type.name()); 111 | item.setId(type.getMenuId()); 112 | items[index++] = item; 113 | switch (type) 114 | { 115 | case Cut: 116 | item.setOnAction(event -> handleCut(selected_items)); 117 | break; 118 | case Copy: 119 | item.setOnAction(event -> handleCopy(selected_items)); 120 | break; 121 | case Delete: 122 | item.setOnAction(event -> handleDelete(selected_items)); 123 | break; 124 | case Paste: 125 | item.setOnAction(event -> handlePaste(selected_items)); 126 | break; 127 | } 128 | } 129 | return items; 130 | } 131 | 132 | public enum EditType 133 | { 134 | Cut("ftoh-et-cut"), 135 | Copy("ftoh-et-copy"), 136 | Paste("ftoh-et-paste"), 137 | Delete("ftoh-et-delete"); 138 | 139 | EditType(String id) 140 | { 141 | _id = id; 142 | } 143 | 144 | public String getMenuId() 145 | { 146 | return _id; 147 | } 148 | 149 | private String _id; 150 | } 151 | } 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeView.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.application.*; 4 | import javafx.collections.*; 5 | import javafx.scene.control.*; 6 | import javafx.scene.input.*; 7 | 8 | import java.util.*; 9 | 10 | /** 11 | * A tree with lots of fancy features already implemented...so you don't have to. It is intended to help 12 | * implement a tree on a complex hierarchical data model without forcing the model to comply with the expectations 13 | * of TreeView. This includes asynchronous state changes and complex user interactions, such as drag-and-drop. 14 | * 15 | * Fancy features include: 16 | * - update view when the model node properties change (not just when the entire model node is replaced). This requires the custom FancyTreeNode extension to notify the FancyTreeItemFacade when these changes happen. 17 | * - update view from asynchronous events (as above) 18 | * - smart scroll-to-item behavior 19 | * - expand tree to make an item visible 20 | * - cut/copy/paste keystroke support implemented, including common OS key combinations (Windows) 21 | * - drag and drop support implemented, including tree-aware drop targets (drop before, into, or after) 22 | * - hover cursor over a tree item to expand it during drag 23 | * 24 | * @author Christopher L Merrill (see LICENSE.txt for license details) 25 | */ 26 | @SuppressWarnings("ALL") 27 | public class FancyTreeView extends TreeView 28 | { 29 | @SuppressWarnings("WeakerAccess") // part of public API 30 | public FancyTreeView(FancyTreeOperationHandler ops_handler) 31 | { 32 | this(ops_handler, true); 33 | } 34 | 35 | /** 36 | * @param enable_dnd False to disable drag-and-drop support. 37 | */ 38 | @SuppressWarnings("unused,WeakerAccess") // public API 39 | public FancyTreeView(FancyTreeOperationHandler ops_handler, boolean enable_dnd) 40 | { 41 | _ops_handler = ops_handler; 42 | _enable_dnd = enable_dnd; 43 | new FancyTreeKeyHandler(this, _ops_handler); 44 | setCellFactory(param -> 45 | { 46 | FancyTreeCell cell = new FancyTreeCell(_ops_handler, _enable_dnd); 47 | cell.setHoverExpandDuration(_hover_expand_duration); 48 | 49 | cell.addEventFilter(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> 50 | { 51 | if (e.getClickCount() == 2 && e.getButton().equals(MouseButton.PRIMARY)) 52 | { 53 | e.consume(); 54 | _ops_handler.handleDoubleClick(cell, e.isControlDown(), e.isShiftDown(), e.isAltDown()); 55 | // Platform.runLater(() -> _ops_handler.handleDoubleClick(cell, e.isControlDown(), e.isShiftDown(), e.isAltDown())); 56 | } 57 | }); 58 | 59 | return cell; 60 | }); 61 | setSkin(new FancyTreeViewSkin(this)); 62 | getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); 63 | getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> 64 | _ops_handler.selectionChanged(getSelectionModel().getSelectedItems())); 65 | 66 | addEventFilter(MouseEvent.MOUSE_RELEASED, e-> 67 | { 68 | if (_context_menu != null) 69 | { 70 | _context_menu.hide(); 71 | _context_menu = null; 72 | } 73 | if (e.getButton() == MouseButton.SECONDARY) 74 | { 75 | ObservableList selections = getSelectionModel().getSelectedItems(); 76 | _context_menu = _ops_handler.getContextMenu(selections); 77 | if (_context_menu != null) 78 | _context_menu.show(this, e.getScreenX(), e.getScreenY()); 79 | } 80 | }); 81 | } 82 | 83 | @SuppressWarnings("WeakerAccess") // part of public API 84 | public void expandAll() 85 | { 86 | TreeItem root = getRoot(); 87 | expandNodeAndChilren(root); 88 | } 89 | 90 | private void expandNodeAndChilren(TreeItem node) 91 | { 92 | node.setExpanded(true); 93 | node.getChildren().forEach(this::expandNodeAndChilren); 94 | } 95 | 96 | public void collapseAll() 97 | { 98 | TreeItem root = getRoot(); 99 | collapseNodeAndChildren(root); 100 | } 101 | 102 | private void collapseNodeAndChildren(TreeItem node) 103 | { 104 | node.getChildren().forEach(this::collapseNodeAndChildren); 105 | node.setExpanded(false); 106 | } 107 | 108 | @SuppressWarnings("WeakerAccess") // part of public API 109 | public List> expandToMakeVisible(Object node) 110 | { 111 | TreeItem item = findItemForModelNode(node); 112 | if (item == null) 113 | return Collections.emptyList(); 114 | 115 | List> expanded = new ArrayList<>(); 116 | item = item.getParent(); 117 | while (item != null) 118 | { 119 | if (!item.isExpanded()) 120 | { 121 | item.setExpanded(true); 122 | expanded.add(item); 123 | } 124 | item = item.getParent(); 125 | } 126 | return expanded; 127 | } 128 | 129 | @SuppressWarnings({"unused", "WeakerAccess"}) // public API 130 | public List> expandAndScrollTo(Object node) 131 | { 132 | List> expanded = expandToMakeVisible(node); 133 | scrollToVisibleItem(node); 134 | return expanded; 135 | } 136 | 137 | @SuppressWarnings({"unused", "UnusedReturnValue"}) // public API 138 | public List> expandScrollToAndSelect(Object node) 139 | { 140 | TreeItem item = findItemForModelNode(node); 141 | if (item == null) 142 | return Collections.emptyList(); 143 | 144 | List> expanded = expandToMakeVisible(node); 145 | scrollToVisibleItem(node); 146 | 147 | getSelectionModel().select(item); 148 | return expanded; 149 | } 150 | 151 | private TreeItem findItemForModelNode(Object node) 152 | { 153 | return findItemForModelNode(getRoot(), node); 154 | } 155 | 156 | private TreeItem findItemForModelNode(TreeItem item, Object node) 157 | { 158 | if (item.getValue().getModelNode() == node) 159 | return item; 160 | for (TreeItem child : item.getChildren()) 161 | { 162 | TreeItem found = findItemForModelNode(child, node); 163 | if (found != null) 164 | return found; 165 | } 166 | return null; 167 | } 168 | 169 | @SuppressWarnings("WeakerAccess") // part of public API 170 | public int findIndexOfVisibleItem(TreeItem target_item) 171 | { 172 | int index = 0; 173 | TreeItem item = getTreeItem(index); 174 | while (item != null) 175 | { 176 | if (item == target_item) 177 | return index; 178 | index++; 179 | item = getTreeItem(index); 180 | } 181 | 182 | return -1; 183 | } 184 | 185 | @SuppressWarnings("WeakerAccess") // part of public API 186 | public void scrollToAndMakeVisible(Object node) 187 | { 188 | TreeItem item = findItemForModelNode(node); 189 | if (item == null) 190 | return; 191 | expandToMakeVisible(item); // do this first - can't scoll to an item if it is hidden (any ancestor is not expanded) 192 | 193 | int index = findIndexOfVisibleItem(item); 194 | if (!((FancyTreeViewSkin)getSkin()).isIndexVisible(index)) // don't scroll if it is already visible on screen 195 | Platform.runLater(() -> scrollTo(index)); 196 | } 197 | 198 | @SuppressWarnings("unused,WeakerAccess") // public API 199 | public void scrollToVisibleItem(Object node) 200 | { 201 | TreeItem item = findItemForModelNode(node); 202 | if (item == null) 203 | return; 204 | 205 | int index = findIndexOfVisibleItem(item); 206 | if (!((FancyTreeViewSkin)getSkin()).isIndexVisible(index)) // don't scroll if it is already visible on screen 207 | Platform.runLater(() -> scrollTo(index)); 208 | } 209 | 210 | @SuppressWarnings("WeakerAccess") // part of public API 211 | public List> getSelectionPaths() 212 | { 213 | ObservableList selected_items = getSelectionModel().getSelectedItems(); 214 | List> paths = new ArrayList<>(); 215 | for (TreeItem item : selected_items) 216 | { 217 | List path = new ArrayList<>(); 218 | createPathToItem(path, item); 219 | paths.add(path); 220 | } 221 | return paths; 222 | } 223 | 224 | private void createPathToItem(List path, TreeItem item) 225 | { 226 | while (item.getParent() != null) 227 | { 228 | TreeItem parent = item.getParent(); 229 | path.add(0, parent.getChildren().indexOf(item)); 230 | item = parent; 231 | } 232 | } 233 | 234 | public void setRoot(FancyTreeNodeFacade root_facade) 235 | { 236 | setRoot(FancyTreeItemBuilder.create(root_facade)); 237 | } 238 | 239 | @SuppressWarnings("WeakerAccess") // part of public API 240 | public void setHoverExpandDuration(long hover_expand_duration) 241 | { 242 | _hover_expand_duration = hover_expand_duration; 243 | } 244 | 245 | 246 | 247 | private final FancyTreeOperationHandler _ops_handler; 248 | private final boolean _enable_dnd; 249 | private ContextMenu _context_menu = null; 250 | 251 | private long _hover_expand_duration = DEFAULT_HOVER_EXPAND_DURATION; 252 | static final long DEFAULT_HOVER_EXPAND_DURATION = 2000; 253 | } -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/FancyTreeViewSkin.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.scene.control.*; 4 | import javafx.scene.control.skin.*; 5 | 6 | /** 7 | * @author Christopher L Merrill (see LICENSE.txt for license details) 8 | */ 9 | class FancyTreeViewSkin extends TreeViewSkin 10 | { 11 | FancyTreeViewSkin(TreeView tree) 12 | { 13 | super(tree); 14 | } 15 | 16 | boolean isIndexVisible(int index) 17 | { 18 | VirtualFlow flow = getVirtualFlow(); 19 | if (flow.getFirstVisibleCell() != null && 20 | flow.getLastVisibleCell() != null && 21 | flow.getFirstVisibleCell().getIndex() <= index && 22 | flow.getLastVisibleCell().getIndex() >= index) 23 | return true; 24 | return false; 25 | } 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/TextCellEditor.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.scene.*; 4 | import javafx.scene.control.*; 5 | import javafx.scene.input.*; 6 | 7 | /** 8 | * @author Christopher L Merrill (see LICENSE.txt for license details) 9 | */ 10 | public class TextCellEditor implements FancyTreeCellEditor 11 | { 12 | TextCellEditor() 13 | { 14 | _field = new TextField("value1"); 15 | _field.getStyleClass().add(NODE_STYLE); 16 | 17 | _field.focusedProperty().addListener((observable, oldValue, newValue) -> 18 | { 19 | if (!newValue && !_done) // focus lost 20 | { 21 | _done = true; 22 | _cell.getItem().setLabelText(_field.getText()); 23 | } 24 | }); 25 | _field.setOnKeyPressed(event -> 26 | { 27 | if (event.getCode().equals(KeyCode.ENTER)) 28 | { 29 | _cell.getItem().setLabelText(_field.getText()); 30 | _done = true; 31 | _cell.commitEdit(_cell.getItem()); 32 | event.consume(); 33 | } 34 | else if (event.getCode().equals(KeyCode.ESCAPE)) 35 | { 36 | _done = true; 37 | _cell.cancelEdit(); 38 | event.consume(); 39 | } 40 | }); 41 | } 42 | 43 | @Override 44 | public Node getNode() 45 | { 46 | return _field; 47 | } 48 | 49 | @Override 50 | public void setCell(FancyTreeCell cell) 51 | { 52 | _cell = cell; 53 | _field.setText(cell.getItem().getLabelText()); 54 | } 55 | 56 | private TextField _field; 57 | private FancyTreeCell _cell; 58 | 59 | @Override 60 | public void cancelEdit() 61 | { 62 | _done = true; 63 | } 64 | 65 | /** 66 | * False until an edit has been completed. Then true to prevent further events from duplicating the commit. 67 | */ 68 | private boolean _done = false; 69 | 70 | final static String NODE_STYLE = "fancyfxtree-default-cell-editor"; 71 | } -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/example/ExampleCustomCellEditor.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree.example; 2 | 3 | import javafx.scene.*; 4 | import javafx.scene.control.*; 5 | import net.christophermerrill.FancyFxTree.*; 6 | 7 | /** 8 | * @author Christopher L Merrill (see LICENSE.txt for license details) 9 | */ 10 | public class ExampleCustomCellEditor implements FancyTreeCellEditor 11 | { 12 | @Override 13 | public Node getNode() 14 | { 15 | final Label editor = new Label("custom editor"); 16 | editor.getStyleClass().add(NODE_STYLE); 17 | return editor; 18 | } 19 | 20 | @Override 21 | public void setCell(FancyTreeCell cell) 22 | { 23 | 24 | } 25 | 26 | @Override 27 | public void cancelEdit() 28 | { 29 | 30 | } 31 | 32 | public final static String NODE_STYLE = "example-custom-cell-editor"; 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/example/ExampleDataNode.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree.example; 2 | 3 | import java.io.*; 4 | import java.util.*; 5 | 6 | /** 7 | * This example class represents your application data model. To fit your 8 | * data into the tree, implement FancyTreeNodeFacade to wrap these objects. 9 | 10 | * @author Christopher L Merrill (see LICENSE.txt for license details) 11 | */ 12 | public class ExampleDataNode implements Serializable 13 | { 14 | public ExampleDataNode(String name) 15 | { 16 | _name = name; 17 | } 18 | 19 | private ExampleDataNode(ExampleDataNode original) 20 | { 21 | _name = original.getName(); 22 | } 23 | 24 | static ExampleDataNode deepCopy(ExampleDataNode original, boolean annotate_label) 25 | { 26 | ExampleDataNode copy = new ExampleDataNode(original); 27 | if (annotate_label) 28 | copy.setName(getCopyName(original)); 29 | for (ExampleDataNode child : original.getChildren()) 30 | copy.addChild(deepCopy(child, annotate_label)); 31 | return copy; 32 | } 33 | 34 | public static String getCopyName(ExampleDataNode node) 35 | { 36 | return node.getName() + " (copy)"; 37 | } 38 | 39 | public List getChildren() 40 | { 41 | return Collections.unmodifiableList(_children); 42 | } 43 | 44 | public void addChild(ExampleDataNode child) 45 | { 46 | _children.add(child); 47 | fireChildAdded(child, _children.size() - 1); 48 | } 49 | 50 | void addChild(int index, ExampleDataNode child) 51 | { 52 | _children.add(index, child); 53 | fireChildAdded(child, index); 54 | } 55 | 56 | public void removeChild(ExampleDataNode child) 57 | { 58 | int index = _children.indexOf(child); 59 | if (_children.remove(child)) 60 | fireChildRemoved(child, index); 61 | } 62 | 63 | public String getName() 64 | { 65 | return _name; 66 | } 67 | 68 | public void setName(String new_name) 69 | { 70 | _name = new_name; 71 | firePropertyChange(); 72 | } 73 | 74 | String getExtraData() 75 | { 76 | return _extra_data; 77 | } 78 | 79 | void setExtraData(String extra_data) 80 | { 81 | _extra_data = extra_data; 82 | firePropertyChange(); 83 | } 84 | 85 | public void insertChild(ExampleDataNode child, int index) 86 | { 87 | _children.add(index, child); 88 | fireChildAdded(child, index); 89 | } 90 | 91 | /** 92 | * Picks a random node in the hierarchy. 93 | */ 94 | ExampleDataNode pickRandom() 95 | { 96 | List all = toList(); 97 | int random_index = new Random().nextInt(all.size()); 98 | return all.get(random_index); 99 | } 100 | 101 | private List toList() 102 | { 103 | List all = new ArrayList<>(); 104 | all.add(this); 105 | addDescendantsToList(all); 106 | return all; 107 | } 108 | 109 | private void addDescendantsToList(List list) 110 | { 111 | for (ExampleDataNode child : _children) 112 | { 113 | list.add(child); 114 | child.addDescendantsToList(list); 115 | } 116 | } 117 | 118 | public ExampleDataNode findParentFor(ExampleDataNode target) 119 | { 120 | if (_children.contains(target)) 121 | return this; 122 | for (ExampleDataNode child : _children) 123 | { 124 | ExampleDataNode parent = child.findParentFor(target); 125 | if (parent != null) 126 | return parent; 127 | } 128 | return null; 129 | } 130 | 131 | boolean isAncestorOf(ExampleDataNode target) 132 | { 133 | if (_children.contains(target)) 134 | return true; 135 | for (ExampleDataNode child : _children) 136 | if (child.isAncestorOf(target)) 137 | return true; 138 | return false; 139 | } 140 | 141 | void addAfter(ExampleDataNode to_add, ExampleDataNode to_follow) 142 | { 143 | int index = _children.indexOf(to_follow) + 1; 144 | _children.add(index, to_add); 145 | fireChildAdded(to_add, index); 146 | } 147 | 148 | public ExampleDataNode getNodeByName(String name) 149 | { 150 | if (name.equals(_name)) 151 | return this; 152 | for (ExampleDataNode child : _children) 153 | { 154 | ExampleDataNode found = child.getNodeByName(name); 155 | if (found != null) 156 | return found; 157 | } 158 | return null; 159 | } 160 | 161 | public boolean contains(ExampleDataNode target) 162 | { 163 | return contains(target, false); 164 | } 165 | 166 | public boolean contains(ExampleDataNode target, boolean direct_children_only) 167 | { 168 | if (equals(target)) 169 | return true; 170 | for (ExampleDataNode child : _children) 171 | { 172 | if (child == target) 173 | return true; 174 | if (!direct_children_only && child.contains(target)) 175 | return true; 176 | } 177 | return false; 178 | } 179 | 180 | private UUID getId() 181 | { 182 | return _id; 183 | } 184 | 185 | public void addStyle(String style) 186 | { 187 | _styles.add(style); 188 | firePropertyChange(); 189 | } 190 | 191 | public void removeStyle(String style) 192 | { 193 | _styles.remove(style); 194 | firePropertyChange(); 195 | } 196 | 197 | List getStyles() 198 | { 199 | return _styles; 200 | } 201 | 202 | @Override 203 | public boolean equals(Object obj) 204 | { 205 | return obj instanceof ExampleDataNode 206 | && ((ExampleDataNode)obj).getId().equals(_id); 207 | } 208 | 209 | @Override 210 | public String toString() 211 | { 212 | return _name; 213 | } 214 | 215 | private List _children = new ArrayList<>(); 216 | private String _name; 217 | private String _extra_data; 218 | private List _styles = new ArrayList<>(); 219 | private UUID _id = UUID.randomUUID(); 220 | 221 | public boolean _use_custom_editor = false; 222 | 223 | private transient List _change_listeners; 224 | 225 | private void firePropertyChange() 226 | { 227 | if (_change_listeners != null) 228 | //noinspection Convert2streamapi (causes a ConcurrentModificationException) 229 | for (ChangeListener listener : _change_listeners) 230 | listener.propertyChanged(); 231 | } 232 | 233 | private void fireChildAdded(ExampleDataNode node, int index) 234 | { 235 | if (_change_listeners != null) 236 | for (ChangeListener listener : _change_listeners) 237 | listener.childAdded(node, index); 238 | } 239 | 240 | private void fireChildRemoved(ExampleDataNode node, int index) 241 | { 242 | if (_change_listeners != null) 243 | for (ChangeListener listener : _change_listeners) 244 | listener.childRemoved(node, index); 245 | } 246 | 247 | synchronized void addChangeListener(ChangeListener listener) 248 | { 249 | if (_change_listeners == null) 250 | _change_listeners = new ArrayList<>(); 251 | if (!_change_listeners.contains(listener)) 252 | _change_listeners.add(listener); 253 | } 254 | 255 | synchronized void removeChangeListener(ChangeListener listener) 256 | { 257 | if (_change_listeners != null) 258 | _change_listeners.remove(listener); 259 | } 260 | 261 | synchronized void replaceChangeListener(ChangeListener old_listener, ChangeListener new_listener) 262 | { 263 | if (_change_listeners != null) 264 | { 265 | _change_listeners.remove(old_listener); 266 | _change_listeners.add(new_listener); 267 | } 268 | } 269 | 270 | interface ChangeListener 271 | { 272 | void childAdded(ExampleDataNode child, int index); 273 | void childRemoved(ExampleDataNode child, int index); 274 | void propertyChanged(); 275 | } 276 | } 277 | 278 | 279 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/example/ExampleDataNodeBuilder.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree.example; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * This class builds nodes to provide initial data for the example (and unit tests). 7 | * 8 | * @author Christopher L Merrill (see LICENSE.txt for license details) 9 | */ 10 | public class ExampleDataNodeBuilder 11 | { 12 | public static ExampleDataNode create(int[] num_descendents) 13 | { 14 | ExampleDataNode root = new ExampleDataNode("1"); 15 | addDecendents(root, num_descendents); 16 | return root; 17 | } 18 | 19 | /** 20 | * Adds a hierarchy of children, grandchildren. The length of the array indicates 21 | * the depth of the hierarchy. The values of the array indicates the number of 22 | * descendants at each depth. 23 | */ 24 | private static void addDecendents(ExampleDataNode parent, int[] num_descendents) 25 | { 26 | if (num_descendents.length < 1) 27 | return; 28 | 29 | int[] num_grandchildren = null; 30 | if (num_descendents.length > 1) 31 | num_grandchildren = Arrays.copyOfRange(num_descendents, 1, num_descendents.length); 32 | 33 | for (int i = 0; i < num_descendents[0]; i++) 34 | { 35 | ExampleDataNode child = new ExampleDataNode(parent.getName() + "." + (i+1)); 36 | if (num_grandchildren != null) 37 | addDecendents(child, num_grandchildren); 38 | parent.addChild(child); 39 | } 40 | } 41 | 42 | public static ExampleDataNode createRandomLeaf(ExampleDataNode root) 43 | { 44 | ExampleDataNode node = root; 45 | List children = node.getChildren(); 46 | while (children.size() > 0) 47 | { 48 | node = children.get(new Random().nextInt(children.size())); 49 | children = node.getChildren(); 50 | } 51 | 52 | ExampleDataNode new_node = new ExampleDataNode(node.getName() + " - new leaf "); 53 | node.addChild(new_node); 54 | return new_node; 55 | } 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/example/ExampleOperationHandler.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree.example; 2 | 3 | import javafx.application.*; 4 | import javafx.collections.*; 5 | import javafx.scene.control.*; 6 | import javafx.scene.input.*; 7 | import net.christophermerrill.FancyFxTree.*; 8 | 9 | import java.util.*; 10 | import java.util.stream.*; 11 | 12 | import static net.christophermerrill.FancyFxTree.FancyTreeOperationHandler.EditType.*; 13 | 14 | /** 15 | * @author Christopher L Merrill (see LICENSE.txt for license details) 16 | */ 17 | public class ExampleOperationHandler extends FancyTreeOperationHandler 18 | { 19 | public ExampleOperationHandler(ExampleDataNode root) 20 | { 21 | _root = root; 22 | } 23 | 24 | @Override 25 | public boolean handleDelete(ObservableList> selected_items) 26 | { 27 | for (TreeItem item : selected_items) 28 | { 29 | ExampleDataNode node_to_remove = item.getValue().getModelNode(); 30 | ExampleDataNode parent = _root.findParentFor(node_to_remove); 31 | if (parent != null) 32 | parent.removeChild(node_to_remove); 33 | } 34 | return true; 35 | } 36 | 37 | @Override 38 | public boolean handleCut(ObservableList> selected_items) 39 | { 40 | _cut = true; 41 | _copy = false; 42 | _cut_or_copied_nodes = captureSelection(selected_items); 43 | return true; 44 | } 45 | 46 | @Override 47 | public boolean handleCopy(ObservableList> selected_items) 48 | { 49 | _copy = true; 50 | _cut = false; 51 | _cut_or_copied_nodes = captureSelection(selected_items); 52 | return true; 53 | } 54 | 55 | private List captureSelection(ObservableList> selected_items) 56 | { 57 | return selected_items.stream().filter(item -> item != null && item.getValue() != null).map(item -> item.getValue().getModelNode()).collect(Collectors.toList()); 58 | } 59 | 60 | @Override 61 | public boolean handlePaste(ObservableList> selected_items) 62 | { 63 | if (!_copy && !_cut) 64 | { 65 | System.out.println("Expected either copy or cut. Neither...fail!"); 66 | return false; 67 | } 68 | 69 | ExampleDataNode target = selected_items.get(0).getValue().getModelNode(); 70 | ExampleDataNode parent = _root.findParentFor(target); 71 | 72 | for (ExampleDataNode selected_node : _cut_or_copied_nodes) 73 | { 74 | if (_copy) 75 | { 76 | ExampleDataNode node_to_paste = ExampleDataNode.deepCopy(selected_node, true); 77 | parent.addAfter(node_to_paste, target); 78 | } 79 | else if (_cut) 80 | { 81 | ExampleDataNode parent_to_cut_from = _root.findParentFor(selected_node); 82 | parent_to_cut_from.removeChild(selected_node); 83 | parent.addAfter(selected_node, target); 84 | } 85 | } 86 | 87 | _copy = false; 88 | _cut = false; 89 | _selected_nodes = Collections.emptyList(); 90 | return true; 91 | } 92 | 93 | @Override 94 | public boolean handleUndo() 95 | { 96 | System.out.println("Undo is not implemented for this example"); 97 | return false; 98 | } 99 | 100 | @Override 101 | public StartDragInfo startDrag(List> selection_paths, ObservableList> selected_items) 102 | { 103 | StartDragInfo info = new StartDragInfo(); 104 | _dragged_items = selected_items; 105 | List selections = selected_items.stream().map(item -> item.getValue().getModelNode()).collect(Collectors.toList()); 106 | info.addContent(LIST_OF_NODES, selections); 107 | _drag_count = selected_items.size(); 108 | return info; 109 | } 110 | 111 | @Override 112 | public DragOverInfo dragOver(Dragboard dragboard, ExampleTreeNodeFacade onto_node) 113 | { 114 | DragOverInfo info = new DragOverInfo(); 115 | if (dragboard.getContent(LIST_OF_NODES) != null) 116 | { 117 | _dropped_nodes = (List) dragboard.getContent(LIST_OF_NODES); 118 | if (_dropped_nodes.contains(onto_node.getModelNode())) 119 | return info; 120 | for (ExampleDataNode to_be_dropped : _dropped_nodes) 121 | if (to_be_dropped.isAncestorOf(onto_node.getModelNode())) 122 | return info; 123 | } 124 | 125 | info.addAllModesAndLocations(); 126 | return info; 127 | } 128 | 129 | @Override 130 | public boolean finishDrag(TransferMode transfer_mode, Dragboard dragboard, ExampleTreeNodeFacade item, DropLocation location) 131 | { 132 | if (dragboard.getContent(LIST_OF_NODES) != null) 133 | { 134 | _dropped_nodes = (List) dragboard.getContent(LIST_OF_NODES); 135 | if (_dropped_nodes.contains(item.getModelNode())) 136 | return false; // TODO indicate this? 137 | ExampleDataNode parent; 138 | int add_index; 139 | switch (location) 140 | { 141 | case BEFORE: 142 | parent = _root.findParentFor(item.getModelNode()); 143 | add_index = parent.getChildren().indexOf(item.getModelNode()); 144 | break; 145 | case ON: 146 | parent = item.getModelNode(); 147 | add_index = (item.getModelNode()).getChildren().size(); 148 | break; 149 | case AFTER: 150 | parent = _root.findParentFor(item.getModelNode()); 151 | add_index = parent.getChildren().indexOf(item.getModelNode()) + 1; 152 | break; 153 | default: 154 | return false; 155 | } 156 | 157 | for (ExampleDataNode node : _dropped_nodes) 158 | { 159 | ExampleDataNode node_to_drop; 160 | if (transfer_mode.equals(TransferMode.COPY)) 161 | node_to_drop = ExampleDataNode.deepCopy(node, true); 162 | else 163 | { 164 | node_to_drop = node; 165 | Platform.runLater(() -> _root.findParentFor(node).removeChild(node_to_drop)); 166 | } 167 | 168 | final int index_to_add_at = add_index; 169 | Platform.runLater(() -> parent.addChild(index_to_add_at, node_to_drop)); // updates to tree should be done on the UI thread when possible? 170 | add_index++; 171 | } 172 | return true; 173 | } 174 | 175 | return false; 176 | } 177 | 178 | public void selectionChanged(ObservableList> selected_items) 179 | { 180 | _selected_nodes = captureSelection(selected_items); 181 | } 182 | 183 | public List getSelectedNodes() 184 | { 185 | return _selected_nodes; 186 | } 187 | 188 | @SuppressWarnings("unused") // public API 189 | ExampleDataNode getSelectedNode() 190 | { 191 | if (_selected_nodes.size() == 1) 192 | return _selected_nodes.get(0); 193 | return null; 194 | } 195 | 196 | @Override 197 | public void handleDoubleClick(TreeCell cell, boolean control_down, boolean shift_down, boolean alt_down) 198 | { 199 | _double_clicked_node_name = cell.getTreeItem().getValue().getModelNode().getName(); 200 | if (cell.getTreeView().isEditable() && !cell.isEditing()) 201 | cell.startEdit(); 202 | } 203 | 204 | @Override 205 | public ContextMenu getContextMenu(ObservableList> selected_items) 206 | { 207 | ContextMenu menu = new ContextMenu(); 208 | menu.getItems().add(new MenuItem(MENU_ITEM_1)); 209 | menu.getItems().add(new MenuItem(MENU_ITEM_2)); 210 | menu.getItems().add(new SeparatorMenuItem()); 211 | menu.getItems().addAll(createEditMenuItems(selected_items, Cut, Copy, Paste, Delete)); 212 | return menu; 213 | } 214 | 215 | public String getDoubleClickedNodeName() 216 | { 217 | return _double_clicked_node_name; 218 | } 219 | 220 | private ExampleDataNode _root; 221 | 222 | private List _selected_nodes = Collections.emptyList(); 223 | public List _cut_or_copied_nodes = Collections.emptyList(); 224 | private boolean _cut = false; 225 | private boolean _copy = false; 226 | 227 | public int _drag_count; 228 | public ObservableList> _dragged_items; 229 | public List _dropped_nodes; 230 | public Object _dropped_content; 231 | 232 | private String _double_clicked_node_name; 233 | 234 | 235 | private final static DataFormat LIST_OF_NODES = new DataFormat("application/x-ListOfExampleDataNodes"); 236 | 237 | public final static String MENU_ITEM_1 = "click me"; 238 | private final static String MENU_ITEM_2 = "click me 2"; 239 | } 240 | 241 | 242 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/example/ExampleTreeNodeFacade.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree.example; 2 | 3 | import javafx.scene.Node; 4 | import net.christophermerrill.FancyFxTree.*; 5 | 6 | import java.util.*; 7 | import java.util.stream.*; 8 | 9 | /** 10 | * The implementation of FancyTreeNodeFacade is intended to wrap your existing 11 | * hierarchical data model to provide the API that FancyTreeView needs to model 12 | * and render your data in the tree. 13 | * 14 | * @author Christopher L Merrill (see LICENSE.txt for license details) 15 | */ 16 | public class ExampleTreeNodeFacade implements FancyTreeNodeFacade 17 | { 18 | public ExampleTreeNodeFacade(ExampleDataNode model) 19 | { 20 | _model = model; 21 | _model.addChangeListener(_listener); 22 | _children.addAll(_model.getChildren().stream().map(ExampleTreeNodeFacade::new).collect(Collectors.toList())); 23 | } 24 | 25 | private ExampleTreeNodeFacade(ExampleDataNode model, FancyTreeItemFacade facade, List> children) 26 | { 27 | _model = model; 28 | _item_facade = facade; 29 | _children = children; 30 | } 31 | 32 | @Override 33 | public List> getChildren() 34 | { 35 | return _children; 36 | } 37 | 38 | @Override 39 | public String getLabelText() 40 | { 41 | if (_model.getExtraData() == null) 42 | return _model.getName(); 43 | else 44 | return String.format("%s (%s)", _model.getName(), _model.getExtraData()); 45 | } 46 | 47 | @Override 48 | public Node getIcon() 49 | { 50 | return null; 51 | } 52 | 53 | @Override 54 | public Node getCustomCellUI() 55 | { 56 | return null; 57 | } 58 | 59 | @Override 60 | public FancyTreeCellEditor getCustomEditorUI() 61 | { 62 | if (_model._use_custom_editor) 63 | return new ExampleCustomCellEditor(); 64 | return null; 65 | } 66 | 67 | @Override 68 | public void editStarting() 69 | { 70 | _is_editing = true; 71 | } 72 | 73 | @Override 74 | public void editFinished() 75 | { 76 | _is_editing = false; 77 | } 78 | 79 | @Override 80 | public ExampleDataNode getModelNode() 81 | { 82 | return _model; 83 | } 84 | 85 | @Override 86 | public void setLabelText(String new_value) 87 | { 88 | _model.setName(new_value); 89 | } 90 | 91 | @Override 92 | public void setTreeItemFacade(FancyTreeItemFacade item_facade) 93 | { 94 | _item_facade = item_facade; 95 | } 96 | 97 | @Override 98 | public FancyTreeNodeFacade copyAndDestroy() 99 | { 100 | synchronized (_model) 101 | { 102 | ExampleTreeNodeFacade copy = new ExampleTreeNodeFacade(_model, _item_facade, _children); 103 | _model.replaceChangeListener(_listener, copy._listener); 104 | return copy; 105 | } 106 | } 107 | 108 | @Override 109 | public void destroy() 110 | { 111 | _model.removeChangeListener(_listener); 112 | _item_facade = null; 113 | } 114 | 115 | @Override 116 | public List getStyles() 117 | { 118 | return _model.getStyles(); 119 | } 120 | 121 | private ExampleDataNode.ChangeListener _listener = new ExampleDataNode.ChangeListener() 122 | { 123 | @Override 124 | public void childAdded(ExampleDataNode child, int index) 125 | { 126 | if (_item_facade == null) 127 | System.out.println("why don't I have a facade?"); 128 | ExampleTreeNodeFacade child_facade = new ExampleTreeNodeFacade(child); 129 | _children.add(index, child_facade); 130 | _item_facade.addChild(child_facade, index); 131 | } 132 | 133 | @Override 134 | public void childRemoved(ExampleDataNode child, int index) 135 | { 136 | _item_facade.removeChild(index, new ExampleTreeNodeFacade(child)); 137 | _children.remove(index); 138 | } 139 | 140 | @Override 141 | public void propertyChanged() 142 | { 143 | if (_item_facade != null && !_is_editing) 144 | _item_facade.refreshDisplay(); 145 | } 146 | }; 147 | 148 | @Override 149 | public String toString() 150 | { 151 | return _model.getName(); 152 | } 153 | 154 | private ExampleDataNode _model; 155 | private FancyTreeItemFacade _item_facade; 156 | private List> _children = new ArrayList<>(); 157 | private boolean _is_editing = false; 158 | } 159 | 160 | 161 | -------------------------------------------------------------------------------- /src/main/java/net/christophermerrill/FancyFxTree/example/FancyTreeExample.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree.example; 2 | 3 | import javafx.application.*; 4 | import javafx.collections.*; 5 | import javafx.scene.*; 6 | import javafx.scene.control.*; 7 | import javafx.scene.layout.*; 8 | import javafx.stage.*; 9 | import net.christophermerrill.FancyFxTree.*; 10 | 11 | import java.net.*; 12 | 13 | /** 14 | * @author Christopher L Merrill (see LICENSE.txt for license details) 15 | */ 16 | public class FancyTreeExample extends Application 17 | { 18 | public static void main(String[] args) 19 | { 20 | launch(args); 21 | } 22 | 23 | @Override 24 | public void start(Stage stage) throws Exception 25 | { 26 | stage.setTitle(getClass().getSimpleName()); 27 | BorderPane root = new BorderPane(); 28 | 29 | FlowPane button_bar = new FlowPane(); 30 | button_bar.getChildren().add(createNodeChangeButton()); 31 | _add_node_button = createAddNodeButton(); 32 | button_bar.getChildren().add(_add_node_button); 33 | button_bar.getChildren().add(createAddLeafNodeButton()); 34 | button_bar.getChildren().add(createStyleNodeButton()); 35 | button_bar.getChildren().add(createExpandAllButton()); 36 | button_bar.getChildren().add(createCollapseAllButton()); 37 | root.setTop(button_bar); 38 | 39 | _model_root = ExampleDataNodeBuilder.create(new int[] {4,1,3,2}); 40 | _tree = new FancyTreeView(new ExampleOperationHandler(_model_root) 41 | { 42 | @Override 43 | public void selectionChanged(ObservableList> selected_items) 44 | { 45 | super.selectionChanged(selected_items); 46 | _status.setText(String.format("%d items selected", selected_items.size())); 47 | _add_node_button.setDisable(selected_items.size() != 1); 48 | } 49 | }); 50 | _tree.setRoot(new ExampleTreeNodeFacade(_model_root)); 51 | _tree.expandAll(); 52 | _tree.setEditable(true); 53 | root.setCenter(_tree); 54 | 55 | _status = new Label(); 56 | root.setBottom(_status); 57 | 58 | URL resource = getClass().getResource("FancyTreeExample.css"); 59 | _tree.getStylesheets().add(resource.toExternalForm()); 60 | 61 | stage.setScene(new Scene(root, 300, 250)); 62 | stage.show(); 63 | } 64 | 65 | private Button createNodeChangeButton() 66 | { 67 | Button button = new Button(); 68 | button.setText("Change a node"); 69 | button.setOnAction(event -> 70 | { 71 | ExampleDataNode node = _model_root.pickRandom(); 72 | if (node.getExtraData() == null) 73 | node.setExtraData("modified"); 74 | else 75 | node.setExtraData(null); 76 | }); 77 | return button; 78 | } 79 | 80 | private Button createAddNodeButton() 81 | { 82 | Button button = new Button(); 83 | button.setText("Add a node"); 84 | button.setOnAction(event -> 85 | { 86 | MultipleSelectionModel> selectionModel = _tree.getSelectionModel(); 87 | ExampleDataNode node = selectionModel.getSelectedItem().getValue().getModelNode(); 88 | ExampleDataNode parent = _model_root.findParentFor(node); 89 | parent.addAfter(new ExampleDataNode("after " + node.getName()), node); 90 | }); 91 | button.setDisable(true); 92 | return button; 93 | } 94 | 95 | private Button createAddLeafNodeButton() 96 | { 97 | Button button = new Button(); 98 | button.setText("Add a leaf"); 99 | button.setOnAction(event -> 100 | { 101 | ExampleDataNode leaf = ExampleDataNodeBuilder.createRandomLeaf(_model_root); 102 | _tree.expandScrollToAndSelect(leaf); 103 | }); 104 | return button; 105 | } 106 | 107 | private Button createStyleNodeButton() 108 | { 109 | Button button = new Button(); 110 | button.setText("Style the node"); 111 | button.setOnAction(event -> 112 | Platform.runLater(() -> 113 | { 114 | final ObservableList items = _tree.getSelectionModel().getSelectedItems(); 115 | for (Object item : items) 116 | { 117 | ExampleTreeNodeFacade node = ((TreeItem) item).getValue(); 118 | ExampleDataNode data = node.getModelNode(); 119 | if (data.getStyles().isEmpty()) 120 | data.addStyle(STYLE1); 121 | else 122 | { 123 | String current_style = data.getStyles().get(0); 124 | switch (current_style) 125 | { 126 | case STYLE1: 127 | data.addStyle(STYLE2); 128 | break; 129 | case STYLE2: 130 | data.addStyle(STYLE3); 131 | break; 132 | } 133 | data.removeStyle(current_style); 134 | } 135 | } 136 | })); 137 | return button; 138 | } 139 | 140 | private Button createExpandAllButton() 141 | { 142 | Button button = new Button(); 143 | button.setText("Expand All"); 144 | button.setOnAction(event -> _tree.expandAll()); 145 | return button; 146 | } 147 | 148 | private Button createCollapseAllButton() 149 | { 150 | Button button = new Button(); 151 | button.setText("Collapse All"); 152 | button.setOnAction(event -> _tree.collapseAll()); 153 | return button; 154 | } 155 | 156 | private ExampleDataNode _model_root; 157 | private Button _add_node_button; 158 | private FancyTreeView _tree; 159 | private Label _status; 160 | 161 | private final static String STYLE1 = "customstyle1"; 162 | private final static String STYLE2 = "customstyle2"; 163 | private final static String STYLE3 = "customstyle3"; 164 | } -------------------------------------------------------------------------------- /src/main/resources/net/christophermerrill/FancyFxTree/example/FancyTreeExample.css: -------------------------------------------------------------------------------- 1 | .fancytreecell { 2 | -fx-padding: 1; 3 | } 4 | 5 | .fancytreecell:selected { 6 | -fx-text-fill: black; 7 | -fx-border-radius: 5; 8 | -fx-background-radius: 5; 9 | -fx-border-color: darkgray; 10 | -fx-background: #F0F8FF; 11 | -fx-padding: 0; 12 | } 13 | 14 | .customstyle1 { 15 | -fx-background-color: #a58282; 16 | } 17 | 18 | .customstyle2 { 19 | -fx-background-color: #a5936b; 20 | } 21 | 22 | .customstyle3 { 23 | -fx-background-color: #b0fcbe; 24 | } 25 | 26 | .fancytreecell-drop-on { 27 | -fx-background: linear-gradient(to top, rgba(240,240,255,0) 0%, rgba(205,205,220,1) 50%, rgba(205,205,220,1) 51%, rgba(240,240,255,0) 100%); 28 | } 29 | .fancytreecell-drop-before { 30 | -fx-background: linear-gradient(to bottom, rgba(205,205,220,1) 0%, rgba(240,240,255,0) 50%); 31 | } 32 | .fancytreecell-drop-after { 33 | -fx-background: linear-gradient(to top, rgba(205,205,220,1) 0%, rgba(240,240,255,0) 50%); 34 | } -------------------------------------------------------------------------------- /src/test/java/net/christophermerrill/FancyFxTree/FancyTreeTests.java: -------------------------------------------------------------------------------- 1 | package net.christophermerrill.FancyFxTree; 2 | 3 | import javafx.application.*; 4 | import javafx.collections.*; 5 | import javafx.geometry.*; 6 | import javafx.scene.*; 7 | import javafx.scene.control.*; 8 | import javafx.scene.input.*; 9 | import javafx.scene.layout.*; 10 | import net.christophermerrill.FancyFxTree.example.*; 11 | import net.christophermerrill.testfx.*; 12 | import org.junit.jupiter.api.*; 13 | import org.testfx.api.*; 14 | 15 | import java.util.*; 16 | 17 | import static javafx.scene.input.KeyCode.*; 18 | 19 | /** 20 | * @author Christopher L Merrill (see LICENSE.txt for license details) 21 | */ 22 | public class FancyTreeTests extends ComponentTest 23 | { 24 | @Test 25 | public void showNodes() 26 | { 27 | createBasicTreeAndData(); 28 | 29 | // look for the nodes 30 | checkNodesVisible122(); 31 | } 32 | 33 | @Test 34 | public void singleNodeSelected() 35 | { 36 | createBasicTreeAndData(); 37 | 38 | ExampleDataNode selected_node = _model.getNodeByName("1.1.1"); 39 | clickOn(selected_node.getName()); 40 | 41 | Assertions.assertEquals(1, _operations_handler.getSelectedNodes().size(), "wrong number of items was selected"); 42 | Assertions.assertSame(selected_node, _operations_handler.getSelectedNodes().get(0), "wrong item was selected"); 43 | } 44 | 45 | @Test 46 | public void multipleNodesSelected() 47 | { 48 | createBasicTreeAndData(); 49 | 50 | ExampleDataNode node1 = _model.getNodeByName("1.1.1"); 51 | clickOn(node1.getName()); 52 | 53 | ExampleDataNode node2 = _model.getNodeByName("1.2.1"); 54 | press(SHORTCUT); 55 | clickOn(node2.getName()); 56 | release(SHORTCUT); 57 | 58 | final List> paths = _tree.getSelectionPaths(); 59 | Assertions.assertEquals(2, paths.size()); 60 | 61 | final List selections = _operations_handler.getSelectedNodes(); 62 | Assertions.assertEquals(2, selections.size()); 63 | Assertions.assertEquals(node1, selections.get(0)); 64 | Assertions.assertEquals(node2, selections.get(1)); 65 | } 66 | 67 | @Test 68 | public void modelValueChanged() 69 | { 70 | createBasicTreeAndData(); 71 | 72 | // root node 73 | changeNodeAndVerifyDisplayChange(_model); 74 | 75 | // first child 76 | changeNodeAndVerifyDisplayChange(_model.getChildren().get(0)); 77 | 78 | // first grandchild 79 | changeNodeAndVerifyDisplayChange(_model.getChildren().get(0).getChildren().get(0)); 80 | } 81 | 82 | private void changeNodeAndVerifyDisplayChange(ExampleDataNode node) 83 | { 84 | // change the node so that the display value changes 85 | final String old_label = node.getName(); 86 | final String new_label = old_label + "-changed"; 87 | node.setName(new_label); 88 | waitForUiEvents(); 89 | 90 | Assertions.assertFalse(exists(old_label), "old label is still visible"); 91 | Assertions.assertTrue(exists(new_label), "new label is not visible"); 92 | } 93 | 94 | @Test 95 | public void addNode() 96 | { 97 | createBasicTreeAndData(); 98 | addNodeAndVerifyDisplayed(_model); 99 | addNodeAndVerifyDisplayed(_model.getChildren().get(0)); 100 | addNodeAndVerifyDisplayed(_model.getChildren().get(0).getChildren().get(0)); 101 | } 102 | 103 | @Test 104 | public void addNodeWithChildren() 105 | { 106 | createBasicTreeAndData(); 107 | ExampleDataNode new_parent = new ExampleDataNode("new_parent"); 108 | ExampleDataNode new_child = new ExampleDataNode("new_child"); 109 | new_parent.addChild(new_child); 110 | insertNodeAndVerifyDisplayed(_model, new_parent); 111 | waitForUiEvents(); 112 | 113 | _tree.expandToMakeVisible(new_child); 114 | waitForUiEvents(); 115 | 116 | Assertions.assertTrue(exists(new_child.getName()), "new child is not visible"); 117 | } 118 | 119 | private void addNodeAndVerifyDisplayed(ExampleDataNode node) 120 | { 121 | final String new_node_label = node.getName() + "-new" + node.getChildren().size(); 122 | ExampleDataNode new_node = new ExampleDataNode(new_node_label); 123 | node.addChild(new_node); 124 | waitForUiEvents(); 125 | 126 | Assertions.assertTrue(exists(new_node_label), "new node is not visible"); 127 | } 128 | 129 | private void insertNodeAndVerifyDisplayed(ExampleDataNode parent, ExampleDataNode new_node) 130 | { 131 | int index = parent.getChildren().size() > 0 ? 1 : 0; 132 | parent.insertChild(new_node, index); 133 | waitForUiEvents(); 134 | 135 | _tree.expandToMakeVisible(new_node); 136 | waitForUiEvents(); 137 | 138 | Assertions.assertTrue(exists(new_node.getName()), "new node is not visible"); 139 | } 140 | 141 | @Test 142 | public void removeFirstChildNode() 143 | { 144 | createBasicTreeAndData(); 145 | ExampleDataNode to_remove = _model.getChildren().get(0); 146 | removeNode(_model, to_remove); 147 | } 148 | 149 | @Test 150 | public void removeLastChildNode() 151 | { 152 | createBasicTreeAndData(); 153 | ExampleDataNode to_remove = _model.getChildren().get(_model.getChildren().size() - 1); 154 | removeNode(_model, to_remove); 155 | } 156 | 157 | @Test 158 | public void removeGrandchild() 159 | { 160 | createBasicTreeAndData(); 161 | ExampleDataNode to_remove = _model.getChildren().get(0).getChildren().get(0); 162 | removeNode(_model.getChildren().get(0), to_remove); 163 | } 164 | 165 | private void removeNode(ExampleDataNode parent, ExampleDataNode to_remove) 166 | { 167 | parent.removeChild(to_remove); 168 | waitForUiEvents(); 169 | Assertions.assertFalse(exists(to_remove.getName()), "removed node is still visible"); 170 | } 171 | 172 | @Test 173 | public void copyPasteByAlphaControlKeys() 174 | { 175 | tryCopyPaste(SHORTCUT, C, SHORTCUT, V); 176 | } 177 | 178 | @Test 179 | public void copyPasteBySpecialKeys() 180 | { 181 | tryCopyPaste(SHORTCUT, INSERT, SHIFT, INSERT); 182 | } 183 | 184 | private void tryCopyPaste(KeyCode copy_modifier, KeyCode copy_key, KeyCode paste_modifier, KeyCode paste_key) 185 | { 186 | createBasicTreeAndData(); 187 | 188 | ExampleDataNode original_node = _model.getNodeByName("1.1.1"); 189 | ExampleDataNode original_node_parent = _model.findParentFor(original_node); 190 | ExampleDataNode node_to_copy_after = _model.getNodeByName("1.2.2"); 191 | ExampleDataNode node_to_copy_into = _model.getNodeByName("1.2"); 192 | String copy_name = ExampleDataNode.getCopyName(original_node); 193 | 194 | clickOn(original_node.getName()); 195 | push(copy_modifier, copy_key); // copy 196 | clickOn(node_to_copy_after.getName()); 197 | push(paste_modifier, paste_key); // paste 198 | 199 | ExampleDataNode copied_node = _model.getNodeByName(copy_name); 200 | Assertions.assertNotNull(copied_node, "copy not found in tree"); 201 | Assertions.assertTrue(node_to_copy_into.contains(copied_node, true), "copy not in right parent"); 202 | Assertions.assertTrue(original_node_parent.contains(original_node, true), "original not found in parent"); 203 | } 204 | 205 | @Test 206 | public void cutPasteByAlphaControlKeys() 207 | { 208 | tryCutPaste(SHORTCUT, X, SHORTCUT, V); 209 | } 210 | 211 | @Test 212 | public void cutPasteBySpecialKeys() 213 | { 214 | // Test fails due to: https://github.com/TestFX/TestFX/issues/310 215 | // tryCutPaste(SHIFT, DELETE, CONTROL, INSERT); 216 | } 217 | 218 | private void tryCutPaste(KeyCode copy_modifier, KeyCode copy_key, KeyCode paste_modifier, KeyCode paste_key) 219 | { 220 | createBasicTreeAndData(); 221 | 222 | ExampleDataNode original_node = _model.getNodeByName("1.1.1"); 223 | ExampleDataNode original_node_parent = _model.findParentFor(original_node); 224 | ExampleDataNode node_to_paste_after = _model.getNodeByName("1.2.2"); 225 | ExampleDataNode node_to_paste_into = _model.getNodeByName("1.2"); 226 | 227 | clickOn(original_node.getName()); 228 | push(copy_modifier, copy_key); // copy 229 | clickOn(node_to_paste_after.getName()); 230 | push(paste_modifier, paste_key); // paste 231 | 232 | Assertions.assertNotNull(_model.getNodeByName(original_node.getName()), "cut node not found in tree"); 233 | Assertions.assertTrue(node_to_paste_into.contains(original_node, true), "not pasted into right parent"); 234 | Assertions.assertFalse(original_node_parent.contains(original_node, true), "original is still in parent"); 235 | } 236 | 237 | @Test 238 | public void deleteByDeleteKey() 239 | { 240 | createBasicTreeAndData(); 241 | 242 | ExampleDataNode node_to_delete = _model.getNodeByName("1.1.1"); 243 | 244 | clickOn(node_to_delete.getName()); 245 | push(DELETE); 246 | 247 | Assertions.assertFalse(_model.contains(node_to_delete), "node was not removed from model"); 248 | } 249 | 250 | @Test 251 | public void moveByDragInto() 252 | { 253 | createBasicTreeAndData(); 254 | 255 | ExampleDataNode target_parent = _model.getNodeByName("1.1"); 256 | ExampleDataNode target_node = _model.getNodeByName("1.1.1"); 257 | ExampleDataNode destination_node = _model.getNodeByName("1.2.2"); 258 | 259 | Assertions.assertEquals(2, target_parent.getChildren().size(), "expected root to start with 2 children"); 260 | Assertions.assertEquals(0, destination_node.getChildren().size(), "expected destination node to start with no children"); 261 | 262 | drag(target_node.getName(), MouseButton.PRIMARY).dropTo(destination_node.getName()); 263 | waitForUiEvents(); 264 | 265 | Assertions.assertNotNull(_model.getNodeByName(target_node.getName()), "node not found in tree"); 266 | Assertions.assertTrue(exists(target_node.getName()), "the target node is not displayed"); 267 | 268 | Assertions.assertEquals(1, target_parent.getChildren().size(), "node was not removed from root"); 269 | Assertions.assertEquals(1, destination_node.getChildren().size(), "node was not added to destination"); 270 | } 271 | 272 | @Test 273 | public void dragOntoSelfDenied() 274 | { 275 | createBasicTreeAndData(); 276 | 277 | ExampleDataNode target_node = _model.getNodeByName("1.1.1"); 278 | FxRobot robot = drag(target_node.getName(), MouseButton.PRIMARY); 279 | robot.moveTo("1.2.2"); 280 | robot.dropTo(target_node.getName()); 281 | 282 | Assertions.assertNull(_operations_handler._dropped_content, "the drop should have been denied"); 283 | } 284 | 285 | @Test 286 | public void dragOntoChildDenied() 287 | { 288 | createBasicTreeAndData(); 289 | 290 | ExampleDataNode target_node = _model.getNodeByName("1"); 291 | FxRobot robot = drag(target_node.getName(), MouseButton.PRIMARY); 292 | robot.moveTo("1.2.1"); 293 | robot.dropTo("1.1"); 294 | 295 | Assertions.assertNull(_operations_handler._dropped_content, "the drop should have been denied"); 296 | } 297 | 298 | @Test 299 | public void dragOntoDescendentDenied() 300 | { 301 | createBasicTreeAndData(); 302 | 303 | ExampleDataNode target_node = _model.getNodeByName("1"); 304 | FxRobot robot = drag(target_node.getName(), MouseButton.PRIMARY); 305 | robot.moveTo("1.2.1"); 306 | robot.dropTo("1.1.1"); 307 | 308 | Assertions.assertNull(_operations_handler._dropped_content, "the drop should have been denied"); 309 | } 310 | 311 | @Test 312 | public void copyByDragInto() 313 | { 314 | createBasicTreeAndData(); 315 | 316 | ExampleDataNode target_parent = _model.getNodeByName("1.1"); 317 | ExampleDataNode target_node = _model.getNodeByName("1.1.1"); 318 | ExampleDataNode destination_node = _model.getNodeByName("1.2.2"); 319 | 320 | Assertions.assertEquals(2, target_parent.getChildren().size(), "expected root to start with 2 children"); 321 | Assertions.assertEquals(0, destination_node.getChildren().size(), "expected destination node to start with no children"); 322 | 323 | drag(target_node.getName(), MouseButton.PRIMARY); 324 | press(SHORTCUT); 325 | dropTo(destination_node.getName()); 326 | release(SHORTCUT); 327 | waitForUiEvents(); 328 | 329 | Assertions.assertNotNull(_model.getNodeByName(ExampleDataNode.getCopyName(target_node)), "copy not found"); 330 | 331 | Assertions.assertNotEquals(1, target_parent.getChildren().size(), "node was removed from root"); 332 | Assertions.assertEquals(1, destination_node.getChildren().size(), "node was not added to destination"); 333 | } 334 | 335 | /** 336 | * This test passes in isolation but fails when run after certain tests. 337 | * Diagnosis reveals that the drag handler (FancyTreeCell) never gets called. 338 | * Unable to determine cause. 339 | */ 340 | @Test 341 | public void moveMultipleByDragInto() 342 | { 343 | createBasicTreeAndData(); 344 | 345 | ExampleDataNode target1 = _model.getNodeByName("1.1.1"); 346 | ExampleDataNode target2 = _model.getNodeByName("1.1.2"); 347 | ExampleDataNode destination = _model.getNodeByName("1.2.1"); 348 | 349 | clickOn(target1.getName()); 350 | press(SHIFT).clickOn(target2.getName()).release(SHIFT); 351 | drag(target2.getName(), MouseButton.PRIMARY); 352 | dropTo(destination.getName()); 353 | waitForUiEvents(); 354 | 355 | Assertions.assertEquals(2, _operations_handler._drag_count, "2 items should be dragged"); 356 | Assertions.assertTrue(dropListContains(_operations_handler._dropped_nodes, target1), "first item was not dropped"); 357 | Assertions.assertTrue(dropListContains(_operations_handler._dropped_nodes, target2), "second item was not dropped"); 358 | 359 | Assertions.assertTrue(destination.contains(target1)); 360 | Assertions.assertTrue(destination.contains(target2)); 361 | } 362 | 363 | /** 364 | * This test passes in isolation but fails when run after certain tests. 365 | * Diagnosis reveals that the drag handler (FancyTreeCell) never gets called. 366 | * Unable to determine cause. 367 | */ 368 | @Test 369 | public void copyMultipleByDragInto() 370 | { 371 | createBasicTreeAndData(); 372 | 373 | ExampleDataNode target_parent = _model.getNodeByName("1.1"); 374 | ExampleDataNode target_node_1 = _model.getNodeByName("1.1.1"); 375 | ExampleDataNode target_node_2 = _model.getNodeByName("1.1.2"); 376 | ExampleDataNode destination = _model.getNodeByName("1.2.2"); 377 | 378 | clickOn(target_node_1.getName()); 379 | press(SHIFT).clickOn(target_node_2.getName()).release(SHIFT); 380 | press(SHORTCUT); 381 | drag(target_node_2.getName(), MouseButton.PRIMARY); 382 | dropTo(destination.getName()); 383 | release(SHORTCUT); 384 | waitForUiEvents(); 385 | 386 | Assertions.assertEquals(2, _operations_handler._drag_count, "2 items should be dragged"); 387 | Assertions.assertTrue(dragListContains(_operations_handler._dragged_items, _model.getNodeByName("1.1.1")), "first item was not dragged"); 388 | Assertions.assertTrue(dragListContains(_operations_handler._dragged_items, _model.getNodeByName("1.1.2")), "second item was not dragged"); 389 | 390 | Assertions.assertTrue(target_parent.contains(target_node_1), "orignal #1 was removed"); 391 | Assertions.assertTrue(target_parent.contains(target_node_2), "orignal #2 was removed"); 392 | 393 | ExampleDataNode copy_1 = _model.getNodeByName(ExampleDataNode.getCopyName(target_node_1)); 394 | Assertions.assertNotNull(copy_1, "1st node was not copied"); 395 | ExampleDataNode copy_2 = _model.getNodeByName(ExampleDataNode.getCopyName(target_node_2)); 396 | Assertions.assertNotNull(copy_2, "2nd node was not copied"); 397 | Assertions.assertTrue(destination.contains(copy_1), "1st copy is not in destination"); 398 | Assertions.assertTrue(destination.contains(copy_2), "2nd copy is not in destination"); 399 | } 400 | 401 | private boolean dragListContains(ObservableList> dragged_items, ExampleDataNode node) 402 | { 403 | for (TreeItem item : dragged_items) 404 | if (item.getValue().getModelNode() == node) 405 | return true; 406 | return false; 407 | } 408 | 409 | private boolean dropListContains(List dropped_nodes, ExampleDataNode node) 410 | { 411 | for (ExampleDataNode dropped_node : dropped_nodes) 412 | if (dropped_node.equals(node)) 413 | return true; 414 | return false; 415 | } 416 | 417 | @Test 418 | public void disableDragAndDrop() 419 | { 420 | _enable_dnd = false; 421 | createBasicTreeAndData(); 422 | 423 | ExampleDataNode target = _model.getNodeByName("1.1"); 424 | ExampleDataNode destination = _model.getNodeByName("1.2"); 425 | drag(target.getName(), MouseButton.PRIMARY); 426 | dropTo(destination.getName()); 427 | 428 | Assertions.assertNull(_operations_handler._dragged_items, "something was dragged"); 429 | } 430 | 431 | @Test 432 | public void dragAndDropIntoDenied() 433 | { 434 | createBasicTreeAndData(); 435 | 436 | drag("1.1", MouseButton.PRIMARY); 437 | dropTo("1.2.2"); 438 | 439 | Assertions.assertNull(_operations_handler._dropped_content, "the drop should have been denied"); 440 | } 441 | 442 | @Test 443 | public void moveByDragBefore() 444 | { 445 | createBasicTreeAndData(); 446 | 447 | ExampleDataNode target_node = _model.getNodeByName("1.1.1"); 448 | ExampleDataNode target_parent = _model.getNodeByName("1.1"); 449 | ExampleDataNode destination_node = _model.getNodeByName("1.2.2"); 450 | ExampleDataNode destination_parent = _model.getNodeByName("1.2"); 451 | 452 | Node destination_area = lookup(destination_node.getName()).query(); 453 | drag(target_node.getName(), MouseButton.PRIMARY); 454 | moveTo(destination_area); 455 | moveBy(0, -destination_area.getBoundsInParent().getHeight() * 0.4d); 456 | drop(); 457 | 458 | Assertions.assertFalse(target_parent.contains(target_node), "The target node was not removed from its parent"); 459 | Assertions.assertTrue(destination_parent.contains(target_node), "The target node was not moved into the destination"); 460 | Assertions.assertEquals(destination_parent.getChildren().get(1), target_node, "The target node is not in the right place in the destination"); 461 | Assertions.assertTrue(exists(target_node.getName()), "the target node is not displayed"); 462 | } 463 | 464 | @Test 465 | public void moveByDragAfter() 466 | { 467 | createBasicTreeAndData(); 468 | 469 | ExampleDataNode target_node = _model.getNodeByName("1.1.1"); 470 | ExampleDataNode target_parent = _model.getNodeByName("1.1"); 471 | ExampleDataNode destination_node = _model.getNodeByName("1.2.1"); 472 | ExampleDataNode destination_parent = _model.getNodeByName("1.2"); 473 | 474 | Node destination_area = lookup(destination_node.getName()).query(); 475 | drag(target_node.getName(), MouseButton.PRIMARY); 476 | moveTo(destination_area); 477 | moveBy(0, destination_area.getBoundsInParent().getHeight() * 0.4d); 478 | drop(); 479 | 480 | Assertions.assertFalse(target_parent.contains(target_node), "The target node was not removed from its parent"); 481 | Assertions.assertTrue(destination_parent.contains(target_node), "The target node was not moved into the destination"); 482 | Assertions.assertEquals(destination_parent.getChildren().get(1), target_node, "The target node is not in the right place in the destination"); 483 | Assertions.assertTrue(exists(target_node.getName()), "the target node is not displayed"); 484 | } 485 | 486 | @Test 487 | public void expandOnHover() 488 | { 489 | _hover_duration = 50; 490 | createBasicTreeAndData(); 491 | 492 | TreeItem item = _tree.getTreeItem(1); 493 | item.setExpanded(false); 494 | waitForUiEvents(); 495 | 496 | Node collapsed = lookup("1.1").query(); 497 | Assertions.assertFalse(exists("1.1.1")); // make sure it is hidden 498 | 499 | drag("1.2.2"); 500 | moveTo(collapsed); 501 | sleep(75); 502 | waitForUiEvents(); 503 | release(MouseButton.PRIMARY); 504 | 505 | Assertions.assertTrue(exists("1.1.1")); 506 | } 507 | 508 | @Test 509 | public void expandToNode() 510 | { 511 | // This capability is tested indirectly by the node addition tests, since they must make the node visible 512 | // in order to check that it displayed in the tree. This no-op test remains as documententation of such. 513 | } 514 | 515 | @Test 516 | public void scrollToNode() 517 | { 518 | ExampleDataNode root = ExampleDataNodeBuilder.create(new int[]{2, 2, 3, 2, 1}); 519 | setupTree(root); 520 | 521 | ExampleDataNode last_node = root.getChildren().get(1) 522 | .getChildren().get(1) 523 | .getChildren().get(1) 524 | .getChildren().get(1) 525 | .getChildren().get(0); 526 | 527 | _tree.scrollToAndMakeVisible(last_node); 528 | waitForUiEvents(); 529 | 530 | // there is currently no way to positively determine if the item is visible on-screen. 531 | // https://groups.google.com/forum/#!topic/testfx-discuss/R0WM_TaloDI 532 | // 533 | // This test is left here for manual use and documentation of the issue. 534 | // To test manually, set a breakpoint on the next line and visually verify 535 | // that the view is scrolled to the bottom of the tree (1.2.2.2.2.2) 536 | System.out.println("is node 1.2.2.2.2.2 visible?"); 537 | } 538 | 539 | @Test 540 | public void expandScrollToAndSelectNewItem() 541 | { 542 | ExampleDataNode root = ExampleDataNodeBuilder.create(new int[]{3, 3, 3, 3, 3}); 543 | setupTree(root, false); 544 | waitForUiEvents(); 545 | 546 | ExampleDataNode leaf = ExampleDataNodeBuilder.createRandomLeaf(root); 547 | waitForUiEvents(); 548 | 549 | // won't exist in tree if it wasn't shown initially 550 | Assertions.assertFalse(exists(leaf.getName())); 551 | 552 | _tree.expandScrollToAndSelect(leaf); 553 | waitForUiEvents(); 554 | 555 | // technically, checking for exists does not verify it is visible, but at least 556 | // we know it was added to the node graph, which implies it was expanded and visible 557 | Assertions.assertTrue(exists(leaf.getName())); 558 | } 559 | 560 | @Test 561 | public void expandScrollToAndMakeVisible() 562 | { 563 | ExampleDataNode root = ExampleDataNodeBuilder.create(new int[]{3, 3, 3, 3, 3}); 564 | setupTree(root, false); 565 | 566 | final ExampleDataNode node = _model.getNodeByName("1.3.3.3.1"); 567 | Assertions.assertFalse(exists(node.getName())); // not in tree yet because it hasn't been shown 568 | 569 | List> expanded = _tree.expandAndScrollTo(node); 570 | waitForUiEvents(); 571 | Assertions.assertTrue(exists(node.getName())); 572 | // check the expanded nodes were returned 573 | Assertions.assertEquals("1.3.3.3", expanded.get(0).getValue().getLabelText()); 574 | Assertions.assertEquals("1.3.3", expanded.get(1).getValue().getLabelText()); 575 | Assertions.assertEquals("1.3", expanded.get(2).getValue().getLabelText()); 576 | Assertions.assertEquals("1", expanded.get(3).getValue().getLabelText()); 577 | 578 | final ExampleDataNode still_hidden_node = _model.getNodeByName("1.3.3.3.1.2"); 579 | Assertions.assertFalse(exists(still_hidden_node.getName()), "expanded 1 level too far"); 580 | } 581 | 582 | @Test 583 | public void defaultStyleApplied() 584 | { 585 | createBasicTreeAndData(); 586 | Node node = lookup("1.1").query(); 587 | Assertions.assertTrue(node.getStyleClass().contains(FancyTreeCell.CELL_STYLE_NAME), "style is missing from TreeCell"); 588 | } 589 | 590 | @Test 591 | public void dropOntoStyleApplied() 592 | { 593 | createBasicTreeAndData(); 594 | 595 | ExampleDataNode destination_node = _model.getNodeByName("1.2.2"); 596 | drag("1.1.1", MouseButton.PRIMARY); 597 | moveTo(destination_node.getName()); 598 | 599 | Node node = lookup(destination_node.getName()).query(); 600 | Assertions.assertTrue(node.getStyleClass().contains(FancyTreeCell.DROP_ON_STYLE_NAME), "drop-into style is missing from cell"); 601 | 602 | moveTo("1.1.1"); 603 | Assertions.assertFalse(node.getStyleClass().contains(FancyTreeCell.DROP_ON_STYLE_NAME), "drop-into style was not removed from cell"); 604 | 605 | // leave the mouse in a normal state by dropping the drag that we started. If this doesn't happen, it can affect the next test 606 | drop(); 607 | clickOn("1.1"); 608 | } 609 | 610 | @Test 611 | public void dropBeforeStyleApplied() 612 | { 613 | createBasicTreeAndData(); 614 | 615 | ExampleDataNode destination_node = _model.getNodeByName("1.2.2"); 616 | Node destination_area = lookup(destination_node.getName()).query(); 617 | drag("1.1.1", MouseButton.PRIMARY); 618 | moveTo(destination_node.getName()); 619 | moveBy(0, -destination_area.getBoundsInParent().getHeight() * 0.4d); 620 | 621 | Node node = lookup(destination_node.getName()).query(); 622 | Assertions.assertTrue(node.getStyleClass().contains(FancyTreeCell.DROP_BEFORE_STYLE_NAME), "drop-before style is missing from cell"); 623 | 624 | moveTo("1.1.1"); 625 | Assertions.assertFalse(node.getStyleClass().contains(FancyTreeCell.DROP_BEFORE_STYLE_NAME), "drop-before style was not removed from cell"); 626 | 627 | // leave the mouse in a normal state by dropping the drag that we started. If this doesn't happen, it can affect the next test 628 | drop(); 629 | clickOn("1.1"); 630 | } 631 | 632 | @Test 633 | public void dropAfterStyleApplied() 634 | { 635 | createBasicTreeAndData(); 636 | 637 | ExampleDataNode destination_node = _model.getNodeByName("1.2.2"); 638 | Node destination_area = lookup(destination_node.getName()).query(); 639 | drag("1.1.1", MouseButton.PRIMARY); 640 | moveTo(destination_node.getName()); 641 | moveBy(0, destination_area.getBoundsInParent().getHeight() * 0.4d); 642 | 643 | Node node = lookup(destination_node.getName()).query(); 644 | Assertions.assertTrue(node.getStyleClass().contains(FancyTreeCell.DROP_AFTER_STYLE_NAME), "drop-after style is missing from cell"); 645 | 646 | moveTo("1.1.1"); 647 | Assertions.assertFalse(node.getStyleClass().contains(FancyTreeCell.DROP_AFTER_STYLE_NAME), "drop-after style was not removed from cell"); 648 | 649 | // leave the mouse in a normal state by dropping the drag that we started. If this doesn't happen, it can affect the next test 650 | drop(); 651 | clickOn("1.1"); 652 | } 653 | 654 | @Test 655 | public void doubleClick() 656 | { 657 | createBasicTreeAndData(); 658 | ExampleDataNode target_node = _model.getNodeByName("1.2.1"); 659 | doubleClickOn(target_node.getName()); 660 | 661 | Assertions.assertEquals("double-click detected on wrong node", target_node.getName(), _operations_handler.getDoubleClickedNodeName()); 662 | } 663 | 664 | @Test 665 | public void showContextMenu() 666 | { 667 | createBasicTreeAndData(); 668 | ExampleDataNode target_node = _model.getNodeByName("1.1"); 669 | clickOn(target_node.getName(), MouseButton.SECONDARY); 670 | 671 | Assertions.assertTrue(exists(ExampleOperationHandler.MENU_ITEM_1), "context menu not visible"); 672 | } 673 | 674 | @Test 675 | public void cutByContextMenu() 676 | { 677 | createBasicTreeAndData(); 678 | ExampleDataNode target_node = _model.getNodeByName("1.1"); 679 | Assertions.assertNotNull(target_node); 680 | clickOn(target_node.getName(), MouseButton.SECONDARY); 681 | 682 | Assertions.assertTrue(exists(id(FancyTreeOperationHandler.EditType.Cut.getMenuId()))); 683 | clickOn(id(FancyTreeOperationHandler.EditType.Cut.getMenuId())); 684 | 685 | Assertions.assertEquals(1, _operations_handler._cut_or_copied_nodes.size()); 686 | Assertions.assertEquals(_model.getNodeByName("1.1"), _operations_handler._cut_or_copied_nodes.get(0)); 687 | } 688 | 689 | @Test 690 | public void copyAndPasteByContextMenu() 691 | { 692 | createBasicTreeAndData(); 693 | ExampleDataNode target_node = _model.getNodeByName("1.2.1"); 694 | Assertions.assertNotNull(target_node); 695 | clickOn(target_node.getName(), MouseButton.SECONDARY); 696 | 697 | Assertions.assertTrue(exists(id(FancyTreeOperationHandler.EditType.Copy.getMenuId()))); 698 | clickOn(id(FancyTreeOperationHandler.EditType.Copy.getMenuId())); 699 | 700 | Assertions.assertEquals(1, _operations_handler._cut_or_copied_nodes.size()); 701 | Assertions.assertEquals(_model.getNodeByName("1.2.1"), _operations_handler._cut_or_copied_nodes.get(0)); 702 | 703 | String copy_name = ExampleDataNode.getCopyName(target_node); 704 | Assertions.assertFalse(exists(copy_name)); 705 | 706 | clickOn(target_node.getName(), MouseButton.SECONDARY); 707 | clickOn(id(FancyTreeOperationHandler.EditType.Paste.getMenuId())); 708 | Assertions.assertTrue(exists(copy_name)); 709 | } 710 | 711 | @Test 712 | public void displayDefaultTextEditorOnDoubleClick() 713 | { 714 | createBasicTreeAndData(); 715 | ExampleDataNode target_node = _model.getNodeByName("1.2.1"); 716 | 717 | Assertions.assertFalse(exists("." + TextCellEditor.NODE_STYLE)); 718 | doubleClickOn(target_node.getName()); 719 | Assertions.assertTrue(exists("." + TextCellEditor.NODE_STYLE)); 720 | } 721 | 722 | @Test 723 | public void notEditable() 724 | { 725 | createBasicTreeAndData(); 726 | _tree.setEditable(false); 727 | ExampleDataNode target_node = _model.getNodeByName("1.2.1"); 728 | doubleClickOn(target_node.getName()); 729 | Assertions.assertFalse(exists("." + TextCellEditor.NODE_STYLE)); 730 | } 731 | 732 | @Test 733 | public void editTextCompletedByTab() 734 | { 735 | testTextEditCompletion(KeyCode.TAB, true); 736 | } 737 | 738 | @Test 739 | public void cancelTextEdit() 740 | { 741 | testTextEditCompletion(KeyCode.ESCAPE, false); 742 | } 743 | 744 | @Test 745 | public void editTextCompletedByEnter() 746 | { 747 | testTextEditCompletion(KeyCode.ENTER, true); 748 | } 749 | 750 | @Test 751 | public void editTwice() 752 | { 753 | createBasicTreeAndData(); 754 | ExampleDataNode target_node = _model.getNodeByName("1.2.1"); 755 | doubleClickOn(target_node.getName()); 756 | clickOn(withStyle(TextCellEditor.NODE_STYLE)).push(SHORTCUT, KeyCode.A).write("name1").push(KeyCode.ENTER); 757 | 758 | waitForUiEvents(); 759 | Assertions.assertFalse(exists("1.2.1")); 760 | Assertions.assertTrue(exists("name1")); 761 | Assertions.assertEquals("name1", target_node.getName()); 762 | 763 | doubleClickOn(target_node.getName()); 764 | clickOn(withStyle(TextCellEditor.NODE_STYLE)).push(SHORTCUT, KeyCode.A).write("name2").push(KeyCode.ENTER); 765 | waitForUiEvents(); 766 | Assertions.assertFalse(exists("1.2.1")); 767 | Assertions.assertFalse(exists("name1")); 768 | Assertions.assertTrue(exists("name2")); 769 | Assertions.assertEquals("name2", target_node.getName()); 770 | } 771 | 772 | @Test 773 | public void editThenCancelEdit() 774 | { 775 | createBasicTreeAndData(); 776 | ExampleDataNode target_node = _model.getNodeByName("1.2.1"); 777 | doubleClickOn(target_node.getName()); 778 | clickOn(withStyle(TextCellEditor.NODE_STYLE)).push(SHORTCUT, KeyCode.A).write("name1").push(KeyCode.ENTER); 779 | 780 | waitForUiEvents(); 781 | Assertions.assertFalse(exists("1.2.1")); 782 | Assertions.assertTrue(exists("name1")); 783 | Assertions.assertEquals("name1", target_node.getName()); 784 | 785 | doubleClickOn(target_node.getName()); 786 | clickOn(withStyle(TextCellEditor.NODE_STYLE)).push(SHORTCUT, KeyCode.A).write("name2").push(KeyCode.ESCAPE); 787 | waitForUiEvents(); 788 | Assertions.assertFalse(exists("1.2.1")); 789 | Assertions.assertFalse(exists("name2")); 790 | Assertions.assertTrue(exists("name1")); 791 | Assertions.assertEquals("name1", target_node.getName()); 792 | } 793 | 794 | @Test 795 | public void editNodeWithChildren() // ensure it doesn't expand the node instead of editing 796 | { 797 | createBasicTreeAndData(); 798 | ExampleDataNode target_node = _model.getNodeByName("1.2"); 799 | 800 | Assertions.assertFalse(exists("." + TextCellEditor.NODE_STYLE)); 801 | doubleClickOn(target_node.getName()); 802 | Assertions.assertTrue(exists("." + TextCellEditor.NODE_STYLE)); 803 | } 804 | 805 | private void testTextEditCompletion(KeyCode final_keystroke, boolean changed) 806 | { 807 | createBasicTreeAndData(); 808 | ExampleDataNode target_node = _model.getNodeByName("1.2.1"); 809 | doubleClickOn(target_node.getName()); 810 | 811 | // don't understand why fillFieldAndTabAway() doesn't work here :( 812 | clickOn(withStyle(TextCellEditor.NODE_STYLE)).push(SHORTCUT, KeyCode.A).write("newname").push(final_keystroke); 813 | waitForUiEvents(); 814 | 815 | Assertions.assertEquals(!changed, exists("1.2.1")); 816 | Assertions.assertEquals(changed, exists("newname")); 817 | Assertions.assertEquals(changed ? "newname" : "1.2.1", target_node.getName()); 818 | } 819 | 820 | @Test 821 | public void showCustomEditor() 822 | { 823 | createBasicTreeAndData(); 824 | 825 | ExampleDataNode target_node = _model.getNodeByName("1.1.2"); 826 | target_node._use_custom_editor = true; 827 | Assertions.assertFalse(exists("." + ExampleCustomCellEditor.NODE_STYLE)); 828 | doubleClickOn(target_node.getName()); 829 | Assertions.assertTrue(exists("." + ExampleCustomCellEditor.NODE_STYLE)); 830 | } 831 | 832 | @Test 833 | public void deleteByContextMenu() 834 | { 835 | createBasicTreeAndData(); 836 | ExampleDataNode target_node = _model.getNodeByName("1.1"); 837 | Assertions.assertNotNull(target_node); 838 | clickOn(target_node.getName(), MouseButton.SECONDARY); 839 | 840 | Assertions.assertTrue(exists(id(FancyTreeOperationHandler.EditType.Delete.getMenuId()))); 841 | clickOn(id(FancyTreeOperationHandler.EditType.Delete.getMenuId())); 842 | 843 | Assertions.assertNull(_model.getNodeByName("1.1")); 844 | } 845 | 846 | @Test 847 | public void applyCellStyles() 848 | { 849 | createBasicTreeAndData(); 850 | final String node_to_style = "1.2.1"; 851 | final String style_name = "style1"; 852 | 853 | Node styled_node = lookup(node_to_style).query(); 854 | Assertions.assertFalse(styled_node.getStyleClass().contains(style_name)); 855 | final int num_default_styles = styled_node.getStyleClass().size(); 856 | Assertions.assertTrue(num_default_styles >= 4, "should be at least 4 styles to start with"); 857 | 858 | ExampleDataNode styled_data = _model.getNodeByName(node_to_style); 859 | styled_data.addStyle(style_name); 860 | waitForUiEvents(); 861 | styled_node = lookup(node_to_style).query(); 862 | Assertions.assertTrue(styled_node.getStyleClass().contains(style_name), "style was not added"); 863 | 864 | styled_data.removeStyle(style_name); 865 | waitForUiEvents(); 866 | Assertions.assertFalse(styled_node.getStyleClass().contains(style_name), "style was not removed"); 867 | Assertions.assertEquals(num_default_styles, styled_node.getStyleClass().size(), "all initial styles not present"); 868 | } 869 | 870 | /** 871 | * While editing, changes to the node should be ignored (rather than updating 872 | * the cell UI...which would close the editor). 873 | */ 874 | @Test 875 | public void delayUpdatesWhileEditing() 876 | { 877 | createBasicTreeAndData(); 878 | ExampleDataNode target_node = _model.getNodeByName("1.1.2"); 879 | doubleClickOn(target_node.getName()); 880 | lookup("." + TextCellEditor.NODE_STYLE).query(); 881 | 882 | target_node.setName("newnamefor1.1.2"); 883 | waitForUiEvents(); 884 | Assertions.assertFalse(exists("newnamefor1.1.2"), "new name shown in tree"); 885 | lookup("." + TextCellEditor.NODE_STYLE).query(); 886 | } 887 | 888 | @Test 889 | public void doubleClickCollapseExpandBug() 890 | { 891 | createBasicTreeAndData(); 892 | 893 | ExampleDataNode target_node = _model.getNodeByName("1.1"); 894 | doubleClickOn(target_node.getName()); 895 | 896 | final Node node = lookup(target_node.getName()).query(); 897 | final Bounds bounds = bounds(node).query(); 898 | final Point2D chevron = new Point2D(bounds.getMinX() - 10, bounds.getMinY() + (bounds.getMaxY() - bounds.getMinY())/2); 899 | 900 | // type(KeyCode.ESCAPE); // this fixes the bug 901 | // waitForUiEvents(); 902 | 903 | clickOn(chevron); 904 | waitForUiEvents(); 905 | 906 | final Node node_again = lookup(target_node.getName()).query(); 907 | Assertions.assertNotNull(node_again); // the node became invisible 908 | } 909 | 910 | private void createBasicTreeAndData() 911 | { 912 | ExampleDataNode root = ExampleDataNodeBuilder.create(new int[]{2, 2}); 913 | setupTree(root); 914 | } 915 | 916 | private void setupTree(ExampleDataNode root) 917 | { 918 | setupTree(root, true); 919 | } 920 | 921 | private void setupTree(ExampleDataNode root, boolean expand_all) 922 | { 923 | _model = root; 924 | ExampleTreeNodeFacade root_facade = new ExampleTreeNodeFacade(_model); 925 | TreeItem root_item = FancyTreeItemBuilder.create(root_facade); 926 | 927 | _operations_handler = new ExampleOperationHandler(_model); 928 | _tree = new FancyTreeView(_operations_handler, _enable_dnd); 929 | _tree.setHoverExpandDuration(_hover_duration); 930 | _tree.setRoot(root_item); 931 | _tree.setEditable(true); 932 | if (expand_all) 933 | _tree.expandAll(); 934 | 935 | Platform.runLater(() -> _pane.setCenter(_tree)); 936 | waitForUiEvents(); 937 | } 938 | 939 | private void checkNodesVisible122() 940 | { 941 | Assertions.assertTrue(exists("1"), "The root node (1) is not visible"); 942 | Assertions.assertTrue(exists("1.1"), "node 1.1 is not visible"); 943 | Assertions.assertTrue(exists("1.1.1"), "node 1.1.1 is not visible"); 944 | Assertions.assertTrue(exists("1.1.2"), "node 1.1.2 is not visible"); 945 | Assertions.assertTrue(exists("1.2"), "node 1.2 is not visible"); 946 | Assertions.assertTrue(exists("1.2.1"), "node 1.2.1 is not visible"); 947 | Assertions.assertTrue(exists("1.2.2"), "node 1.2.2 is not visible"); 948 | } 949 | 950 | @Override 951 | protected Node createComponentNode() 952 | { 953 | _pane = new BorderPane(); 954 | return _pane; 955 | } 956 | 957 | @Override 958 | protected double getDefaultHeight() 959 | { 960 | return super.getDefaultHeight() * 2; 961 | } 962 | 963 | private BorderPane _pane; 964 | private ExampleDataNode _model; 965 | private FancyTreeView _tree; 966 | private ExampleOperationHandler _operations_handler; 967 | private boolean _enable_dnd = true; 968 | private long _hover_duration = FancyTreeView.DEFAULT_HOVER_EXPAND_DURATION; 969 | } --------------------------------------------------------------------------------