Browse Source

Implemented a libGDX-AI Behavior Tree Editor, time to celebrate!

MeFisto94 6 years ago
parent
commit
5f4252ef44
51 changed files with 6881 additions and 0 deletions
  1. 1 0
      build.gradle
  2. 8 0
      jme3-behaviortrees/build.xml
  3. 7 0
      jme3-behaviortrees/manifest.mf
  4. 45 0
      jme3-behaviortrees/nbproject/build-impl.xml
  5. 8 0
      jme3-behaviortrees/nbproject/genfiles.properties
  6. 178 0
      jme3-behaviortrees/nbproject/platform.properties
  7. 8 0
      jme3-behaviortrees/nbproject/project.properties
  8. 250 0
      jme3-behaviortrees/nbproject/project.xml
  9. 1 0
      jme3-behaviortrees/nbproject/suite.properties
  10. 394 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeDataObject.java
  11. 268 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeExporter.java
  12. 82 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeExporterUtils.java
  13. 408 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeMetaData.java
  14. 820 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeParser.java
  15. 2 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BehaviorTree.btree
  16. 7 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/Bundle.properties
  17. 192 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/DistributionStringConverter.java
  18. 14 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/InputMappingBlock.java
  19. 14 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/OutputMappingBlock.java
  20. 193 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/dialog/AddNodeDialog.form
  21. 424 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/dialog/AddNodeDialog.java
  22. 17 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/dialog/Bundle.properties
  23. 62 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/BTreeNodeEditorElement.form
  24. 582 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/BTreeNodeEditorElement.java
  25. 21 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/Bundle.properties
  26. 52 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/InOut.java
  27. 97 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/TreeConnectionEndpoint.java
  28. 284 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/TreeDiagram.java
  29. 611 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/TreeNodePanel.java
  30. 14 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/layer.xml
  31. 37 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/BTreeNavigatorPanel.form
  32. 227 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/BTreeNavigatorPanel.java
  33. 136 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/nodes/AbstractTaskNode.java
  34. 85 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/nodes/TreeNodePanelNode.java
  35. 156 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/AttrInfoProperty.java
  36. 8 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/Bundle.properties
  37. 89 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/DistributionEditor.java
  38. 69 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/DistributionProperties.form
  39. 665 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/DistributionProperties.java
  40. 5 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/Bundle.properties
  41. 31 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/CustomBeanTreeView.java
  42. 85 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/DynamicOutputNodePanel.java
  43. 29 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/LeafTreeNodePanel.java
  44. 33 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/SequentialNodePanel.java
  45. 24 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/BuiltinLeafTask.java
  46. 21 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/DecoratorNodePanel.java
  47. 19 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/ParallelNodePanel.java
  48. 21 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/RootNodePanel.java
  49. 22 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/SelectorNodePanel.java
  50. 20 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/SequenceNodePanel.java
  51. 35 0
      jme3-behaviortrees/src/com/jme3/gde/behaviortrees/package-info.java

+ 1 - 0
build.gradle

@@ -60,6 +60,7 @@ configurations {
 dependencies {
 
     corelibs dep("com.github.xbuf:jme3_xbuf:0.9.1", false, false)
+    corelibs dep("com.badlogicgames.gdx:gdx-ai:1.8.1", true, true)
 
     corelibs dep("org.jmonkeyengine:jme3-core:$jmeEngineVersion", true, true)
     corelibs dep("org.jmonkeyengine:jme3-desktop:$jmeEngineVersion", true, true)

+ 8 - 0
jme3-behaviortrees/build.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- You may freely edit this file. See harness/README in the NetBeans platform -->
+<!-- for some information on what you could do (e.g. targets to override). -->
+<!-- If you delete this file and reopen the project it will be recreated. -->
+<project name="com.jme3.gde.behaviortrees" default="netbeans" basedir=".">
+    <description>Builds, tests, and runs the project com.jme3.gde.behaviortrees.</description>
+    <import file="nbproject/build-impl.xml"/>
+</project>

+ 7 - 0
jme3-behaviortrees/manifest.mf

@@ -0,0 +1,7 @@
+Manifest-Version: 1.0
+OpenIDE-Module: com.jme3.gde.behaviortrees/1
+OpenIDE-Module-Implementation-Version: 0
+OpenIDE-Module-Layer: com/jme3/gde/behaviortrees/layer.xml
+OpenIDE-Module-Localizing-Bundle: com/jme3/gde/behaviortrees/Bundle.properties
+OpenIDE-Module-Requires: org.openide.windows.WindowManager
+

+ 45 - 0
jme3-behaviortrees/nbproject/build-impl.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+*** GENERATED FROM project.xml - DO NOT EDIT  ***
+***         EDIT ../build.xml INSTEAD         ***
+-->
+<project name="com.jme3.gde.behaviortrees-impl" basedir="..">
+    <fail message="Please build using Ant 1.7.1 or higher.">
+        <condition>
+            <not>
+                <antversion atleast="1.7.1"/>
+            </not>
+        </condition>
+    </fail>
+    <property file="nbproject/private/suite-private.properties"/>
+    <property file="nbproject/suite.properties"/>
+    <fail unless="suite.dir">You must set 'suite.dir' to point to your containing module suite</fail>
+    <property file="${suite.dir}/nbproject/private/platform-private.properties"/>
+    <property file="${suite.dir}/nbproject/platform.properties"/>
+    <macrodef name="property" uri="http://www.netbeans.org/ns/nb-module-project/2">
+        <attribute name="name"/>
+        <attribute name="value"/>
+        <sequential>
+            <property name="@{name}" value="${@{value}}"/>
+        </sequential>
+    </macrodef>
+    <macrodef name="evalprops" uri="http://www.netbeans.org/ns/nb-module-project/2">
+        <attribute name="property"/>
+        <attribute name="value"/>
+        <sequential>
+            <property name="@{property}" value="@{value}"/>
+        </sequential>
+    </macrodef>
+    <property file="${user.properties.file}"/>
+    <nbmproject2:property name="harness.dir" value="nbplatform.${nbplatform.active}.harness.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <nbmproject2:property name="nbplatform.active.dir" value="nbplatform.${nbplatform.active}.netbeans.dest.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <nbmproject2:evalprops property="cluster.path.evaluated" value="${cluster.path}" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <fail message="Path to 'platform' cluster missing in $${cluster.path} property or using corrupt Netbeans Platform (missing harness).">
+        <condition>
+            <not>
+                <contains string="${cluster.path.evaluated}" substring="platform"/>
+            </not>
+        </condition>
+    </fail>
+    <import file="${harness.dir}/build.xml"/>
+</project>

+ 8 - 0
jme3-behaviortrees/nbproject/genfiles.properties

@@ -0,0 +1,8 @@
+build.xml.data.CRC32=4eae1740
+build.xml.script.CRC32=f284e28d
[email protected]
+# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
+# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
+nbproject/build-impl.xml.data.CRC32=8aca329b
+nbproject/build-impl.xml.script.CRC32=35831a68
+nbproject/[email protected]

+ 178 - 0
jme3-behaviortrees/nbproject/platform.properties

@@ -0,0 +1,178 @@
+branding.token=jmonkeyplatform
+keystore=../nbproject/private/keystore
+nbm_alias=jmeupdates
+cluster.path=\
+    ${nbplatform.active.dir}/extide:\
+    ${nbplatform.active.dir}/harness:\
+    ${nbplatform.active.dir}/ide:\
+    ${nbplatform.active.dir}/java:\
+    ${nbplatform.active.dir}/nb:\
+    ${nbplatform.active.dir}/platform
+disabled.modules=\
+    org.apache.ws.commons.util,\
+    org.apache.xmlrpc,\
+    org.eclipse.core.contenttype,\
+    org.eclipse.core.net,\
+    org.eclipse.core.runtime,\
+    org.eclipse.mylyn.bugzilla.core,\
+    org.eclipse.mylyn.commons.core,\
+    org.eclipse.mylyn.commons.net,\
+    org.eclipse.mylyn.commons.repositories.core,\
+    org.eclipse.mylyn.commons.xmlrpc,\
+    org.eclipse.mylyn.tasks.core,\
+    org.eclipse.mylyn.wikitext.confluence.core,\
+    org.eclipse.mylyn.wikitext.core,\
+    org.eclipse.mylyn.wikitext.textile.core,\
+    org.netbeans.api.maven,\
+    org.netbeans.core.browser.webview,\
+    org.netbeans.lib.uihandler,\
+    org.netbeans.libs.commons_net,\
+    org.netbeans.libs.javafx,\
+    org.netbeans.libs.jsr223,\
+    org.netbeans.libs.nbi.ant,\
+    org.netbeans.libs.nbi.engine,\
+    org.netbeans.libs.smack,\
+    org.netbeans.libs.springframework,\
+    org.netbeans.modules.autoupdate.pluginimporter,\
+    org.netbeans.modules.bugtracking,\
+    org.netbeans.modules.bugtracking.bridge,\
+    org.netbeans.modules.bugtracking.commons,\
+    org.netbeans.modules.bugzilla,\
+    org.netbeans.modules.bugzilla.exceptionreporter,\
+    org.netbeans.modules.db,\
+    org.netbeans.modules.db.core,\
+    org.netbeans.modules.db.dataview,\
+    org.netbeans.modules.db.drivers,\
+    org.netbeans.modules.db.kit,\
+    org.netbeans.modules.db.metadata.model,\
+    org.netbeans.modules.db.mysql,\
+    org.netbeans.modules.db.sql.editor,\
+    org.netbeans.modules.db.sql.visualeditor,\
+    org.netbeans.modules.dbapi,\
+    org.netbeans.modules.dbschema,\
+    org.netbeans.modules.deadlock.detector,\
+    org.netbeans.modules.derby,\
+    org.netbeans.modules.form,\
+    org.netbeans.modules.form.binding,\
+    org.netbeans.modules.form.j2ee,\
+    org.netbeans.modules.form.kit,\
+    org.netbeans.modules.form.nb,\
+    org.netbeans.modules.form.refactoring,\
+    org.netbeans.modules.hibernate,\
+    org.netbeans.modules.hibernate4lib,\
+    org.netbeans.modules.html.angular,\
+    org.netbeans.modules.html.knockout,\
+    org.netbeans.modules.hudson,\
+    org.netbeans.modules.hudson.ant,\
+    org.netbeans.modules.hudson.git,\
+    org.netbeans.modules.hudson.maven,\
+    org.netbeans.modules.hudson.mercurial,\
+    org.netbeans.modules.hudson.subversion,\
+    org.netbeans.modules.hudson.tasklist,\
+    org.netbeans.modules.hudson.ui,\
+    org.netbeans.modules.i18n.form,\
+    org.netbeans.modules.ide.branding,\
+    org.netbeans.modules.ide.branding.kit,\
+    org.netbeans.modules.j2ee.core.utilities,\
+    org.netbeans.modules.j2ee.eclipselink,\
+    org.netbeans.modules.j2ee.eclipselinkmodelgen,\
+    org.netbeans.modules.j2ee.jpa.refactoring,\
+    org.netbeans.modules.j2ee.jpa.verification,\
+    org.netbeans.modules.j2ee.persistence,\
+    org.netbeans.modules.j2ee.persistence.kit,\
+    org.netbeans.modules.javaee.injection,\
+    org.netbeans.modules.jellytools.ide,\
+    org.netbeans.modules.jellytools.java,\
+    org.netbeans.modules.jellytools.platform,\
+    org.netbeans.modules.jemmy,\
+    org.netbeans.modules.languages,\
+    org.netbeans.modules.localtasks,\
+    org.netbeans.modules.maven,\
+    org.netbeans.modules.maven.checkstyle,\
+    org.netbeans.modules.maven.coverage,\
+    org.netbeans.modules.maven.embedder,\
+    org.netbeans.modules.maven.grammar,\
+    org.netbeans.modules.maven.graph,\
+    org.netbeans.modules.maven.hints,\
+    org.netbeans.modules.maven.indexer,\
+    org.netbeans.modules.maven.junit,\
+    org.netbeans.modules.maven.kit,\
+    org.netbeans.modules.maven.model,\
+    org.netbeans.modules.maven.osgi,\
+    org.netbeans.modules.maven.persistence,\
+    org.netbeans.modules.maven.refactoring,\
+    org.netbeans.modules.maven.repository,\
+    org.netbeans.modules.maven.search,\
+    org.netbeans.modules.maven.spring,\
+    org.netbeans.modules.mercurial,\
+    org.netbeans.modules.mylyn.util,\
+    org.netbeans.modules.performance,\
+    org.netbeans.modules.projectimport.eclipse.j2se,\
+    org.netbeans.modules.schema2beans,\
+    org.netbeans.modules.server,\
+    org.netbeans.modules.spellchecker,\
+    org.netbeans.modules.spellchecker.bindings.htmlxml,\
+    org.netbeans.modules.spellchecker.bindings.properties,\
+    org.netbeans.modules.spellchecker.dictionary_en,\
+    org.netbeans.modules.spellchecker.kit,\
+    org.netbeans.modules.spring.beans,\
+    org.netbeans.modules.testng.maven,\
+    org.netbeans.modules.uihandler,\
+    org.netbeans.modules.uihandler.exceptionreporter,\
+    org.netbeans.modules.web.webkit.debugging,\
+    org.netbeans.modules.websvc.saas.codegen.java,\
+    org.netbeans.modules.welcome,\
+    org.netbeans.modules.xml.wsdl.model,\
+    org.netbeans.upgrader,\
+    org.openide.compat,\
+    org.openide.options,\
+    org.openide.util.enumerations,\
+    org.openidex.util
+nbjdk.active=default
+nbplatform.active=default
+
+#need these in the file for all dependencies to resolve when using command line
+#ant and a download of the platform, same as those generated by netbeans platform
+#under ${user.properties.file} (defined in platform-private.properties)
+default.javac.source=1.6
+default.javac.target=1.6
+libs.absolutelayout.classpath=${nbplatform.default.netbeans.dest.dir}/java/modules/ext/AbsoluteLayout.jar
+libs.absolutelayout.javadoc=
+libs.absolutelayout.maven-pom=
+libs.absolutelayout.src=
+libs.beans-binding.classpath=${nbplatform.default.netbeans.dest.dir}/java/modules/ext/beansbinding-1.2.1.jar
+libs.beans-binding.javadoc=${nbplatform.default.netbeans.dest.dir}/java/docs/beansbinding-1.2.1-doc.zip
+libs.beans-binding.maven-pom=
+libs.beans-binding.src=
+libs.CopyLibs.classpath=${nbplatform.default.netbeans.dest.dir}/java/ant/extra/org-netbeans-modules-java-j2seproject-copylibstask.jar
+libs.CopyLibs.javadoc=
+libs.CopyLibs.maven-pom=
+libs.CopyLibs.src=
+libs.javac-api.classpath=${nbplatform.default.netbeans.dest.dir}/java/modules/ext/nb-javac-api.jar
+libs.javac-api.javadoc=
+libs.javac-api.maven-pom=
+libs.javac-api.src=
+libs.JAXB-ENDORSED.classpath=${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/api/jaxb-api.jar
+libs.JAXB-ENDORSED.javadoc=
+libs.JAXB-ENDORSED.maven-pom=
+libs.JAXB-ENDORSED.src=
+libs.jaxb.classpath=${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/jaxb-impl.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/jaxb-xjc.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/jaxb1-impl.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/activation.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/api/jaxb-api.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/api/jsr173_1.0_api.jar
+libs.jaxb.javadoc=${nbplatform.default.netbeans.dest.dir}/ide/docs/jaxb-api-doc.zip
+libs.jaxb.maven-pom=
+libs.jaxb.src=
+libs.jaxws21.classpath=${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/jaxb-impl.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/jaxb-xjc.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/FastInfoset.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/gmbal-api-only.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/ha-api.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/javax.mail_1.4.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/jaxws-rt.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/jaxws-tools.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/management-api.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/mimepull.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/policy.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/saaj-impl.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/stax-ex.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/stax2-api.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/streambuffer.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/woodstox-core-asl.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/api/jaxws-api.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/api/jsr181-api.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/api/javax.annotation.jar:${nbplatform.default.netbeans.dest.dir}/java/modules/ext/jaxws22/api/saaj-api.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/activation.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/api/jaxb-api.jar:${nbplatform.default.netbeans.dest.dir}/ide/modules/ext/jaxb/api/jsr173_1.0_api.jar
+libs.jaxws21.javadoc=${nbplatform.default.netbeans.dest.dir}/java/docs/jaxws-api-doc.zip
+libs.jaxws21.maven-pom=
+libs.jaxws21.src=
+libs.JWSAntTasks.classpath=${nbplatform.default.netbeans.dest.dir}/java/ant/extra/org-netbeans-modules-javawebstart-anttasks.jar
+libs.JWSAntTasks.javadoc=
+libs.JWSAntTasks.maven-pom=
+libs.JWSAntTasks.src=
+libs.swing-layout.classpath=${nbplatform.default.netbeans.dest.dir}/platform/modules/ext/swing-layout-1.0.4.jar
+libs.swing-layout.javadoc=${nbplatform.default.netbeans.dest.dir}/platform/docs/swing-layout-1.0.4-doc.zip
+libs.swing-layout.maven-pom=
+libs.swing-layout.src=${nbplatform.default.netbeans.dest.dir}/platform/docs/swing-layout-1.0.4-src.zip
+libs.testng.classpath=${nbplatform.default.netbeans.dest.dir}/platform/modules/ext/testng-6.8.1-dist.jar
+libs.testng.javadoc=${nbplatform.default.netbeans.dest.dir}/platform/docs/testng-6.8.1-javadoc.zip
+libs.testng.maven-pom=
+libs.testng.src=

+ 8 - 0
jme3-behaviortrees/nbproject/project.properties

@@ -0,0 +1,8 @@
+#Thu, 25 Aug 2011 20:26:49 +0200
+javac.source=1.8
+javac.compilerargs=-Xlint -Xlint:-serial -g
+license.file=../license-jme.txt
+nbm.homepage=http://www.jmonkeyengine.org
+nbm.module.author=MeFisto94
+nbm.needs.restart=true
+spec.version.base=3.2.0

+ 250 - 0
jme3-behaviortrees/nbproject/project.xml

@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+    <type>org.netbeans.modules.apisupport.project</type>
+    <configuration>
+        <data xmlns="http://www.netbeans.org/ns/nb-module-project/3">
+            <code-name-base>com.jme3.gde.behaviortrees</code-name-base>
+            <suite-component/>
+            <module-dependencies>
+                <dependency>
+                    <code-name-base>com.jme3.gde.core</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>3.2.0</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>com.jme3.gde.core.baselibs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>3.2.0</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>com.jme3.gde.core.libraries</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>3.2.0</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.api.java.classpath</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.52.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.api.templates</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>1.6.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.api.visual</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>2.43.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.core.multiview</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.40.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.editor.document</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>1.5.1.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.editor.lib</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>3</release-version>
+                        <specification-version>3.49.2.22.43</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.java.project</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.66.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.java.project.ui</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.69.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.java.source.base</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>2.20.1.2.2.25.8.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.projectapi</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.60.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.settings</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.45.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.spi.navigator</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.33.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.actions</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.38.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.awt</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.62.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.dialogs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.38.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.explorer</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.57.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.filesystems</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>9.7.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.filesystems.nb</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>9.7.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.loaders</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.57.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.nodes</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.39.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.text</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.62.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.39.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util.lookup</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.25.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util.ui</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>9.4.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.windows</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.71.1</specification-version>
+                    </run-dependency>
+                </dependency>
+            </module-dependencies>
+            <public-packages>
+                <package>com.jme3.gde.behaviortrees</package>
+            </public-packages>
+        </data>
+    </configuration>
+</project>

+ 1 - 0
jme3-behaviortrees/nbproject/suite.properties

@@ -0,0 +1 @@
+suite.dir=${basedir}/..

+ 394 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeDataObject.java

@@ -0,0 +1,394 @@
+/*
+ * Copyright (c) 2009-2018 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees;
+
+import com.badlogic.gdx.ai.btree.BehaviorTree;
+import com.jme3.gde.behaviortrees.editor.BTreeNodeEditorElement;
+import com.jme3.gde.behaviortrees.nodes.impl.RootNodePanel;
+import com.jme3.gde.core.assets.ProjectAssetManager;
+import com.jme3.gde.core.errorreport.ExceptionUtils;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import javax.swing.JEditorPane;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.core.spi.multiview.MultiViewElement;
+import org.netbeans.core.spi.multiview.text.MultiViewEditorElement;
+import org.netbeans.spi.actions.AbstractSavable;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.awt.ActionReferences;
+import org.openide.cookies.SaveCookie;
+import org.openide.filesystems.FileChangeAdapter;
+import org.openide.filesystems.FileEvent;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.MIMEResolver;
+import org.openide.loaders.DataFolder;
+import org.openide.loaders.DataObject;
+import org.openide.loaders.DataObjectExistsException;
+import org.openide.loaders.MultiDataObject;
+import org.openide.loaders.MultiFileLoader;
+import org.openide.nodes.CookieSet;
+import org.openide.util.Lookup;
+import org.openide.util.NbBundle.Messages;
+import org.openide.util.lookup.AbstractLookup;
+import org.openide.util.lookup.InstanceContent;
+import org.openide.util.lookup.ProxyLookup;
+import org.openide.windows.TopComponent;
+
[email protected](
+    displayName = "LibGDX AI Behavior Tree",
+mimeType = "text/gdx-ai-btree",
+extension = {"behaviortree", "btree"})
[email protected](
+    mimeType = "text/gdx-ai-btree",
+iconBase = "com/jme3/gde/core/editor/icons/matdef.png",
+displayName = "LibGDX AI Behavior Tree",
+position = 300)
+@ActionReferences({
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "System", id = "org.openide.actions.OpenAction"),
+    position = 100,
+    separatorAfter = 200),    
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "Edit", id = "org.openide.actions.CutAction"),
+    position = 300),
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "Edit", id = "org.openide.actions.CopyAction"),
+    position = 400,
+    separatorAfter = 500),
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "Edit", id = "org.openide.actions.DeleteAction"),
+    position = 600),
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "System", id = "org.openide.actions.RenameAction"),
+    position = 700,
+    separatorAfter = 800),
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "System", id = "org.openide.actions.SaveAsTemplateAction"),
+    position = 900,
+    separatorAfter = 1000),
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "System", id = "org.openide.actions.FileSystemAction"),
+    position = 1100,
+    separatorAfter = 1200),
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "System", id = "org.openide.actions.ToolsAction"),
+    position = 1300),
+    @ActionReference(
+        path = "Loaders/text/gdx-ai-btree/Actions",
+    id =
+    @ActionID(category = "System", id = "org.openide.actions.PropertiesAction"),
+    position = 1400)
+})
+@Messages("LBL_BTREE_LOADER=LibGDX AI Behavior Tree")
+public class BTreeDataObject extends MultiDataObject {
+    protected final Lookup lookup;
+    protected final InstanceContent lookupContents = new InstanceContent();
+    private boolean loaded = false;
+    protected BehaviorTree mainTree;
+    protected RootNodePanel rootNodePanel;
+    protected final BTreeParser<Object> parser;
+    protected ProjectAssetManager manager;
+    protected BTreeNodeEditorElement topComponent;
+
+    @SuppressWarnings("LeakingThisInConstructor")
+    public BTreeDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
+        super(pf, loader);
+        registerEditor("text/gdx-ai-btree", true);
+        manager = findAssetManager();
+        lookup = new ProxyLookup(getCookieSet().getLookup(), new AbstractLookup(lookupContents));
+        final BTreeMetaData metaData = new BTreeMetaData(this);
+        lookupContents.add(metaData);
+        lookupContents.add(this);
+        pf.addFileChangeListener(new FileChangeAdapter() {
+            @Override
+            public void fileChanged(FileEvent fe) {
+                super.fileChanged(fe);
+                metaData.save();
+                /*if (file.isDirty()) {
+                    file.setLoaded(false);
+                    file.setDirty(false);
+                }*/
+            }
+        });
+        parser = new BTreeParser<>(this, BTreeParser.DEBUG_HIGH);
+        CookieSet cookies = getCookieSet();
+        // cookies.add(new Opener());
+    }
+
+    public BTreeNodeEditorElement getTopComponent() {
+        return topComponent;
+    }
+
+    public void setTopComponent(BTreeNodeEditorElement topComponent) {
+        this.topComponent = topComponent;
+    }
+    
+    private ProjectAssetManager findAssetManager() {
+        FileObject file = getPrimaryFile();
+        ProjectManager pm = ProjectManager.getDefault();
+        while (file != null) {
+            if (file.isFolder() && pm.isProject(file)) {
+                try {
+                    Project project = ProjectManager.getDefault().findProject(file);
+                    if (project != null) {
+                        ProjectAssetManager mgr = project.getLookup().lookup(ProjectAssetManager.class);
+                        if (mgr != null) {
+                            getLookupContents().add(mgr);
+                            return mgr;
+                        }
+                    }
+                } catch (IOException ex) {
+                } catch (IllegalArgumentException ex) {
+                }
+            }
+            file = file.getParent();
+        }
+        return null;
+    }
+
+    public ProjectAssetManager getManager() {
+        return manager;
+    }
+    
+    @Override
+    protected int associateLookup() {
+        return 1;
+    }
+
+    @Override
+    public Lookup getLookup() {
+        return lookup;
+    }
+
+    public BehaviorTree getMainTree() {
+        return mainTree;
+    }
+
+    public RootNodePanel getRootNodePanel() {
+        return rootNodePanel;
+    }
+
+    public void setRootNodePanel(RootNodePanel rootNodePanel) {
+        this.rootNodePanel = rootNodePanel;
+    }
+
+    public BTreeParser<Object> getParser() {
+        return parser;
+    }
+       
+    @MultiViewElement.Registration(
+    displayName = "#LBL_BTREE_EDITOR",
+    iconBase = "com/jme3/gde/core/editor/icons/vert.png",
+    mimeType = "text/gdx-ai-btree",
+    persistenceType = TopComponent.PERSISTENCE_ONLY_OPENED,
+    preferredID = "BehaviorTree",
+    position = 1000)
+    @Messages("LBL_BTREE_EDITOR=Code")
+    public static MultiViewEditorElement createEditor(Lookup lkp) {
+        final BTreeDataObject obj = lkp.lookup(BTreeDataObject.class);
+        obj.refresh();
+        
+        // Open a regular text editor element
+        MultiViewEditorElement ed = new MultiViewEditorElement(lkp) {
+            // Mark as Modified when something has been typed
+            KeyListener listener = new KeyListener() {
+                @Override
+                public void keyTyped(KeyEvent e) {
+                }
+
+                @Override
+                public void keyPressed(KeyEvent e) {
+                    obj.setModified(true);
+                }
+
+                @Override
+                public void keyReleased(KeyEvent e) {                   
+                }
+            };
+
+            @Override
+            public void componentActivated() {
+                super.componentActivated();
+                getEditorPane().addKeyListener(listener);                
+            }
+
+            @Override
+            public void componentDeactivated() {
+                super.componentDeactivated();
+                JEditorPane editorPane = getEditorPane();
+                if (editorPane != null) {
+                    getEditorPane().removeKeyListener(listener);
+                }
+            }
+
+            @Override
+            public void componentClosed() {
+                super.componentClosed();
+                obj.unload();
+            }
+        };
+        obj.getLookupContents().add(ed);
+        return ed;
+    }
+
+    @Override
+    protected void handleDelete() throws IOException {
+        BTreeMetaData metaData = lookup.lookup(BTreeMetaData.class);
+        if(metaData != null){
+            metaData.cleanup();
+        }
+        super.handleDelete();
+    }
+
+    @Override
+    protected FileObject handleRename(String name) throws IOException {
+        BTreeMetaData metaData = lookup.lookup(BTreeMetaData.class);
+        metaData.rename(null, name);
+        return super.handleRename(name);
+    }
+
+    @Override
+    protected FileObject handleMove(DataFolder df) throws IOException {
+        BTreeMetaData metaData = lookup.lookup(BTreeMetaData.class);
+        metaData.rename(df, null);
+        return super.handleMove(df);
+    }
+
+    @Override
+    protected DataObject handleCopy(DataFolder df) throws IOException {
+        BTreeMetaData metaData = lookup.lookup(BTreeMetaData.class);
+        metaData.duplicate(df, null);
+        return super.handleCopy(df);
+    }
+
+    @Override
+    protected DataObject handleCopyRename(DataFolder df, String name, String ext) throws IOException {
+        BTreeMetaData metaData = lookup.lookup(BTreeMetaData.class);
+        metaData.duplicate(df, name);
+        return super.handleCopyRename(df, name, ext);
+    }
+
+    public boolean isLoaded() {
+        return loaded;
+    }
+
+    public void unload() {
+        if (loaded) {
+            loaded = false;
+        }
+    }
+
+    public InstanceContent getLookupContents() {
+        return lookupContents;
+    }
+    
+    public void refresh() {
+        loaded = true;
+        
+        try {
+            mainTree = getParser().parse(getPrimaryFile().getInputStream(), null);
+        } catch (Exception e) {
+            ExceptionUtils.caughtException(e, "This problem occured when trying to load a behavior tree from file " + getPrimaryFile().getPath());
+        }
+    }
+    
+    public void setDirty() {
+        setModified(true);
+        lookupContents.add(new MySavable(this));
+    }
+    
+    public void setClean() {
+        setModified(false);
+        lookupContents.remove(lookup.lookup(MySavable.class));
+    }
+    
+    private class MySavable extends AbstractSavable {
+        private final BTreeDataObject obj;
+        public MySavable(BTreeDataObject obj) {
+            this.obj = obj;
+            register();
+        }
+
+        @Override
+        protected String findDisplayName() {
+            return obj.getName();
+        }
+
+        @Override
+        protected void handleSave() throws IOException {
+            //@TODO: One would have to synchronize access to mainTree as well..            
+            synchronized (obj) {
+                FileObject fo = getPrimaryFile();
+                try (OutputStream out = new BufferedOutputStream(fo.getOutputStream())) {    
+                    BTreeExporter.exportTree(out, mainTree);
+                }
+            }
+            obj.lookupContents.remove(this);
+            setModified(false);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof MySavable) {
+                return obj.mainTree.equals(((MySavable) o).obj.mainTree);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return obj.mainTree.hashCode();
+        }
+    }
+}

+ 268 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeExporter.java

@@ -0,0 +1,268 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees;
+
+import com.badlogic.gdx.ai.btree.BehaviorTree;
+import com.badlogic.gdx.ai.btree.Task;
+import com.badlogic.gdx.ai.btree.branch.DynamicGuardSelector;
+import com.badlogic.gdx.ai.btree.branch.Parallel;
+import com.badlogic.gdx.ai.btree.branch.RandomSelector;
+import com.badlogic.gdx.ai.btree.branch.RandomSequence;
+import com.badlogic.gdx.ai.btree.branch.Selector;
+import com.badlogic.gdx.ai.btree.branch.Sequence;
+import com.badlogic.gdx.ai.btree.decorator.AlwaysFail;
+import com.badlogic.gdx.ai.btree.decorator.AlwaysSucceed;
+import com.badlogic.gdx.ai.btree.decorator.Include;
+import com.badlogic.gdx.ai.btree.decorator.Invert;
+import com.badlogic.gdx.ai.btree.decorator.Random;
+import com.badlogic.gdx.ai.btree.decorator.Repeat;
+import com.badlogic.gdx.ai.btree.decorator.SemaphoreGuard;
+import com.badlogic.gdx.ai.btree.decorator.UntilFail;
+import com.badlogic.gdx.ai.btree.decorator.UntilSuccess;
+import com.badlogic.gdx.ai.btree.leaf.Failure;
+import com.badlogic.gdx.ai.btree.leaf.Success;
+import com.badlogic.gdx.ai.btree.leaf.Wait;
+import com.badlogic.gdx.ai.utils.random.Distribution;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.badlogic.gdx.utils.reflect.Field;
+import com.badlogic.gdx.utils.reflect.ReflectionException;
+import com.jme3.gde.behaviortrees.BTreeExporterUtils.AttrInfo;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * Exports BehaviorTrees to .btree files.<br>
+ * It is specially geared towards DummyTasks as used in the SDK Editor.<br>
+ * It even supports using imports to decrease filesize and increase human readability
+ * @TODO: /NOTE: Get rid of the usage of DummyTasks as we cannot guess the parameters upon import,
+ * so we force the user to make the tasks known to the engine
+ * @author MeFisto94
+ */
+public class BTreeExporter {
+    
+    protected HashMap<String, String> classNameToFQNMap;
+    protected HashMap<String, String> fqnToImport;
+    protected PrintWriter w;
+    private final Class<?>[] builtinClasses = new Class<?>[] {
+        AlwaysFail.class,
+        AlwaysSucceed.class,
+        DynamicGuardSelector.class,
+        Failure.class,
+        Include.class,
+        Invert.class,
+        Parallel.class,
+        Random.class,
+        RandomSelector.class,
+        RandomSequence.class,
+        Repeat.class,
+        Selector.class,
+        SemaphoreGuard.class,
+        Sequence.class,
+        Success.class,
+        UntilFail.class,
+        UntilSuccess.class,
+        Wait.class
+    };	
+    
+    private BTreeExporter(OutputStream out) {
+        classNameToFQNMap = new HashMap<>();
+        fqnToImport = new HashMap<>();
+        w = new PrintWriter(out, true);
+    }
+        
+    public static void exportTree(OutputStream out, BehaviorTree tree) {
+        if (tree == null) {
+            return;
+        }
+        
+        BTreeExporter bt = new BTreeExporter(out);
+        
+        /* First we fill the ClassName Map and if we see that a key already 
+         * exists, we add a number to it and strip the TASK suffix.
+         * Also to not have thousands of reverse lookups we populate fqn with
+         * the opposite.
+        */
+        bt.handleImports(tree);
+        bt.writeImports();
+        bt.w.println("root");
+        for (int i = 0; i < tree.getChildCount(); i++) {
+            bt.exportTask(tree.getChild(i), 1);
+        }
+    }
+    
+    protected void handleImports(Task task) {
+        if (isCustomTask(task)) {
+            String c = getCanonical(task);
+            String s = getSimpleName(task);
+            if (!fqnToImport.containsKey(c)) {
+                if (!classNameToFQNMap.containsKey(s)) {
+                    classNameToFQNMap.put(s, c);
+                    fqnToImport.put(c, s);
+                } else { // Fix Name Clash
+                    boolean success = false;
+                    for (int i = 0; i < Integer.MAX_VALUE; i++) {
+                        if (!classNameToFQNMap.containsKey(s + i)) {
+                            classNameToFQNMap.put(s + i, c);
+                            fqnToImport.put(c, s);
+                            success = true;
+                            break;
+                        }
+                    }
+                    
+                    if (!success) {
+                        throw new IllegalStateException("Failed to solve the name clash at import handling");
+                    }
+                }
+            }
+        }
+        
+        for (int i = 0; i < task.getChildCount(); i++) {
+            handleImports(task.getChild(i));
+        }
+    }
+    
+    protected void writeImports() {
+        classNameToFQNMap.forEach((k, v) -> w.println("import " + k + ":\"" + v + "\""));
+    }
+    
+    protected void exportTask(Task task, int ident) {
+        String guard = "";
+        
+        if (task.getGuard() != null) {
+            guard = "(" + extractTask(task.getGuard()) + ") ";
+        }
+        w.println(ident(ident) + guard + extractTask(task)); // No params so far
+        
+        for (int i = 0; i < task.getChildCount(); i++) {
+            exportTask(task.getChild(i), ident + 1);
+        }
+    }
+    
+    protected String extractTask(Task task) {
+        return extractTaskName(task) + extractAttributes(task);
+    }
+
+    protected String extractAttributes(Task task) {
+        ObjectMap<String, AttrInfo> attrs = BTreeExporterUtils.findMetadata(task.getClass());
+
+        if (attrs.size == 0) {
+            return "";
+        }
+
+        return " " + StreamSupport.stream(attrs.spliterator(), false)
+            .map(entry -> {
+                try {
+                    return entry.key + ":" + attrVal(task, entry.value);
+                } catch (ReflectionException refl) {
+                    return "";                        
+                }
+            }).collect(Collectors.joining(" "));
+    }
+    
+    private String attrVal(Task t, AttrInfo ai) throws ReflectionException {
+        Field f = ai.f;
+        Class type = ai.f.getType();
+        if (String.class.isAssignableFrom(type)) {
+            return "\"" + (String)f.get(t) + "\"";
+        } else if (Enum.class.isAssignableFrom(type)) {
+            return "\"" + f.get(t).toString().toLowerCase() + "\"";
+        } else if (Distribution.class.isAssignableFrom(type)) {
+            return "\"" + DistributionStringConverter.fromDistribution((Distribution)f.get(t)) + "\"";
+        }
+
+        return String.valueOf(f.get(t));
+    }
+    
+    protected String extractTaskName(Task task) throws IllegalStateException {
+        if (isCustomTask(task)) {
+            String c = getCanonical(task);
+            if (fqnToImport.containsKey(c)) {
+                return fqnToImport.get(c);
+            } else {
+                throw new IllegalStateException("Could not find import for class " + c);
+            }
+        } else if (isBuiltin(task.getClass())) {
+            return builtinTask(task);
+        } else {
+            throw new IllegalStateException("Unknown Task");
+        }
+    }
+    
+    protected boolean isBuiltin(Class c) {        
+        // We could also use assignableFrom, but it works without.
+        return c.equals(BehaviorTree.class) || Arrays.stream(builtinClasses).anyMatch(b -> b.equals(c));
+    }    
+    
+    protected boolean isCustomTask(Task t) {
+        if (t.getClass().getCanonicalName().startsWith("com.badlogic.gdx.ai.btree")) {
+            return false;
+        } else if (isBuiltin(t.getClass())) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+    
+    protected String builtinTask(Task t) {
+        if (t instanceof BehaviorTree) {
+            BehaviorTree bt = (BehaviorTree)t; // @TODO: BTree References
+            return "root";
+        }
+        
+        // Code also taken from BehaviorTaskParser
+        String cn = t.getClass().getSimpleName();
+        return Character.toLowerCase(cn.charAt(0)) + (cn.length() > 1 ? cn.substring(1) : "");
+    }
+    
+    /* Because our editor deals with DummyTask wrapping classes for unknown tasks
+     * unwrap them here to export them properly again
+     */
+    protected String getCanonical(Task t) {
+        return t.getClass().getCanonicalName();
+    }
+    
+    protected String getSimpleName(Task t) {
+        return t.getClass().getSimpleName();
+    }
+    
+    protected String ident(int ident) {
+        StringBuilder sB = new StringBuilder();
+        for (int i = 0; i < ident; i++) {
+            sB.append("  "); // 2 spaces used in example files
+        }        
+        return sB.toString();
+    }
+}

+ 82 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeExporterUtils.java

@@ -0,0 +1,82 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees;
+
+import com.badlogic.gdx.ai.btree.annotation.TaskAttribute;
+import com.badlogic.gdx.ai.btree.annotation.TaskConstraint;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.badlogic.gdx.utils.reflect.Annotation;
+import com.badlogic.gdx.utils.reflect.ClassReflection;
+import com.badlogic.gdx.utils.reflect.Field;
+
+/**
+ * This class contains code which can be used by both: BTreeExporter and some
+ * other spots in the SDK.
+ * @author MeFisto94
+ */
+public class BTreeExporterUtils {
+    public static ObjectMap<String, AttrInfo> findMetadata (Class<?> clazz) {
+        Annotation tca = ClassReflection.getAnnotation(clazz, TaskConstraint.class);
+        if (tca != null) {
+            ObjectMap<String, AttrInfo> taskAttributes = new ObjectMap<>();
+            Field[] fields = ClassReflection.getFields(clazz);
+            for (Field f : fields) {
+                Annotation a = f.getDeclaredAnnotation(TaskAttribute.class);
+                if (a != null) {
+                    AttrInfo ai = new AttrInfo(f.getName(), a.getAnnotation(TaskAttribute.class), f);
+                    taskAttributes.put(ai.name, ai);
+                }
+            }
+            return taskAttributes;
+        } else {
+            return null;
+        }
+    }
+    
+    public static class AttrInfo {
+        public String name;
+        public String fieldName;
+        public boolean required;
+        public Field f;
+
+        AttrInfo (String fieldName, TaskAttribute annotation, Field f) {
+            this(annotation.name(), fieldName, annotation.required(), f);
+        }
+
+        AttrInfo (String name, String fieldName, boolean required, Field f) {
+            this.name = name == null || name.length() == 0 ? fieldName : name;
+            this.fieldName = fieldName;
+            this.required = required;
+            this.f = f;
+        }
+    }
+}

+ 408 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeMetaData.java

@@ -0,0 +1,408 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.BufferedOutputStream;
+import java.io.BufferedInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.netbeans.api.project.Project;
+import org.openide.filesystems.FileLock;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.loaders.DataFolder;
+import org.openide.util.Exceptions;
+import org.openide.util.Mutex;
+import org.openide.util.Mutex.Action;
+
+/**
+ * Global object to access actual jME3 data within an AssetDataObject, available
+ * through the Lookup of any AssetDataObject. AssetDataObjects that wish to use
+ *
+ * @author normenhansen
+ */
+@SuppressWarnings("unchecked")
+public class BTreeMetaData {
+
+    private static final Logger logger = Logger.getLogger(BTreeMetaData.class.getName());
+    private final List<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
+    private final Mutex propsMutex = new Mutex();
+    private final Properties props = new Properties();
+    private static final Properties defaultProps = new Properties();
+
+    static {
+        defaultProps.put("Default/position", "0,120");
+        defaultProps.put("Default/MatParam.Color", "438,351");
+        defaultProps.put("Default/Default/ColorMult", "605,372");
+        defaultProps.put("Default/WorldParam.WorldViewProjectionMatrix", "33,241");
+        defaultProps.put("Default/color", "0,478");
+        defaultProps.put("Default/Default/CommonVert", "211,212");
+    }
+    private BTreeDataObject file;
+    private String extension = "jmpdata";
+    private Date lastLoaded;
+    private FileObject folder;
+    private FileObject root;
+
+//    private XMLFileSystem fs;
+    public BTreeMetaData(BTreeDataObject file) {
+        try {
+            this.file = file;
+            getFolder();
+            FileObject primaryFile = file.getPrimaryFile();
+
+            if (primaryFile != null) {
+                extension = primaryFile.getExt() + "data";
+            }
+
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    public BTreeMetaData(BTreeDataObject file, String extension) {
+        this.file = file;
+        this.extension = extension;
+    }
+
+    public synchronized String getProperty(final String key) {
+        readProperties();
+        return propsMutex.readAccess(new Action<String>() {
+            @Override
+            public String run() {
+                String prop = props.getProperty(key);
+                if (prop == null) {
+                    return defaultProps.getProperty(key);
+                }
+                return prop;
+            }
+        });
+    }
+
+    public synchronized String getProperty(final String key, final String defaultValue) {
+        readProperties();
+        return propsMutex.readAccess(new Action<String>() {
+            @Override
+            public String run() {
+                String prop = props.getProperty(key);
+                if (prop == null) {
+                    return defaultProps.getProperty(key);
+                }
+                return prop;
+            }
+        });
+    }
+
+    public synchronized String setProperty(final String key, final String value) {
+        readProperties();
+        String ret = propsMutex.writeAccess(new Action<String>() {
+            @Override
+            public String run() {
+                String ret = (String) props.setProperty(key, value);
+                return ret;
+            }
+        });
+        // writeProperties();
+        notifyListeners(key, ret, value);
+        return ret;
+    }
+
+    private void readProperties() {
+        propsMutex.writeAccess(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    FileObject pFile = file.getPrimaryFile();
+                    FileObject storageFolder = getFolder();
+                    if (storageFolder == null) {
+                        return;
+                    }
+                    final FileObject myFile = storageFolder.getFileObject(getFileFullName(pFile)); //fs.findResource(fs.getRoot().getPath()+"/"+ file.getPrimaryFile().getName() + "." + extension);//
+
+                    if (myFile == null) {
+                        return;
+                    }
+                    final Date lastMod = myFile.lastModified();
+                    if (!lastMod.equals(lastLoaded)) {
+                        props.clear();
+                        lastLoaded = lastMod;
+                        InputStream in = null;
+                        try {
+                            in = new BufferedInputStream(myFile.getInputStream());
+                            try {
+                                props.load(in);
+                            } catch (IOException ex) {
+                                Exceptions.printStackTrace(ex);
+                            }
+                        } catch (FileNotFoundException ex) {
+                            Exceptions.printStackTrace(ex);
+                        } finally {
+                            try {
+                                in.close();
+                            } catch (IOException ex) {
+                                Exceptions.printStackTrace(ex);
+                            }
+                        }
+                        logger.log(Level.FINE, "Read AssetData properties for {0}", file);
+                    }
+                } catch (IOException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            }
+        });
+    }
+
+    public void cleanup() {
+        propsMutex.writeAccess(new Runnable() {
+            @Override
+            public void run() {
+                OutputStream out = null;
+                FileLock lock = null;
+                try {
+                    FileObject pFile = file.getPrimaryFile();
+                    FileObject storageFolder = getFolder();
+                    if (storageFolder == null) {
+                        return;
+                    }
+
+                    FileObject myFile = storageFolder.getFileObject(getFileFullName(pFile));//FileUtil.findBrother(pFile, extension);//fs.findResource(fs.getRoot().getPath()+"/"+ pFile.getName() + "." + extension);//
+                    if (myFile == null) {
+                        return;
+                    }
+                    lock = myFile.lock();
+                    myFile.delete(lock);
+                } catch (IOException e) {
+                    Exceptions.printStackTrace(e);
+                } finally {
+                    if (out != null) {
+                        try {
+                            out.close();
+                        } catch (IOException ex) {
+                            Exceptions.printStackTrace(ex);
+                        }
+                    }
+                    if (lock != null) {
+                        lock.releaseLock();
+                    }
+                }
+            }
+        });
+
+    }
+
+    public void rename(final DataFolder df, final String name) {
+        propsMutex.writeAccess(new Runnable() {
+            @Override
+            public void run() {
+                OutputStream out = null;
+                FileLock lock = null;
+                try {
+                    FileObject pFile = file.getPrimaryFile();
+                    FileObject storageFolder = getFolder();
+                    if (storageFolder == null) {
+                        return;
+                    }
+
+                    FileObject myFile = storageFolder.getFileObject(getFileFullName(pFile));//FileUtil.findBrother(pFile, extension);//fs.findResource(fs.getRoot().getPath()+"/"+ pFile.getName() + "." + extension);//
+                    if (myFile == null) {
+                        return;
+                    }
+                    lock = myFile.lock();
+                    if (df == null) {
+                        myFile.rename(lock, getFileFullName(pFile).replaceAll(file.getName() + "." + extension, name + "." + extension), "");
+                    } else {
+                        myFile.rename(lock, FileUtil.getRelativePath(root, df.getPrimaryFile()).replaceAll("/", ".") + "." + pFile.getName(), extension);
+                    }
+                } catch (IOException e) {
+                    Exceptions.printStackTrace(e);
+                } finally {
+                    if (out != null) {
+                        try {
+                            out.close();
+                        } catch (IOException ex) {
+                            Exceptions.printStackTrace(ex);
+                        }
+                    }
+                    if (lock != null) {
+                        lock.releaseLock();
+                    }
+                }
+            }
+        });
+
+    }
+
+    public void duplicate(final DataFolder df, final String name) {
+        propsMutex.writeAccess(new Runnable() {
+            @Override
+            public void run() {
+                OutputStream out = null;
+                FileLock lock = null;
+                try {
+                    FileObject pFile = file.getPrimaryFile();
+                    FileObject storageFolder = getFolder();
+                    if (storageFolder == null) {
+                        return;
+                    }
+                    String newName = name;
+                    if (newName == null) {
+                        newName = file.getName();
+                    }
+                    String path = FileUtil.getRelativePath(root, df.getPrimaryFile()).replaceAll("/", ".") + "." + newName;
+                    FileObject myFile = storageFolder.getFileObject(getFileFullName(pFile));
+                    if (myFile == null) {
+                        return;
+                    }
+
+                    myFile.copy(storageFolder, path, extension);
+
+                } catch (IOException e) {
+                    Exceptions.printStackTrace(e);
+                } finally {
+                    if (out != null) {
+                        try {
+                            out.close();
+                        } catch (IOException ex) {
+                            Exceptions.printStackTrace(ex);
+                        }
+                    }
+                    if (lock != null) {
+                        lock.releaseLock();
+                    }
+                }
+            }
+        });
+
+    }
+
+    public void save() {
+        if (!props.isEmpty()) {
+            writeProperties();
+        }
+    }
+
+    private void writeProperties() {
+        //writeAccess because we write lastMod date, not because we write to the file
+        //the mutex protects the properties object, not the file
+        propsMutex.writeAccess(new Runnable() {
+            @Override
+            public void run() {
+                OutputStream out = null;
+                FileLock lock = null;
+                try {
+                    FileObject pFile = file.getPrimaryFile();
+                    FileObject storageFolder = getFolder();
+                    if (storageFolder == null) {
+                        return;
+                    }
+
+                    FileObject myFile = storageFolder.getFileObject(getFileFullName(pFile));//FileUtil.findBrother(pFile, extension);//fs.findResource(fs.getRoot().getPath()+"/"+ pFile.getName() + "." + extension);//
+                    if (myFile == null) {
+                        myFile = FileUtil.createData(storageFolder, getFileFullName(pFile));
+                    }
+                    lock = myFile.lock();
+                    out = new BufferedOutputStream(myFile.getOutputStream(lock));
+                    props.store(out, "");
+                    out.flush();
+                    lastLoaded = myFile.lastModified();
+                    logger.log(Level.FINE, "Written AssetData properties for {0}", file);
+                } catch (IOException e) {
+                    Exceptions.printStackTrace(e);
+                } finally {
+                    if (out != null) {
+                        try {
+                            out.close();
+                        } catch (IOException ex) {
+                            Exceptions.printStackTrace(ex);
+                        }
+                    }
+                    if (lock != null) {
+                        lock.releaseLock();
+                    }
+                }
+            }
+        });
+    }
+
+    private String getFileFullName(FileObject pFile) {
+        return FileUtil.getRelativePath(root, pFile).replaceAll("/", ".").replaceAll("j3md", extension);
+    }
+
+    protected void notifyListeners(String property, String before, String after) {
+        synchronized (listeners) {
+            for (PropertyChangeListener propertyChangeListener : listeners) {
+                propertyChangeListener.propertyChange(new PropertyChangeEvent(this, property, before, after));
+            }
+        }
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        synchronized (listeners) {
+            listeners.add(listener);
+        }
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        synchronized (listeners) {
+            listeners.remove(listener);
+        }
+    }
+
+    private FileObject getFolder() throws IOException {
+        if (folder == null) {
+            Project p = file.getLookup().lookup(Project.class);
+            if (p != null) {
+                root = p.getProjectDirectory();
+
+                FileObject jmedataFolder = root.getFileObject("/nbproject/jme3Data");
+                if (jmedataFolder == null) {
+                    jmedataFolder = root.getFileObject("/nbproject");
+                    if(jmedataFolder!=null){
+                        jmedataFolder = jmedataFolder.createFolder("jme3Data");
+                    }
+                }
+                return jmedataFolder;
+            }
+        }
+        return folder;
+    }
+}

+ 820 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BTreeParser.java

@@ -0,0 +1,820 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees;
+
+import java.io.InputStream;
+import java.io.Reader;
+
+import com.badlogic.gdx.ai.GdxAI;
+import com.badlogic.gdx.ai.btree.BehaviorTree;
+import com.badlogic.gdx.ai.btree.Task;
+import com.badlogic.gdx.ai.btree.annotation.TaskAttribute;
+import com.badlogic.gdx.ai.btree.annotation.TaskConstraint;
+import com.badlogic.gdx.ai.btree.branch.DynamicGuardSelector;
+import com.badlogic.gdx.ai.btree.branch.Parallel;
+import com.badlogic.gdx.ai.btree.branch.RandomSelector;
+import com.badlogic.gdx.ai.btree.branch.RandomSequence;
+import com.badlogic.gdx.ai.btree.branch.Selector;
+import com.badlogic.gdx.ai.btree.branch.Sequence;
+import com.badlogic.gdx.ai.btree.decorator.AlwaysFail;
+import com.badlogic.gdx.ai.btree.decorator.AlwaysSucceed;
+import com.badlogic.gdx.ai.btree.decorator.Include;
+import com.badlogic.gdx.ai.btree.decorator.Invert;
+import com.badlogic.gdx.ai.btree.decorator.Random;
+import com.badlogic.gdx.ai.btree.decorator.Repeat;
+import com.badlogic.gdx.ai.btree.decorator.SemaphoreGuard;
+import com.badlogic.gdx.ai.btree.decorator.UntilFail;
+import com.badlogic.gdx.ai.btree.decorator.UntilSuccess;
+import com.badlogic.gdx.ai.btree.leaf.Failure;
+import com.badlogic.gdx.ai.btree.leaf.Success;
+import com.badlogic.gdx.ai.btree.leaf.Wait;
+import com.badlogic.gdx.ai.btree.utils.BehaviorTreeReader;
+import com.badlogic.gdx.ai.btree.utils.DistributionAdapters;
+import com.badlogic.gdx.ai.utils.random.Distribution;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.GdxRuntimeException;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.badlogic.gdx.utils.ObjectMap.Entries;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+import com.badlogic.gdx.utils.ObjectSet;
+import com.badlogic.gdx.utils.SerializationException;
+import com.badlogic.gdx.utils.reflect.Annotation;
+import com.badlogic.gdx.utils.reflect.ClassReflection;
+import com.badlogic.gdx.utils.reflect.Field;
+import com.badlogic.gdx.utils.reflect.ReflectionException;
+
+/**
+ *
+ * @author MeFisto94
+ * @param <E>
+ */
+public class BTreeParser<E> {
+    public static final int DEBUG_NONE = 0;
+    public static final int DEBUG_LOW = 1;
+    public static final int DEBUG_HIGH = 2;
+
+    private static final String TAG = "BTreeParser";
+
+    public int debugLevel;
+    public DistributionAdapters distributionAdapters;
+
+    private DefaultBehaviorTreeReader<E> btReader;
+    protected BTreeDataObject dataObj;
+
+    public BTreeParser (BTreeDataObject dataObj) {
+            this(dataObj, DEBUG_NONE);
+    }
+
+    public BTreeParser (BTreeDataObject dataObj, DistributionAdapters distributionAdapters) {
+            this(dataObj, distributionAdapters, DEBUG_NONE);
+    }
+
+    public BTreeParser (BTreeDataObject dataObj, int debugLevel) {
+            this(dataObj, new DistributionAdapters(), debugLevel);
+    }
+
+    public BTreeParser (BTreeDataObject dataObj, DistributionAdapters distributionAdapters, int debugLevel) {
+            this(dataObj, distributionAdapters, debugLevel, null);
+    }
+
+    public BTreeParser (BTreeDataObject dataObj, DistributionAdapters distributionAdapters, int debugLevel, DefaultBehaviorTreeReader<E> reader) {
+            this.distributionAdapters = distributionAdapters;
+            this.debugLevel = debugLevel;
+            btReader = reader == null ? new DefaultBehaviorTreeReader<>() : reader;
+            btReader.setParser(this);
+            this.dataObj = dataObj;
+    }
+
+    /** Parses the given string.
+     * @param string the string to parse
+     * @param object the blackboard object. It can be {@code null}.
+     * @return the behavior tree
+     * @throws SerializationException if the string cannot be successfully parsed. */
+    public BehaviorTree<E> parse (String string, E object) {
+            btReader.parse(string);
+            return createBehaviorTree(btReader.root, object);
+    }
+
+    /** Parses the given input stream.
+     * @param input the input stream to parse
+     * @param object the blackboard object. It can be {@code null}.
+     * @return the behavior tree
+     * @throws SerializationException if the input stream cannot be successfully parsed. */
+    public BehaviorTree<E> parse (InputStream input, E object) {
+            btReader.parse(input);
+            return createBehaviorTree(btReader.root, object);
+    }
+
+    /** Parses the given file.
+     * @param file the file to parse
+     * @param object the blackboard object. It can be {@code null}.
+     * @return the behavior tree
+     * @throws SerializationException if the file cannot be successfully parsed. */
+    public BehaviorTree<E> parse (FileHandle file, E object) {
+            btReader.parse(file);
+            return createBehaviorTree(btReader.root, object);
+    }
+
+    /** Parses the given reader.
+     * @param reader the reader to parse
+     * @param object the blackboard object. It can be {@code null}.
+     * @return the behavior tree
+     * @throws SerializationException if the reader cannot be successfully parsed. */
+    public BehaviorTree<E> parse (Reader reader, E object) {
+            btReader.parse(reader);
+            return createBehaviorTree(btReader.root, object);
+    }
+
+    protected BehaviorTree<E> createBehaviorTree (Task<E> root, E object) {
+            if (debugLevel > BTreeParser.DEBUG_LOW) printTree(root, 0);
+            return new BehaviorTree<>(root, object);
+    }
+
+    protected static <E> void printTree (Task<E> task, int indent) {
+            for (int i = 0; i < indent; i++)
+                    System.out.print(' ');
+            if (task.getGuard() != null) {
+                    System.out.println("Guard");
+                    indent = indent + 2;
+                    printTree(task.getGuard(), indent);
+                    for (int i = 0; i < indent; i++)
+                            System.out.print(' ');
+            }
+            System.out.println(task.getClass().getSimpleName() + "[" + task.toString() + "]");
+            for (int i = 0; i < task.getChildCount(); i++) {
+                    printTree(task.getChild(i), indent + 2);
+            }
+    }
+
+    public static class DefaultBehaviorTreeReader<E> extends BehaviorTreeReader {
+        private static final ObjectMap<String, String> DEFAULT_IMPORTS = new ObjectMap<String, String>();
+        static {
+                Class<?>[] classes = new Class<?>[] {// @off - disable libgdx formatter
+                        AlwaysFail.class,
+                        AlwaysSucceed.class,
+                        DynamicGuardSelector.class,
+                        Failure.class,
+                        Include.class,
+                        Invert.class,
+                        Parallel.class,
+                        Random.class,
+                        RandomSelector.class,
+                        RandomSequence.class,
+                        Repeat.class,
+                        Selector.class,
+                        SemaphoreGuard.class,
+                        Sequence.class,
+                        Success.class,
+                        UntilFail.class,
+                        UntilSuccess.class,
+                        Wait.class
+                }; // @on - enable libgdx formatter
+                for (Class<?> c : classes) {
+                        String fqcn = c.getName();
+                        String cn = c.getSimpleName();
+                        String alias = Character.toLowerCase(cn.charAt(0)) + (cn.length() > 1 ? cn.substring(1) : "");
+                        DEFAULT_IMPORTS.put(alias, fqcn);
+                }
+        }
+
+        enum Statement {
+                Import("import") {
+                        @Override
+                        protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
+                        }
+
+                        @Override
+                        protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
+                                if (!(value instanceof String)) reader.throwAttributeTypeException(this.name, name, "String");
+                                reader.addImport(name, (String)value);
+                                return true;
+                        }
+
+                        @Override
+                        protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
+                        }
+                },
+                Subtree("subtree") {
+                        @Override
+                        protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
+                        }
+
+                        @Override
+                        protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
+                                if (!name.equals("name")) reader.throwAttributeNameException(this.name, name, "name");
+                                if (!(value instanceof String)) reader.throwAttributeTypeException(this.name, name, "String");
+                                if ("".equals(value)) throw new GdxRuntimeException(this.name + ": the name connot be empty");
+                                if (reader.subtreeName != null)
+                                        throw new GdxRuntimeException(this.name + ": the name has been already specified");
+                                reader.subtreeName = (String)value;
+                                return true;
+                        }
+
+                        @Override
+                        protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
+                                if (reader.subtreeName == null)
+                                        throw new GdxRuntimeException(this.name + ": the name has not been specified");
+                                reader.switchToNewTree(reader.subtreeName);
+                                reader.subtreeName = null;
+                        }
+                },
+                Root("root") {
+                        @Override
+                        protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
+                                reader.subtreeName = ""; // the root tree has empty name
+                        }
+
+                        @Override
+                        protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
+                                reader.throwAttributeTypeException(this.name, name, null);
+                                return true;
+                        }
+
+                        @Override
+                        protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
+                                reader.switchToNewTree(reader.subtreeName);
+                                reader.subtreeName = null;
+                        }
+                },
+                TreeTask(null) {
+                        @Override
+                        protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
+                                // Root tree is the default one
+                                if (reader.currentTree == null) {
+                                        reader.switchToNewTree("");
+                                        reader.subtreeName = null;
+                                }
+
+                                reader.openTask(name, isGuard);
+                        }
+
+                        @Override
+                        protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
+                                StackedTask<E> stackedTask = reader.getCurrentTask();
+                                AttrInfo ai = stackedTask.metadata.attributes.get(name);
+                                if (ai == null) {
+                                    return false;
+                                }
+                                boolean isNew = reader.encounteredAttributes.add(name);
+                                if (!isNew) throw reader.stackedTaskException(stackedTask, "attribute '" + name + "' specified more than once");
+                                Field attributeField = reader.getField(stackedTask.task.getClass(), ai.fieldName);
+                                reader.setField(attributeField, stackedTask.task, value);
+                                return true;
+                        }
+
+                        @Override
+                        protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
+                                if (!reader.isSubtreeRef) {
+                                        reader.checkRequiredAttributes(reader.getCurrentTask());
+                                        reader.encounteredAttributes.clear();
+                                }
+                        }
+                };
+
+                String name;
+
+                Statement(String name) {
+                        this.name = name;
+                }
+
+                protected abstract <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard);
+                protected abstract <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value);
+                protected abstract <E> void exit (DefaultBehaviorTreeReader<E> reader);
+
+        } 
+
+        protected BTreeParser<E> btParser;
+
+        ObjectMap<Class<?>, Metadata> metadataCache = new ObjectMap<>();
+
+        Task<E> root;
+        String subtreeName;
+        Statement statement;
+        private int indent;
+
+        public DefaultBehaviorTreeReader () {
+                this(false);
+        }
+
+        public DefaultBehaviorTreeReader (boolean reportsComments) {
+                super(reportsComments);
+        }
+
+        public BTreeParser<E> getParser () {
+                return btParser;
+        }
+
+        public void setParser (BTreeParser<E> parser) {
+                this.btParser = parser;
+        }
+
+        @Override
+        public void parse (char[] data, int offset, int length) {
+                debug = btParser.debugLevel > BTreeParser.DEBUG_NONE;
+                root = null;
+                clear();
+                super.parse(data, offset, length);
+
+                // Pop all task from the stack and check their minimum number of children
+                popAndCheckMinChildren(0);
+
+                Subtree<E> rootTree = subtrees.get("");
+                if (rootTree == null) throw new GdxRuntimeException("Missing root tree");
+                root = rootTree.rootTask;
+                if (root == null) throw new GdxRuntimeException("The tree must have at least the root task");
+
+                clear();
+        }
+
+        @Override
+        protected void startLine (int indent) {
+                if (btParser.debugLevel > BTreeParser.DEBUG_LOW)
+                        GdxAI.getLogger().debug(TAG, lineNumber + ": <" + indent + ">");
+                this.indent = indent;
+        }
+
+        private Statement checkStatement (String name) {
+                if (name.equals(Statement.Import.name)) return Statement.Import;
+                if (name.equals(Statement.Subtree.name)) return Statement.Subtree;
+                if (name.equals(Statement.Root.name)) return Statement.Root;
+                return Statement.TreeTask;
+        }
+
+        @Override
+        protected void startStatement (String name, boolean isSubtreeReference, boolean isGuard) {
+                if (btParser.debugLevel > BTreeParser.DEBUG_LOW)
+                        GdxAI.getLogger().debug(TAG, (isGuard? " guard" : " task") + " name '" + name + "'");
+
+                this.isSubtreeRef = isSubtreeReference;
+
+                this.statement = isSubtreeReference ? Statement.TreeTask : checkStatement(name);
+                if (isGuard) {
+                        if (statement != Statement.TreeTask)
+                                throw new GdxRuntimeException(name + ": only tree's tasks can be guarded");
+                }
+
+                statement.enter(this, name, isGuard);
+        }
+
+        @Override
+        protected void attribute (String name, Object value) {
+                if (btParser.debugLevel > BTreeParser.DEBUG_LOW)
+                        GdxAI.getLogger().debug(TAG, lineNumber + ": attribute '" + name + " : " + value + "'");
+
+                boolean validAttribute = statement.attribute(this, name, value);
+                // Unfortunately since we operate on dummy tasks, we can't verify these
+                if (getCurrentTask() != null && !validAttribute) {
+                        if (statement == Statement.TreeTask) {
+                                throw stackedTaskException(getCurrentTask(), "unknown attribute '" + name + "'");
+                        } else {
+                                throw new GdxRuntimeException(statement.name + ": unknown attribute '" + name + "'");
+                        }
+                }
+        }
+
+        private Field getField (Class<?> clazz, String name) {
+                try {
+                        return ClassReflection.getField(clazz, name);
+                } catch (ReflectionException e) {
+                        throw new GdxRuntimeException(e);
+                }
+        }
+
+        private void setField (Field field, Task<E> task, Object value) {
+                field.setAccessible(true);
+                Object valueObject = castValue(field, value);
+                if (valueObject == null) throwAttributeTypeException(getCurrentTask().name, field.getName(), field.getType().getSimpleName());
+                try {
+                        field.set(task, valueObject);
+                } catch (ReflectionException e) {
+                        throw new GdxRuntimeException(e);
+                }
+        }
+
+        /**
+         * Convert serialized value to java value.
+         * Parsed value must be assignable to field argument.
+         * Subclasses may override this method to parse unsupported types.
+         * @param field task attribute field
+         * @param value unparsed value (can be Number, String or Boolean)
+         * @return parsed value or null if field type is not supported.
+         */
+        protected Object castValue (Field field, Object value) {
+                Class<?> type = field.getType();
+                Object ret = null;
+                if (value instanceof Number) {
+                        Number numberValue = (Number)value;
+                        if (type == int.class || type == Integer.class)
+                                ret = numberValue.intValue();
+                        else if (type == float.class || type == Float.class)
+                                ret = numberValue.floatValue();
+                        else if (type == long.class || type == Long.class)
+                                ret = numberValue.longValue();
+                        else if (type == double.class || type == Double.class)
+                                ret = numberValue.doubleValue();
+                        else if (type == short.class || type == Short.class)
+                                ret = numberValue.shortValue();
+                        else if (type == byte.class || type == Byte.class)
+                                ret = numberValue.byteValue();
+                        else if (ClassReflection.isAssignableFrom(Distribution.class, type)) {
+                                @SuppressWarnings("unchecked")
+                                Class<Distribution> distributionType = (Class<Distribution>)type;
+                                ret = btParser.distributionAdapters.toDistribution("constant," + numberValue, distributionType);
+                        }
+                } else if (value instanceof Boolean) {
+                        if (type == boolean.class || type == Boolean.class) ret = value;
+                } else if (value instanceof String) {
+                        String stringValue = (String)value;
+                        if (type == String.class)
+                                ret = value;
+                        else if (type == char.class || type == Character.class) {
+                                if (stringValue.length() != 1) throw new GdxRuntimeException("Invalid character '" + value + "'");
+                                ret = Character.valueOf(stringValue.charAt(0));
+                        } else if (ClassReflection.isAssignableFrom(Distribution.class, type)) {
+                                @SuppressWarnings("unchecked")
+                                Class<Distribution> distributionType = (Class<Distribution>)type;
+                                ret = btParser.distributionAdapters.toDistribution(stringValue, distributionType);
+                        } else if (ClassReflection.isAssignableFrom(Enum.class, type)) {
+                                Enum<?>[] constants = (Enum<?>[])type.getEnumConstants();
+                                for (int i = 0, n = constants.length; i < n; i++) {
+                                        Enum<?> e = constants[i];
+                                        if (e.name().equalsIgnoreCase(stringValue)) {
+                                                ret = e;
+                                                break;
+                                        }
+                                }
+                        }
+                }
+                return ret;
+        }
+
+        private void throwAttributeNameException (String statement, String name, String expectedName) {
+                String expected = " no attribute expected";
+                if (expectedName != null)
+                        expected = "expected '" + expectedName + "' instead";
+                throw new GdxRuntimeException(statement + ": attribute '" + name + "' unknown; " + expected);
+        }
+
+        private void throwAttributeTypeException (String statement, String name, String expectedType) {
+                throw new GdxRuntimeException(statement + ": attribute '" + name + "' must be of type " + expectedType);
+        }
+
+        @Override
+        protected void endLine () {
+        }
+
+        @Override
+        protected void endStatement () {
+                statement.exit(this);
+        }
+
+        private void openTask (String name, boolean isGuard) {
+            Task<E> task;
+            if (isSubtreeRef) {
+                    task = subtreeRootTaskInstance(name);
+            } else {
+                    String className = getImport(name);
+                    if (className == null) className = name;
+                    Class clazz = null;
+                    
+                    try {
+                        clazz = ClassReflection.forName(className);
+                    } catch (ReflectionException refl) { }
+                    
+                    for (ClassLoader cl: getParser().dataObj.manager.getClassLoaders()) {
+                        if (clazz == null) {
+                            try {
+                                clazz = cl.loadClass(className);
+                            } catch (ClassNotFoundException cnf) {}
+                        }
+                    }
+                    
+                    if (clazz == null) {
+                        throw new GdxRuntimeException("Task " + className + "'s class could not be found, be sure to add it to your project.");
+                    } else {
+                        try {
+                            @SuppressWarnings("unchecked")
+                            Task<E> tmpTask = (Task<E>)ClassReflection.newInstance(clazz);
+                            task = tmpTask;
+                        } catch (ReflectionException refl) {
+                            throw new GdxRuntimeException("Task " + className + "'s class could not be found, be sure to add it to your project.");
+                        }
+                    }
+            }
+
+            if (!currentTree.inited()) {
+                    initCurrentTree(task, indent);
+                    indent = 0;
+            } else if (!isGuard) {
+                    StackedTask<E> stackedTask = getPrevTask();
+
+                    indent -= currentTreeStartIndent;
+                    if (stackedTask.task == currentTree.rootTask) {
+                            step = indent;
+                    }
+                    if (indent > currentDepth) {
+                            stack.add(stackedTask); // push
+                    } else if (indent <= currentDepth) {
+                            // Pop tasks from the stack based on indentation
+                            // and check their minimum number of children
+                            int i = (currentDepth - indent) / step;
+                            popAndCheckMinChildren(stack.size - i);
+                    }
+
+                    // Check the max number of children of the parent
+                    StackedTask<E> stackedParent = stack.peek();
+                    int maxChildren = stackedParent.metadata.maxChildren;
+                    if (stackedParent.task.getChildCount() >= maxChildren)
+                            throw stackedTaskException(stackedParent, "max number of children exceeded ("
+                                    + (stackedParent.task.getChildCount() + 1) + " > " + maxChildren + ")");
+
+                    // Add child task to the parent
+                    stackedParent.task.addChild(task);
+            }
+            updateCurrentTask(createStackedTask(name, task), indent, isGuard);
+        }
+
+        private StackedTask<E> createStackedTask (String name, Task<E> task) {
+                Metadata metadata = findMetadata(task.getClass());
+                if (metadata == null)
+                        throw new GdxRuntimeException(name + ": @TaskConstraint annotation not found in '" + task.getClass().getSimpleName()
+                                + "' class hierarchy");
+                return new StackedTask<>(lineNumber, name, task, metadata);
+        }
+
+        private Metadata findMetadata (Class<?> clazz) {
+                Metadata metadata = metadataCache.get(clazz);
+                if (metadata == null) {
+                        Annotation tca = ClassReflection.getAnnotation(clazz, TaskConstraint.class);
+                        if (tca != null) {
+                                TaskConstraint taskConstraint = tca.getAnnotation(TaskConstraint.class);
+                                ObjectMap<String, AttrInfo> taskAttributes = new ObjectMap<>();
+                                Field[] fields = ClassReflection.getFields(clazz);
+                                for (Field f : fields) {
+                                        Annotation a = f.getDeclaredAnnotation(TaskAttribute.class);
+                                        if (a != null) {
+                                                AttrInfo ai = new AttrInfo(f.getName(), a.getAnnotation(TaskAttribute.class));
+                                                taskAttributes.put(ai.name, ai);
+                                        }
+                                }
+                                metadata = new Metadata(taskConstraint.minChildren(), taskConstraint.maxChildren(), taskAttributes);
+                                metadataCache.put(clazz, metadata);
+                        }
+                }
+                return metadata;
+        }
+
+        protected static class StackedTask<E> {
+                public int lineNumber;
+                public String name;
+                public Task<E> task;
+                public Metadata metadata;
+
+                StackedTask (int lineNumber, String name, Task<E> task, Metadata metadata) {
+                        this.lineNumber = lineNumber;
+                        this.name = name;
+                        this.task = task;
+                        this.metadata = metadata;
+                }
+        }
+
+        private static class Metadata {
+                int minChildren;
+                int maxChildren;
+                ObjectMap<String, AttrInfo> attributes;
+
+                /** Creates a {@code Metadata} for a task accepting from {@code minChildren} to {@code maxChildren} children and the given
+                 * attributes.
+                 * @param minChildren the minimum number of children (defaults to 0 if negative)
+                 * @param maxChildren the maximum number of children (defaults to {@link Integer.MAX_VALUE} if negative)
+                 * @param attributes the attributes */
+                Metadata (int minChildren, int maxChildren, ObjectMap<String, AttrInfo> attributes) {
+                        this.minChildren = minChildren < 0 ? 0 : minChildren;
+                        this.maxChildren = maxChildren < 0 ? Integer.MAX_VALUE : maxChildren;
+                        this.attributes = attributes;
+                }
+        }
+
+        private static class AttrInfo {
+                String name;
+                String fieldName;
+                boolean required;
+
+                AttrInfo (String fieldName, TaskAttribute annotation) {
+                        this(annotation.name(), fieldName, annotation.required());
+                }
+
+                AttrInfo (String name, String fieldName, boolean required) {
+                        this.name = name == null || name.length() == 0 ? fieldName : name;
+                        this.fieldName = fieldName;
+                        this.required = required;
+                }
+        }
+
+        protected static class Subtree<E> {
+                String name;  // root tree must have no name
+                Task<E> rootTask;
+                int referenceCount;
+
+                Subtree() {
+                        this(null);
+                }
+
+                Subtree(String name) {
+                        this.name = name;
+                        this.rootTask = null;
+                        this.referenceCount = 0;
+                }
+
+                public void init(Task<E> rootTask) {
+                        this.rootTask = rootTask;
+                }
+
+                public boolean inited() {
+                        return rootTask != null;
+                }
+
+                public boolean isRootTree() {
+                        return name == null || "".equals(name);
+                }
+
+                public Task<E> rootTaskInstance () {
+                        if (referenceCount++ == 0) {
+                                return rootTask;
+                        }
+                        return rootTask.cloneTask();
+                }
+        }
+
+        ObjectMap<String, String> userImports = new ObjectMap<>();
+
+        ObjectMap<String, Subtree<E>> subtrees = new ObjectMap<>();
+        Subtree<E> currentTree;
+
+        int currentTreeStartIndent;
+        int currentDepth;
+        int step;
+        boolean isSubtreeRef;
+        protected StackedTask<E> prevTask;
+        protected StackedTask<E> guardChain;
+        protected Array<StackedTask<E>> stack = new Array<>();
+        ObjectSet<String> encounteredAttributes = new ObjectSet<>();			
+        boolean isGuard;
+
+        StackedTask<E> getLastStackedTask() {
+                return stack.peek();
+        }
+
+        StackedTask<E> getPrevTask() {
+                return prevTask;
+        }
+
+        StackedTask<E> getCurrentTask() {
+                return isGuard? guardChain : prevTask;
+        }
+
+        void updateCurrentTask(StackedTask<E> stackedTask, int indent, boolean isGuard) {
+                this.isGuard = isGuard;
+                stackedTask.task.setGuard(guardChain == null ? null : guardChain.task);
+                if (isGuard) {
+                        guardChain = stackedTask;
+                }
+                else {
+                        prevTask = stackedTask;
+                        guardChain = null;
+                        currentDepth = indent;
+                }
+        }
+
+        void clear() {
+                prevTask = null;
+                guardChain = null;
+                currentTree = null;
+                userImports.clear();
+                subtrees.clear();
+                stack.clear();
+                encounteredAttributes.clear();
+        }
+
+        //
+        // Subtree
+        //
+
+        void switchToNewTree(String name) {
+                // Pop all task from the stack and check their minimum number of children
+                popAndCheckMinChildren(0);
+
+                this.currentTree = new Subtree<>(name);
+                Subtree<E> oldTree = subtrees.put(name, currentTree);
+                if (oldTree != null)
+                        throw new GdxRuntimeException("A subtree named '" + name + "' is already defined");
+        }
+
+        void initCurrentTree(Task<E> rootTask, int startIndent) {
+                currentDepth = -1;
+                step = 1;
+                currentTreeStartIndent = startIndent;
+                this.currentTree.init(rootTask);
+                prevTask = null;
+        }
+
+        Task<E> subtreeRootTaskInstance(String name) {
+                Subtree<E> tree = subtrees.get(name);
+                if (tree == null)
+                        throw new GdxRuntimeException("Undefined subtree with name '" + name + "'");
+                return tree.rootTaskInstance();
+        }
+
+        //
+        // Import
+        //
+
+        void addImport (String alias, String task) {
+                if (task == null) throw new GdxRuntimeException("import: missing task class name.");
+                if (alias == null) {
+                        Class<?> clazz = null;
+                        try {
+                                clazz = ClassReflection.forName(task);
+                        } catch (ReflectionException e) {
+                                throw new GdxRuntimeException("import: class not found '" + task + "'");
+                        }
+                        alias = clazz.getSimpleName();
+                }
+                String className = getImport(alias);
+                if (className != null) throw new GdxRuntimeException("import: alias '" + alias + "' previously defined already.");
+                userImports.put(alias, task);
+        }
+
+        String getImport (String as) {
+                String className = DEFAULT_IMPORTS.get(as);
+                return className != null ? className : userImports.get(as);
+        }
+
+        //
+        // Integrity checks
+        //
+
+        private void popAndCheckMinChildren (int upToFloor) {
+                // Check the minimum number of children in prevTask
+                if (prevTask != null) checkMinChildren(prevTask);
+
+                // Check the minimum number of children while popping up to the specified floor
+                while (stack.size > upToFloor) {
+                        StackedTask<E> stackedTask = stack.pop();
+                        checkMinChildren(stackedTask);
+                }
+        }
+
+        private void checkMinChildren (StackedTask<E> stackedTask) {
+                // Check the minimum number of children
+                int minChildren = stackedTask.metadata.minChildren;
+                if (stackedTask.task.getChildCount() < minChildren)
+                        throw stackedTaskException(stackedTask, "not enough children (" + stackedTask.task.getChildCount() + " < " + minChildren
+                                + ")");
+        }
+
+        private void checkRequiredAttributes (StackedTask<E> stackedTask) {
+                // Check the minimum number of children
+                Entries<String, AttrInfo> entries = stackedTask.metadata.attributes.iterator();
+                while (entries.hasNext()) {
+                        Entry<String, AttrInfo> entry = entries.next();
+                        if (entry.value.required && !encounteredAttributes.contains(entry.key))
+                                throw stackedTaskException(stackedTask, "missing required attribute '" + entry.key + "'");
+                }
+        }
+
+        private GdxRuntimeException stackedTaskException(StackedTask<E> stackedTask, String message) {
+                return new GdxRuntimeException(stackedTask.name + " at line " + stackedTask.lineNumber + ": " + message);
+        }
+    }
+}

+ 2 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/BehaviorTree.btree

@@ -0,0 +1,2 @@
+# Most minimal file to get started
+root

+ 7 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/Bundle.properties

@@ -0,0 +1,7 @@
+MatParamTopComponent.jScrollPane2.TabConstraints.tabTitle=Parameters
+MatParamTopComponent.jScrollPane3.TabConstraints.tabTitle=Render states
+MatParamTopComponent.jScrollPane3.TabConstraints.tabToolTip=Material parameters
+MatParamTopComponent.jScrollPane2.TabConstraints.tabToolTip=Additional render states
+OpenIDE-Module-Display-Category=jME3 - SDK Plugin
+OpenIDE-Module-Name=BehaviorTree Editor
+OpenIDE-Module-Short-Description=An Editor for libgdx-ai Behavior Trees

+ 192 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/DistributionStringConverter.java

@@ -0,0 +1,192 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees;
+
+import com.badlogic.gdx.ai.utils.random.ConstantDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.ConstantFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.ConstantIntegerDistribution;
+import com.badlogic.gdx.ai.utils.random.ConstantLongDistribution;
+import com.badlogic.gdx.ai.utils.random.Distribution;
+import com.badlogic.gdx.ai.utils.random.GaussianDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.GaussianFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularIntegerDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularLongDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformIntegerDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformLongDistribution;
+
+/**
+ * The Purpose of this class is to convert the many variants of Distributions
+ * to and from strings
+ * @author MeFisto94
+ */
+public class DistributionStringConverter {
+    
+    /* 
+    if (distribution instanceof UniformIntegerDistribution) {
+            UniformIntegerDistribution uid = (UniformIntegerDistribution)distribution;
+            return "uniform," + uid.getLow() + "," + uid.getHigh();
+    }
+    if (distribution instanceof UniformLongDistribution) {
+            UniformLongDistribution uld = (UniformLongDistribution)distribution;
+            return "uniform," + uld.getLow() + "," + uld.getHigh();
+    }
+    if (distribution instanceof UniformFloatDistribution) {
+            UniformFloatDistribution ufd = (UniformFloatDistribution)distribution;
+            return "uniform," + ufd.getLow() + "," + ufd.getHigh();
+    }
+    if (distribution instanceof UniformDoubleDistribution) {
+            UniformDoubleDistribution udd = (UniformDoubleDistribution)distribution;
+            return "uniform," + udd.getLow() + "," + udd.getHigh();
+    }
+    */
+    
+    public static String fromDistribution(ConstantIntegerDistribution cid) {
+        // Constant could be a short cut (leave out "constant", but we don't do that
+        return "constant," + cid.getValue();
+    }
+    
+    public static String fromDistribution(ConstantLongDistribution cld) {
+        return "constant," + cld.getValue();
+    }
+    
+    public static String fromDistribution(ConstantFloatDistribution cfd) {
+        return "constant," + cfd.getValue();
+    }
+    
+    public static String fromDistribution(ConstantDoubleDistribution cdd) {
+        return "constant," + cdd.getValue();
+    }
+
+    public static String fromDistribution(GaussianFloatDistribution gfd) {
+        return "gaussian," + gfd.getMean() + "," + gfd.getStandardDeviation();
+    }
+    
+    public static String fromDistribution(GaussianDoubleDistribution gdd) {
+        return "gaussian," + gdd.getMean() + "," + gdd.getStandardDeviation();
+    }
+    
+    public static String fromDistribution(TriangularIntegerDistribution tid) {
+        return "triangular," + tid.getLow() + "," + tid.getHigh() + "," + tid.getMode();
+    }
+    
+    public static String fromDistribution(TriangularLongDistribution tld) {
+        return "triangular," + tld.getLow() + "," + tld.getHigh() + "," + tld.getMode();
+    }
+
+    public static String fromDistribution(TriangularFloatDistribution tfd) {
+        return "triangular," + tfd.getLow() + "," + tfd.getHigh() + "," + tfd.getMode();
+    }
+    
+    public static String fromDistribution(TriangularDoubleDistribution tdd) {
+        return "triangular," + tdd.getLow() + "," + tdd.getHigh() + "," + tdd.getMode();
+    }
+    
+    public static String fromDistribution(UniformIntegerDistribution uid) {
+        return "uniform," + uid.getLow() + "," + uid.getHigh();
+    }
+    
+    public static String fromDistribution(UniformLongDistribution uld) {
+        return "uniform," + uld.getLow() + "," + uld.getHigh();
+    }
+    
+    public static String fromDistribution(UniformFloatDistribution ufd) {
+        return "uniform," + ufd.getLow() + "," + ufd.getHigh();
+    }
+    
+    public static String fromDistribution(UniformDoubleDistribution udd) {
+        return "uniform," + udd.getLow() + "," + udd.getHigh();
+    }
+
+    public static String fromDistribution(Distribution d) {
+        if (d instanceof ConstantIntegerDistribution) {
+            return fromDistribution((ConstantIntegerDistribution)d);
+        }
+        
+        if (d instanceof ConstantLongDistribution) {
+            return fromDistribution((ConstantLongDistribution)d);
+        }
+        
+        if (d instanceof ConstantFloatDistribution) {
+            return fromDistribution((ConstantFloatDistribution)d);
+        }
+        
+        if (d instanceof ConstantDoubleDistribution) {
+            return fromDistribution((ConstantDoubleDistribution)d);
+        }
+        
+        if (d instanceof GaussianFloatDistribution) {
+            return fromDistribution((GaussianFloatDistribution)d);
+        }
+        
+        if (d instanceof GaussianDoubleDistribution) {
+            return fromDistribution((GaussianDoubleDistribution)d);
+        }
+        
+        if (d instanceof TriangularIntegerDistribution) {
+            return fromDistribution((TriangularIntegerDistribution)d);
+        }
+        
+        if (d instanceof TriangularLongDistribution) {
+            return fromDistribution((TriangularLongDistribution)d);
+        }
+        
+        if (d instanceof TriangularFloatDistribution) {
+            return fromDistribution((TriangularFloatDistribution)d);
+        }
+        
+        if (d instanceof TriangularDoubleDistribution) {
+            return fromDistribution((TriangularDoubleDistribution)d);
+        }
+        
+        if (d instanceof UniformIntegerDistribution) {
+            return fromDistribution((UniformIntegerDistribution)d);
+        }
+        
+        if (d instanceof UniformLongDistribution) {
+            return fromDistribution((UniformLongDistribution)d);
+        }
+        
+        if (d instanceof UniformFloatDistribution) {
+            return fromDistribution((UniformFloatDistribution)d);
+        }
+        
+        if (d instanceof UniformDoubleDistribution) {
+            return fromDistribution((UniformDoubleDistribution)d);
+        }
+        
+        return null;
+    }
+}

+ 14 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/InputMappingBlock.java

@@ -0,0 +1,14 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.gde.behaviortrees;
+
+/**
+ *
+ * @author darki
+ */
+public class InputMappingBlock {
+    
+}

+ 14 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/OutputMappingBlock.java

@@ -0,0 +1,14 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.gde.behaviortrees;
+
+/**
+ *
+ * @author darki
+ */
+public class OutputMappingBlock {
+    
+}

+ 193 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/dialog/AddNodeDialog.form

@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.3" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JDialogFormInfo">
+  <Properties>
+    <Property name="defaultCloseOperation" type="int" value="2"/>
+    <Property name="title" type="java.lang.String" value="Add a Shader Node"/>
+    <Property name="modal" type="boolean" value="true"/>
+  </Properties>
+  <SyntheticProperties>
+    <SyntheticProperty name="formSizePolicy" type="int" value="1"/>
+    <SyntheticProperty name="generateCenter" type="boolean" value="false"/>
+  </SyntheticProperties>
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="1" attributes="0">
+          <Component id="jPanel2" max="32767" attributes="0"/>
+          <Component id="jSplitPane1" alignment="0" max="32767" attributes="0"/>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" alignment="0" attributes="0">
+              <Component id="jSplitPane1" max="32767" attributes="0"/>
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="jPanel2" min="-2" max="-2" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Container class="javax.swing.JPanel" name="jPanel2">
+
+      <Layout>
+        <DimensionLayout dim="0">
+          <Group type="103" groupAlignment="0" attributes="0">
+              <Group type="102" alignment="1" attributes="0">
+                  <EmptySpace max="-2" attributes="0"/>
+                  <Component id="btnCancel" min="-2" max="-2" attributes="0"/>
+                  <EmptySpace max="32767" attributes="0"/>
+                  <Component id="btnOk" min="-2" max="-2" attributes="0"/>
+                  <EmptySpace max="-2" attributes="0"/>
+              </Group>
+          </Group>
+        </DimensionLayout>
+        <DimensionLayout dim="1">
+          <Group type="103" groupAlignment="0" attributes="0">
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="btnOk" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="btnCancel" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+          </Group>
+        </DimensionLayout>
+      </Layout>
+      <SubComponents>
+        <Component class="javax.swing.JButton" name="btnOk">
+          <Properties>
+            <Property name="text" type="java.lang.String" value="Ok"/>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnOkActionPerformed"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JButton" name="btnCancel">
+          <Properties>
+            <Property name="mnemonic" type="int" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
+              <Connection code="KeyEvent.VK_ESCAPE" type="code"/>
+            </Property>
+            <Property name="text" type="java.lang.String" value="Cancel"/>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnCancelActionPerformed"/>
+          </Events>
+        </Component>
+      </SubComponents>
+    </Container>
+    <Container class="javax.swing.JSplitPane" name="jSplitPane1">
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout"/>
+      <SubComponents>
+        <Container class="javax.swing.JPanel" name="jPanel1">
+          <Properties>
+            <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
+              <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo">
+                <TitledBorder title="Node Overview"/>
+              </Border>
+            </Property>
+          </Properties>
+          <AccessibilityProperties>
+            <Property name="AccessibleContext.accessibleName" type="java.lang.String" value="Node Overview"/>
+          </AccessibilityProperties>
+          <Constraints>
+            <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout$JSplitPaneConstraintsDescription">
+              <JSplitPaneConstraints position="left"/>
+            </Constraint>
+          </Constraints>
+
+          <Layout>
+            <DimensionLayout dim="0">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="jScrollPane3" alignment="0" pref="236" max="32767" attributes="0"/>
+              </Group>
+            </DimensionLayout>
+            <DimensionLayout dim="1">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="jScrollPane3" alignment="0" pref="477" max="32767" attributes="0"/>
+              </Group>
+            </DimensionLayout>
+          </Layout>
+          <SubComponents>
+            <Container class="javax.swing.JScrollPane" name="jScrollPane3">
+              <AuxValues>
+                <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
+              </AuxValues>
+
+              <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
+              <SubComponents>
+                <Component class="javax.swing.JTree" name="jTree1">
+                </Component>
+              </SubComponents>
+            </Container>
+          </SubComponents>
+        </Container>
+        <Container class="javax.swing.JPanel" name="nodeListPanel">
+          <Properties>
+            <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
+              <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo">
+                <TitledBorder title="Select BehaviorTree Node"/>
+              </Border>
+            </Property>
+          </Properties>
+          <AccessibilityProperties>
+            <Property name="AccessibleContext.accessibleName" type="java.lang.String" value="Select BehaviorTree Node"/>
+          </AccessibilityProperties>
+          <Constraints>
+            <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout$JSplitPaneConstraintsDescription">
+              <JSplitPaneConstraints position="right"/>
+            </Constraint>
+          </Constraints>
+
+          <Layout>
+            <DimensionLayout dim="0">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Group type="102" alignment="0" attributes="0">
+                      <Component id="jScrollPane1" pref="327" max="32767" attributes="0"/>
+                      <EmptySpace max="-2" attributes="0"/>
+                  </Group>
+              </Group>
+            </DimensionLayout>
+            <DimensionLayout dim="1">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="jScrollPane1" alignment="0" pref="477" max="32767" attributes="0"/>
+              </Group>
+            </DimensionLayout>
+          </Layout>
+          <SubComponents>
+            <Container class="javax.swing.JScrollPane" name="jScrollPane1">
+              <AuxValues>
+                <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
+              </AuxValues>
+
+              <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
+              <SubComponents>
+                <Component class="javax.swing.JList" name="nodeList">
+                  <Properties>
+                    <Property name="model" type="javax.swing.ListModel" editor="org.netbeans.modules.form.editors2.ListModelEditor">
+                      <StringArray count="0"/>
+                    </Property>
+                    <Property name="selectionMode" type="int" value="0"/>
+                  </Properties>
+                  <AuxValues>
+                    <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="&lt;TaskWrapper&gt;"/>
+                  </AuxValues>
+                </Component>
+              </SubComponents>
+            </Container>
+          </SubComponents>
+        </Container>
+      </SubComponents>
+    </Container>
+  </SubComponents>
+</Form>

+ 424 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/dialog/AddNodeDialog.java

@@ -0,0 +1,424 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.dialog;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.badlogic.gdx.ai.btree.branch.DynamicGuardSelector;
+import com.badlogic.gdx.ai.btree.branch.Parallel;
+import com.badlogic.gdx.ai.btree.branch.RandomSelector;
+import com.badlogic.gdx.ai.btree.branch.RandomSequence;
+import com.badlogic.gdx.ai.btree.branch.Selector;
+import com.badlogic.gdx.ai.btree.branch.Sequence;
+import com.badlogic.gdx.ai.btree.decorator.AlwaysFail;
+import com.badlogic.gdx.ai.btree.decorator.AlwaysSucceed;
+import com.badlogic.gdx.ai.btree.decorator.Include;
+import com.badlogic.gdx.ai.btree.decorator.Invert;
+import com.badlogic.gdx.ai.btree.decorator.Random;
+import com.badlogic.gdx.ai.btree.decorator.Repeat;
+import com.badlogic.gdx.ai.btree.decorator.SemaphoreGuard;
+import com.badlogic.gdx.ai.btree.decorator.UntilFail;
+import com.badlogic.gdx.ai.btree.decorator.UntilSuccess;
+import com.badlogic.gdx.ai.btree.leaf.Failure;
+import com.badlogic.gdx.ai.btree.leaf.Success;
+import com.badlogic.gdx.ai.btree.leaf.Wait;
+import com.jme3.gde.behaviortrees.editor.BTreeNodeEditorElement;
+import com.jme3.gde.behaviortrees.editor.TreeDiagram;
+import com.jme3.gde.behaviortrees.editor.TreeNodePanel;
+import com.jme3.gde.core.assets.ProjectAssetManager;
+import com.jme3.gde.core.util.TreeUtil;
+import java.awt.Point;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import javax.lang.model.element.TypeElement;
+import javax.swing.DefaultListModel;
+import javax.swing.JDialog;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreeSelectionModel;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.project.JavaProjectConstants;
+import org.netbeans.api.java.source.ClassIndex;
+import org.netbeans.api.java.source.ClassIndex.NameKind;
+import org.netbeans.api.java.source.ClassIndex.SearchScope;
+import org.netbeans.api.java.source.ClasspathInfo;
+import org.netbeans.api.java.source.ElementHandle;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+
+/**
+ * This dialog borrows it's design from netbeans "New File" Dialogs, where you
+ * have a tree panel to the left with all the available categories and a list
+ * to the right with actual tasks (in this case)
+ * 
+ * @author MeFisto94
+ * @author Nehon
+ */
+public class AddNodeDialog extends JDialog {
+
+    private final TreeDiagram diagram;
+    private final Point clickPosition;
+    private final HashMap<String, TaskWrapper[]> pathContents;
+    private final ProjectAssetManager mgr;
+    
+    private final Class<?>[] builtinClasses = new Class<?>[] {
+        AlwaysFail.class,
+        AlwaysSucceed.class,
+        DynamicGuardSelector.class,
+        Failure.class,
+        Include.class,
+        Invert.class,
+        Parallel.class,
+        Random.class,
+        RandomSelector.class,
+        RandomSequence.class,
+        Repeat.class,
+        Selector.class,
+        SemaphoreGuard.class,
+        Sequence.class,
+        Success.class,
+        UntilFail.class,
+        UntilSuccess.class,
+        Wait.class
+    };
+
+    /**
+     * Creates new form AddNodeDialog
+     */
+    public AddNodeDialog(java.awt.Frame parent, boolean modal, ProjectAssetManager mgr, TreeDiagram diagram, Point clickPosition) {
+        super(parent, modal);
+        this.diagram = diagram;
+        this.clickPosition = clickPosition;
+        this.mgr = mgr;
+        pathContents = new HashMap<>();
+        initComponents();
+        fillList(mgr);
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        jPanel2 = new javax.swing.JPanel();
+        btnOk = new javax.swing.JButton();
+        btnCancel = new javax.swing.JButton();
+        jSplitPane1 = new javax.swing.JSplitPane();
+        jPanel1 = new javax.swing.JPanel();
+        jScrollPane3 = new javax.swing.JScrollPane();
+        jTree1 = new javax.swing.JTree();
+        nodeListPanel = new javax.swing.JPanel();
+        jScrollPane1 = new javax.swing.JScrollPane();
+        nodeList = new javax.swing.JList<>();
+
+        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
+        setTitle("Add a Shader Node");
+        setModal(true);
+
+        btnOk.setText("Ok");
+        btnOk.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                btnOkActionPerformed(evt);
+            }
+        });
+
+        btnCancel.setMnemonic(KeyEvent.VK_ESCAPE);
+        btnCancel.setText("Cancel");
+        btnCancel.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                btnCancelActionPerformed(evt);
+            }
+        });
+
+        javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
+        jPanel2.setLayout(jPanel2Layout);
+        jPanel2Layout.setHorizontalGroup(
+            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel2Layout.createSequentialGroup()
+                .addContainerGap()
+                .addComponent(btnCancel)
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+                .addComponent(btnOk)
+                .addContainerGap())
+        );
+        jPanel2Layout.setVerticalGroup(
+            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                .addComponent(btnOk)
+                .addComponent(btnCancel))
+        );
+
+        jPanel1.setBorder(javax.swing.BorderFactory.createTitledBorder("Node Overview"));
+
+        jScrollPane3.setViewportView(jTree1);
+
+        javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
+        jPanel1.setLayout(jPanel1Layout);
+        jPanel1Layout.setHorizontalGroup(
+            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(jScrollPane3, javax.swing.GroupLayout.DEFAULT_SIZE, 236, Short.MAX_VALUE)
+        );
+        jPanel1Layout.setVerticalGroup(
+            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(jScrollPane3, javax.swing.GroupLayout.DEFAULT_SIZE, 477, Short.MAX_VALUE)
+        );
+
+        jSplitPane1.setLeftComponent(jPanel1);
+        jPanel1.getAccessibleContext().setAccessibleName("Node Overview");
+
+        nodeListPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Select BehaviorTree Node"));
+
+        nodeList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
+        jScrollPane1.setViewportView(nodeList);
+
+        javax.swing.GroupLayout nodeListPanelLayout = new javax.swing.GroupLayout(nodeListPanel);
+        nodeListPanel.setLayout(nodeListPanelLayout);
+        nodeListPanelLayout.setHorizontalGroup(
+            nodeListPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(nodeListPanelLayout.createSequentialGroup()
+                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 327, Short.MAX_VALUE)
+                .addContainerGap())
+        );
+        nodeListPanelLayout.setVerticalGroup(
+            nodeListPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 477, Short.MAX_VALUE)
+        );
+
+        jSplitPane1.setRightComponent(nodeListPanel);
+        nodeListPanel.getAccessibleContext().setAccessibleName("Select BehaviorTree Node");
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
+        getContentPane().setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
+            .addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+            .addComponent(jSplitPane1, javax.swing.GroupLayout.Alignment.LEADING)
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addComponent(jSplitPane1)
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+        );
+
+        pack();
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void btnCancelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnCancelActionPerformed
+        setVisible(false);
+    }//GEN-LAST:event_btnCancelActionPerformed
+
+    private void btnOkActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnOkActionPerformed
+        setVisible(false);
+        
+        try {
+            Task task = nodeList.getSelectedValue().task.newInstance();
+            TreeNodePanel tnp = BTreeNodeEditorElement.taskToPanel(diagram, task, null);
+            diagram.addNode(tnp);
+            tnp.setLocation(clickPosition);
+            tnp.revalidate();
+            repaint();
+        } catch (IllegalAccessException | InstantiationException ex) {
+            
+        }
+    }//GEN-LAST:event_btnOkActionPerformed
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JButton btnCancel;
+    private javax.swing.JButton btnOk;
+    private javax.swing.JPanel jPanel1;
+    private javax.swing.JPanel jPanel2;
+    private javax.swing.JScrollPane jScrollPane1;
+    private javax.swing.JScrollPane jScrollPane3;
+    private javax.swing.JSplitPane jSplitPane1;
+    private javax.swing.JTree jTree1;
+    private javax.swing.JList<TaskWrapper> nodeList;
+    private javax.swing.JPanel nodeListPanel;
+    // End of variables declaration//GEN-END:variables
+
+    private void fillList(final ProjectAssetManager mgr) {
+        pathContents.put("Builtin/Branch", new TaskWrapper[] { 
+            new TaskWrapper(DynamicGuardSelector.class),
+            new TaskWrapper(Parallel.class), new TaskWrapper(RandomSelector.class),
+            new TaskWrapper(RandomSequence.class), new TaskWrapper(Selector.class),
+            new TaskWrapper(Sequence.class)
+        });
+        
+        pathContents.put("Builtin/Leaf", new TaskWrapper[] { 
+            new TaskWrapper(Failure.class), new TaskWrapper(Success.class),
+            new TaskWrapper(Wait.class) 
+        });
+
+        pathContents.put("Builtin/Decorator", new TaskWrapper[] {
+            new TaskWrapper(AlwaysFail.class), new TaskWrapper(AlwaysSucceed.class),
+            new TaskWrapper(Include.class), new TaskWrapper(Invert.class),
+            new TaskWrapper(Random.class), new TaskWrapper(Repeat.class),
+            new TaskWrapper(SemaphoreGuard.class), new TaskWrapper(UntilFail.class),
+            new TaskWrapper(UntilSuccess.class)
+        });
+        
+        List<Class> taskClasses = getSources();
+        taskClasses.forEach(c -> {
+            String path = "Custom Tasks/" + c.getPackage().getName();
+            if (pathContents.containsKey(path)) {
+                // this is a bit bad but if pathContents' values would've been lists,
+                // the builtin code would look worse.
+                ArrayList<TaskWrapper> list = new ArrayList<>(pathContents.get(path).length + 1);
+                list.addAll(Arrays.asList(pathContents.get(path)));
+                list.add(new TaskWrapper(c));
+                pathContents.put(path, list.toArray(new TaskWrapper[0]));
+            } else {
+            pathContents.put(path,
+                    new TaskWrapper[] { new TaskWrapper(c)});
+            }
+        });
+        
+        /*String[] leaves = new String[] { "Builtin/Branch", 
+            "Builtin/Decorator", "Builtin/Leaf", "Custom Tasks/Stupid Task 1"};*/
+        String[] leaves = pathContents.keySet().toArray(new String[0]);
+        TreeUtil.createTree(jTree1, leaves);
+        TreeUtil.expandTree(jTree1, (TreeNode) jTree1.getModel().getRoot(), 10);
+        
+        jTree1.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+        jTree1.addTreeSelectionListener((TreeSelectionEvent e) -> {
+            DefaultMutableTreeNode node = (DefaultMutableTreeNode)jTree1.getLastSelectedPathComponent();
+            
+            if (node == null) {
+                return;
+            }
+            
+            if (node.isLeaf()) {
+                //jTabbedPane1.removeAll();
+                String path = TreeUtil.getPath(node.getUserObjectPath());
+                path = path.substring(0, path.lastIndexOf("/"));
+                
+                TaskWrapper[] contents = pathContents.get(path);
+                if (contents != null) {
+                    DefaultListModel dlm = new DefaultListModel<>();
+                    for (TaskWrapper tw: contents) {
+                        dlm.addElement(tw);
+                    }
+                    nodeList.setModel(dlm);
+                } else {
+                    nodeList.setModel(new DefaultListModel<>());
+                }
+            }
+        });
+    }
+    
+    private Class findClass(String binaryName) {
+        for (ClassLoader cl: mgr.getClassLoaders()) {
+            try {
+                return cl.loadClass(binaryName);
+            } catch (ClassNotFoundException cnf) {
+                
+            }
+        }
+        
+        return null;
+    }
+    
+    private List<Class> getSources() {
+        // Code taken from jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/actions/impl/NewCustomControlVisualPanel1.java
+        Sources sources = mgr.getProject().getLookup().lookup(Sources.class);
+        final List<Class> list = new ArrayList<>();
+        if (sources != null) {
+            SourceGroup[] groups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
+            if (groups != null) {
+                for (SourceGroup sourceGroup : groups) {
+                    final ClasspathInfo cpInfo = ClasspathInfo.create(ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.BOOT),
+                            ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.COMPILE),
+                            ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.SOURCE));
+
+                    HashSet<SearchScope> set = new HashSet<>();
+                    set.add(ClassIndex.SearchScope.SOURCE);
+                    Set<ElementHandle<TypeElement>> types = cpInfo.getClassIndex().getDeclaredTypes("", NameKind.PREFIX, set);
+                    for (Iterator<ElementHandle<TypeElement>> it = types.iterator(); it.hasNext();) {
+                        final ElementHandle<TypeElement> elementHandle = it.next();
+                        // here we start to deviate from the approach for controls,
+                        // because the Generics in combination with openjdk8 (guessing)
+                        // lead to an error in the compiler
+                        // AND this code is much simpler anyway
+                        String bn = elementHandle.getBinaryName();
+                        Class c = findClass(bn);
+                        
+                        if (c == null || list.contains(c)) {
+                            continue;
+                        }
+                        
+                        if (Task.class.isAssignableFrom(c)) {
+                            list.add(c);
+                        }
+                    }
+                }
+            }
+        }
+        return list;
+    }
+    
+    private final class TaskWrapper {
+        Class<? extends Task> task;
+        String name;
+
+        public TaskWrapper(Class<? extends Task> task, String name) {
+            this.task = task;
+            this.name = name;
+        }
+        
+        public TaskWrapper(Class<? extends Task> task) {
+            this(task, task.getSimpleName());
+        }
+
+        public Class<? extends Task> getTask() {
+            return task;
+        }
+        
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String toString() {
+            return getName();
+        }
+    }
+}

+ 17 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/dialog/Bundle.properties

@@ -0,0 +1,17 @@
+
+AddMaterialParameterDialog.nameField.text=
+AddMaterialParameterDialog.jLabel3.text=Parameter Name
+AddMaterialParameterDialog.jLabel1.text=Parameter Type
+AddMaterialParameterDialog.title=Add Material Parameter
+AddMaterialParameterDialog.jButton1.text=Ok
+AddMaterialParameterDialog.jButton2.text=Cancel
+AddWorldParameterDialog.title=Add Material Parameter
+AddWorldParameterDialog.jButton1.text=Ok
+AddWorldParameterDialog.jButton2.text=Cancel
+AddWorldParameterDialog.jLabel1.text=Parameter Name
+AddAttributeDialog.jButton2.text=Cancel
+AddAttributeDialog.jLabel1.text=Attribute
+AddAttributeDialog.title=Add Material Parameter
+AddAttributeDialog.jButton1.text=Ok
+AddAttributeDialog.jLabel2.text=GLSL Type
+AddAttributeDialog.typeField.text=

+ 62 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/BTreeNodeEditorElement.form

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.9" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Component id="jScrollPane1" alignment="0" pref="470" max="32767" attributes="0"/>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Component id="jScrollPane1" alignment="1" pref="333" max="32767" attributes="0"/>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Container class="javax.swing.JScrollPane" name="jScrollPane1">
+      <Properties>
+        <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[0, 0]"/>
+        </Property>
+        <Property name="name" type="java.lang.String" value="" noResource="true"/>
+      </Properties>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
+      <SubComponents>
+        <Container class="com.jme3.gde.behaviortrees.editor.TreeDiagram" name="diagram1">
+          <Properties>
+            <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor">
+              <Color blue="1d" green="1d" red="1d" type="rgb"/>
+            </Property>
+          </Properties>
+
+          <Layout>
+            <DimensionLayout dim="0">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <EmptySpace min="0" pref="1591" max="32767" attributes="0"/>
+              </Group>
+            </DimensionLayout>
+            <DimensionLayout dim="1">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <EmptySpace min="0" pref="782" max="32767" attributes="0"/>
+              </Group>
+            </DimensionLayout>
+          </Layout>
+        </Container>
+      </SubComponents>
+    </Container>
+  </SubComponents>
+</Form>

+ 582 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/BTreeNodeEditorElement.java

@@ -0,0 +1,582 @@
+/*
+ * Copyright (c) 2009-2018 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.editor;
+
+import com.badlogic.gdx.ai.btree.BehaviorTree;
+import com.badlogic.gdx.ai.btree.BranchTask;
+import com.badlogic.gdx.ai.btree.Decorator;
+import com.badlogic.gdx.ai.btree.LeafTask;
+import com.badlogic.gdx.ai.btree.Task;
+import com.badlogic.gdx.ai.btree.branch.Parallel;
+import com.badlogic.gdx.ai.btree.branch.Selector;
+import com.badlogic.gdx.ai.btree.branch.Sequence;
+import com.badlogic.gdx.ai.btree.leaf.Failure;
+import com.badlogic.gdx.ai.btree.leaf.Success;
+import com.badlogic.gdx.ai.btree.leaf.Wait;
+import com.badlogic.gdx.utils.Array;
+import com.jme3.gde.behaviortrees.BTreeDataObject;
+import com.jme3.gde.behaviortrees.BTreeMetaData;
+import com.jme3.gde.behaviortrees.navigator.BTreeNavigatorPanel;
+import com.jme3.gde.behaviortrees.navigator.nodes.TreeNodePanelNode;
+import com.jme3.gde.behaviortrees.nodes.LeafTreeNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.RootNodePanel;
+import com.jme3.gde.behaviortrees.nodes.SequentialNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.DecoratorNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.ParallelNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.SelectorNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.SequenceNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.BuiltinLeafTask;
+import com.jme3.gde.core.editor.nodes.Connection;
+import com.jme3.gde.core.editor.nodes.NodeEditor;
+import com.jme3.gde.core.editor.nodes.Diagram;
+import com.jme3.gde.core.editor.nodes.NodePanel;
+import com.jme3.gde.core.editor.nodes.Selectable;
+import com.jme3.gde.core.assets.ProjectAssetManager;
+import com.jme3.gde.core.errorreport.ExceptionUtils;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.beans.PropertyVetoException;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+import javax.swing.Action;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import org.netbeans.core.spi.multiview.CloseOperationState;
+import org.netbeans.core.spi.multiview.MultiViewElement;
+import org.netbeans.core.spi.multiview.MultiViewElementCallback;
+import org.netbeans.core.spi.multiview.text.MultiViewEditorElement;
+import org.openide.awt.UndoRedo;
+import org.openide.cookies.EditorCookie;
+import org.openide.filesystems.FileChangeAdapter;
+import org.openide.filesystems.FileEvent;
+import org.openide.nodes.Children;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.NbBundle.Messages;
+import org.openide.util.lookup.InstanceContent;
+import org.openide.windows.TopComponent;
+
+// Register the NodeEditor
[email protected](
+        displayName = "#LBL_BTREE_EDITOR",
+        iconBase = "com/jme3/gde/core/editor/icons/matdef.png",
+        mimeType = "text/gdx-ai-btree",
+        persistenceType = TopComponent.PERSISTENCE_ONLY_OPENED,
+        preferredID = "BehaviorTree",
+        position = 2000)
+@Messages("LBL_BTREE_EDITOR=Nodes")
+public final class BTreeNodeEditorElement extends JPanel implements 
+        MultiViewElement, NodeEditor {
+    protected BTreeDataObject obj;
+    protected Lookup lkp;
+    //private final MatDefEditorToolBar toolbar = new MatDefEditorToolBar();
+    private transient MultiViewElementCallback callback;
+    InstanceContent content;
+    Selectable prevNode;
+    BTreeMetaData metaData;
+
+    @SuppressWarnings("LeakingThisInConstructor")
+    public BTreeNodeEditorElement(final Lookup lkp) {
+        content = new InstanceContent();
+        this.lkp = lkp;
+        initComponents();
+        obj = lkp.lookup(BTreeDataObject.class);
+        metaData = lkp.lookup(BTreeMetaData.class);
+        if (obj == null) { // This happens when there was an error or maybe the object
+            // has already been freed
+            throw new IllegalArgumentException("Cannot build MatDefEditorlElement: obj null");
+        }
+        
+        obj.setTopComponent(this);
+        initDiagram(lkp, false);
+        BTreeNavigatorPanel nav = new BTreeNavigatorPanel();
+        obj.getLookupContents().add(nav);
+        nav.updateData(obj);
+        
+        obj.getPrimaryFile().addFileChangeListener(new FileChangeAdapter() {
+            @Override
+            public void fileChanged(FileEvent fe) {
+                refresh();
+            }
+        });
+    }
+
+    private void initDiagram(Lookup lkp, boolean saveOrphanedNodes) throws NumberFormatException {
+        if (!obj.isLoaded()) {
+            return;
+        }
+        
+        List<TreeNodePanel> orphans = null;
+        
+        if (saveOrphanedNodes) {
+            orphans = diagram1.getOrphanedNodes();
+        }
+        
+        diagram1.clear();
+        diagram1.setEditorParent(this);
+        //diagram1.setPreferredSize(new Dimension(jScrollPane1.getWidth() - 2, jScrollPane1.getHeight() - 2));
+        diagram1.setPreferredSize(new Dimension(-1, -1));
+        
+        BehaviorTree bTree = obj.getMainTree();
+        RootNodePanel rnp = new RootNodePanel(diagram1, bTree);
+        diagram1.addNode(rnp);
+        obj.setRootNodePanel(rnp);
+        
+        if (bTree.getChildCount() > 1) {
+            throw new IllegalStateException("There can only be one child of the rootNode!");
+        }
+        
+        if (bTree.getChildCount() == 1) {
+            TreeNodePanel childPanel = loadFromTree(rnp, bTree.getChild(0));
+            diagram1.connect(rnp.getOutputByIndex(0), childPanel.getInputByIndex(0));
+        }
+        
+        if (saveOrphanedNodes) {
+            diagram1.restoreOrphanedNodes(orphans);
+        }
+        
+        autoLayout();
+        diagram1.setSize(diagram1.maxWidth, diagram1.getPreferredSize().height);
+        diagram1.revalidate();        
+        diagram1.repaint();
+        jScrollPane1.addComponentListener(diagram1);
+        jScrollPane1.setSize(diagram1.getSize());
+        jScrollPane1.revalidate();
+        jScrollPane1.repaint();
+        // For some reason the scroll bars are only correct on the second time we're here
+        /*diagram1.clear();
+        JLabel error = new JLabel("<html><center>Cannot load material definition.<br>Please see the error log and fix it in the text editor</center></html>");
+        error.setForeground(Color.ORANGE);
+        error.setFont(new Font("Arial", Font.BOLD, 24));
+        error.setBounds(0, 0, 400, 100);
+        jScrollPane1.getHorizontalScrollBar().setValue(0);
+        error.setLocation(Math.max(jScrollPane1.getViewport().getWidth() / 2 - 200, 0), Math.max(jScrollPane1.getViewport().getHeight() / 2 - 50, 0));
+        diagram1.add(error);
+        diagram1.repaint();*/
+    }
+    
+    protected TreeNodePanel loadFromTree(TreeNodePanel parent, Task task) {
+        TreeNodePanel panel;
+        
+        panel = taskToPanel(diagram1, task, null);
+        diagram1.addNode(panel);
+        
+        for (int i = 0; i < task.getChildCount(); i++) {
+            TreeNodePanel childPanel = loadFromTree(panel, task.getChild(i));
+            if (panel instanceof SequentialNodePanel) {
+                ((SequentialNodePanel)panel).setDynamic(false);
+                diagram1.connect(panel.getOutputByIndex(i), childPanel.getInputByIndex(0));
+                ((SequentialNodePanel)panel).setDynamic(true);
+            } else if (panel instanceof DecoratorNodePanel) {
+                if (i == 0) {
+                    diagram1.connect(panel.getOutputByIndex(0), childPanel.getInputByIndex(0));
+                } else {
+                    throw new IllegalStateException("A Decorator cannot have more than 1 child");
+                }
+            } else {
+                diagram1.connect(panel.createOutput(true), childPanel.getInputByIndex(0));
+            }
+        }
+        
+        return panel;
+    }
+    
+    /**
+     * Convert a BTree Task into a TreeNodePanel.
+     * @param diagram1 The Diagram to attach it to.
+     * @param task The Task
+     * @param guard If this Panel represents a Guard Task or not
+     * @return The Panel
+     */
+    public static TreeNodePanel taskToPanel(Diagram diagram1, Task task, TreeNodePanel guardedPanel) {
+        TreeNodePanel panel;
+        
+        /* Handle the Panels from specific to generic: First specific implementations
+         * of BranchTask (Parallel) and then catch-all. Same with Wait vs LeafTask
+        */
+        
+        if (task instanceof Sequence) {
+            // for the following tasks: + 1 so that the user can add new paths
+            panel = new SequenceNodePanel(diagram1, task, task.getChildCount() + 1);
+        } else if (task instanceof Selector) {
+            panel = new SelectorNodePanel(diagram1, task, task.getChildCount() + 1);
+        } else if (task instanceof Parallel) {
+            panel = new ParallelNodePanel(diagram1, task, task.getChildCount() + 1);
+        } else if (BranchTask.class.isAssignableFrom(task.getClass())) {
+            panel = new SequentialNodePanel(diagram1, task, task.getChildCount() + 1);
+        } else if (Decorator.class.isAssignableFrom(task.getClass())) {
+            panel = new DecoratorNodePanel(diagram1, task);
+        } else if (task instanceof Wait || task instanceof Success ||
+                task instanceof Failure) {
+            // Unfortunately this could come out of sync with libgdx some day,
+            // but isAssignableFrom also triggers usercode tasks
+            panel = new BuiltinLeafTask(diagram1, task);
+        } else {
+            if (guardedPanel != null) {
+                panel = new LeafTreeNodePanel(diagram1, task, task.getClass().getPackage().getName(), task.getClass().getSimpleName(), guardedPanel);
+            } else {
+                panel = new LeafTreeNodePanel(diagram1, task, task.getClass().getPackage().getName(), task.getClass().getSimpleName());
+            }
+        }
+        
+        return panel;
+    }
+
+    /*public void switchTechnique(TechniqueBlock tech) {
+        obj.getEditableFile().setCurrentTechnique(tech);        
+        reload(obj.getEditableFile(), obj.getLookup());
+    }*/
+
+    public Diagram getDiagram() {
+        return diagram1;
+    }
+
+    @Override
+    public String getName() {
+        return "MatDefVisualElement";
+    }
+
+    /**
+     * Called FROM the Diagram when something has been selected and that change
+     * has to be propagated to the Navigtor View. Unfortunately this loses the
+     * capability of multiple selections atm.
+     * @param selectable 
+     */
+    @Override
+    public void selectionChanged(Selectable selectable) {
+        BTreeNavigatorPanel nav = obj.getLookup().lookup(BTreeNavigatorPanel.class);
+        //It's possible that the navigator is null if it's collapsed in the ui.
+        //In that case we early return to avoid further issues
+        if (nav == null){
+            return;
+        }
+        
+        Node n = findNode(nav.getExplorerManager().getRootContext(), selectable);
+        if (n == null) {
+            n = nav.getExplorerManager().getRootContext();
+        }
+        prevNode = selectable;
+        
+        try {
+            nav.getExplorerManager().setSelectedNodes(new Node[]{n});
+        } catch (PropertyVetoException veto) {
+            
+        }
+        //FIXME this is hackish, each time it's used it spits a warning in the log.
+        //without this line selecting a node in the editor select it in
+        //the navigator explorer but does not displays its property sheet.
+        //the warning says to manipulate the MultiViewElement lookup, but it just voids the tree view
+        callback.getTopComponent().setActivatedNodes(new Node[]{n});
+        
+        /* 
+         * @TODO: Comment MeFisto94 11.12.2018 Right now it looks like it's the
+         * opposite: The Properties show up, but the Navigator Selection isn't
+         * set anymore.
+         */
+    }
+    
+    /**
+     * Called FROM the Navigator when something has changed and we shall 
+     * reflect that in the diagram
+     * @param nodes 
+     */
+    public void selectionChanged(Node[] nodes) {
+        diagram1.select("", false); // Trigger Clearing of Multi Select
+        for (Node n: nodes) {
+            diagram1.select(((TreeNodePanelNode)n).getPanel(), true);
+        }
+    }
+    
+    public void refreshNavigator(boolean dontRefreshTheDiagram) {
+        if (!dontRefreshTheDiagram) {
+            refreshDiagram();
+        }
+        
+        BTreeNavigatorPanel nav = obj.getLookup().lookup(BTreeNavigatorPanel.class);
+        nav.updateData(obj);
+    }
+    
+    public void refreshDiagram() {
+        // we need to keep/handle orphaned nodes.
+        initDiagram(lkp, true);
+        setSize(getPreferredSize());
+    }
+
+    public void refresh() {
+        obj.refresh(); // Re-Parse the tree first
+        refreshNavigator(false); // implicitly calls refreshDiagram(), as the nav
+        // is built upon diagram nodes
+    }
+
+    public void setModified() {
+        obj.setDirty();
+    }
+
+    public ProjectAssetManager getAssetManager() {
+        return obj.getLookup().lookup(ProjectAssetManager.class);
+    }
+
+    private Node findNode(Node root, Selectable select) {
+        if (!(select instanceof Selectable) || !(root instanceof TreeNodePanelNode)) {
+            return null;
+        }
+        
+        TreeNodePanelNode panelNode = (TreeNodePanelNode)root;
+        
+        if (panelNode.getPanel().equals(select)) {
+            return panelNode;
+        } else if (panelNode.getChildren() != Children.LEAF) {
+            Node n;
+            for (Node node: panelNode.getChildren().getNodes()) {
+                n = findNode(node, select);
+                if (n != null) {
+                    return n;
+                }
+            }
+        }
+        
+        return null;
+    }
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        jScrollPane1 = new javax.swing.JScrollPane();
+        diagram1 = new com.jme3.gde.behaviortrees.editor.TreeDiagram();
+
+        jScrollPane1.setMinimumSize(new java.awt.Dimension(0, 0));
+        jScrollPane1.setName(""); // NOI18N
+
+        diagram1.setBackground(new java.awt.Color(29, 29, 29));
+
+        javax.swing.GroupLayout diagram1Layout = new javax.swing.GroupLayout(diagram1);
+        diagram1.setLayout(diagram1Layout);
+        diagram1Layout.setHorizontalGroup(
+            diagram1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 1591, Short.MAX_VALUE)
+        );
+        diagram1Layout.setVerticalGroup(
+            diagram1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 782, Short.MAX_VALUE)
+        );
+
+        jScrollPane1.setViewportView(diagram1);
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 470, Short.MAX_VALUE)
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 333, Short.MAX_VALUE)
+        );
+    }// </editor-fold>//GEN-END:initComponents
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private com.jme3.gde.behaviortrees.editor.TreeDiagram diagram1;
+    private javax.swing.JScrollPane jScrollPane1;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public JComponent getVisualRepresentation() {
+        return this;
+    }
+
+    @Override
+    public JComponent getToolbarRepresentation() {
+        return null; //@TODO: FIXME
+    }
+
+    @Override
+    public Action[] getActions() {
+        return new Action[0];
+    }
+
+    @Override
+    public Lookup getLookup() {
+        return obj.getLookup();
+    }
+
+    @Override
+    public UndoRedo getUndoRedo() {
+        if (getLookup().lookup(MultiViewEditorElement.class) != null) {
+            return obj.getLookup().lookup(MultiViewEditorElement.class).getUndoRedo();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void setMultiViewCallback(MultiViewElementCallback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    public CloseOperationState canCloseElement() {
+        return CloseOperationState.STATE_OK;
+    }
+    
+    private static Field getField(Class clazz, String fieldName)
+        throws NoSuchFieldException {
+    try {
+      return clazz.getDeclaredField(fieldName);
+    } catch (NoSuchFieldException e) {
+      Class superClass = clazz.getSuperclass();
+      if (superClass == null) {
+        throw e;
+      } else {
+        return getField(superClass, fieldName);
+      }
+    }
+  }
+
+    @Override
+    public void makeMapping(Connection conn) {
+        TreeNodePanel start = (TreeNodePanel)conn.getStart().getNode();
+        TreeNodePanel end = (TreeNodePanel)conn.getEnd().getNode();
+        
+        if (!(start.getTask() instanceof LeafTask)) {
+            start.getTask().addChild(end.getTask());
+        }
+        
+        setModified();
+    }
+
+    @Override
+    public void notifyRemoveConnection(Connection conn) {
+        TreeNodePanel start = (TreeNodePanel)conn.getStart().getNode();
+        TreeNodePanel end = (TreeNodePanel)conn.getEnd().getNode();
+        
+        // Unfortunately we need reflection, just hope it works for every subclass
+        // for now it works. start.getTask().removeChild(end.getTask());
+        
+        try {
+            if (!(start.getTask() instanceof LeafTask)) {
+                if (start.getTask() instanceof BehaviorTree) {
+                    Field root = getField(BehaviorTree.class, "rootTask");
+                    root.setAccessible(true);
+                    root.set(start.getTask(), null);
+                } else if (start.getTask() instanceof BranchTask) {
+                    Field children = getField(BranchTask.class, "children");
+                    children.setAccessible(true);
+                    Array<Task> arr = (Array<Task>)children.get(start.getTask());
+                    arr.removeValue(end.getTask(), true); // true -> == over .equals
+                } else if (start.getTask() instanceof Decorator) {
+                    Field child = getField(Decorator.class, "child");
+                    child.setAccessible(true);
+                    child.set(start.getTask(), null);
+                }
+            }
+        } catch (Exception e) {
+            ExceptionUtils.caughtException(e, "Unable to use Reflection based hacks"
+                    + " to remove the connection in the behavior tree. Maybe"
+                    + " something changed in the LibGDX-AI API?");
+        }
+        setModified();
+    }
+
+    public void autoLayout(){
+        diagram1.autoLayout();
+    }
+    
+    @Override
+    public void notifyRemoveNode(NodePanel node) {
+        /* We don't hook into remove/add nodes but instead handle that by the
+         * connection (a node is just not in the tree when it's not child of
+         * someone).
+        */
+    }
+    
+    public void onAttachGuard(TreeNodePanel guardedPanel, TreeNodePanel guard) {
+        guardedPanel.getTask().setGuard(guard.getTask());
+        setModified();
+    }
+    
+    public void onDetachGuard(TreeNodePanel guardedPanel, TreeNodePanel guard) {
+        guardedPanel.getTask().setGuard(null);
+        setModified();
+    }
+
+    @Override
+    public Point getPositionFromMetaData(String key, int defaultx, int defaulty) throws NumberFormatException {
+        Point position = new Point();
+        String pos = metaData.getProperty("blah" + "/" + key, defaultx + "," + defaulty);
+
+        if (pos != null) {
+            String[] s = pos.split(",");
+            position.x = Integer.parseInt(s[0]);
+            position.y = Integer.parseInt(s[1]);
+        }
+        return position;
+    }
+
+    @Override
+    public void savePositionToMetaData(String key, int x, int y) throws NumberFormatException {
+        metaData.setProperty("blah" + "/" + key, x + "," + y);
+    }
+
+    public void reload() {
+        try {
+            obj.getLookup().lookup(EditorCookie.class).saveDocument();
+            /*obj.getEditableFile().load(obj.getLookup());
+            reload(obj.getEditableFile(), obj.getLookup());*/
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public void componentOpened() { }
+
+    @Override
+    public void componentClosed() { }
+
+    @Override
+    public void componentShowing() { }
+
+    @Override
+    public void componentHidden() { }
+
+    @Override
+    public void componentActivated() { }
+
+    @Override
+    public void componentDeactivated() { }
+    
+}

+ 21 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/Bundle.properties

@@ -0,0 +1,21 @@
+ShaderEditPanel.closeButton.text=
+ShaderEditPanel.shaderEditorPane.text=
+ShaderEditPanel.closeButton.toolTipText=Close this panel
+ShaderEditPanel.jToggleButton1.text=jToggleButton1
+ShaderEditPanel.headerText.text=jLabel1
+BackdropPanel.sphereButton.toolTipText=Sphere
+BackdropPanel.expandButton.toolTipText=Hide Backdrop
+BackdropPanel.quadButton.toolTipText=Quad
+BackdropPanel.reloadButton.toolTipText=Refresh
+BackdropPanel.boxButton.toolTipText=Cube
+BackdropPanel.bringToFrontButton.toolTipText=Toggle back/front
+MatDefEditorToolBar.jLabel1.text=Technique
+MatDefEditorToolBar.jButton1.text=Add
+MatDefEditorToolBar.jButton1.toolTipText=Add a new technique
+MatDefEditorToolBar.jButton2.text=Auto layout
+ShaderNodeToolBar.deleteButton.toolTipText=Delete node
+ShaderNodeToolBar.deleteButton.text=
+ShaderNodeToolBar.codeButton.toolTipText=Display code
+NodeToolBar.deleteButton.toolTipText=Delete node
+NodeToolBar.deleteButton.text=
+NodeToolBar.codeButton.toolTipText=Display code

+ 52 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/InOut.java

@@ -0,0 +1,52 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.editor;
+
+import com.jme3.gde.behaviortrees.InputMappingBlock;
+import com.jme3.gde.behaviortrees.OutputMappingBlock;
+
+/**
+ *
+ * @author Nehon
+ */
+public interface InOut {
+
+    public String getName();
+
+    public void addInputMapping(InputMappingBlock block);
+
+    public void removeInputMapping(InputMappingBlock block);
+
+    public void addOutputMapping(OutputMappingBlock block);
+
+    public void removeOutputMapping(OutputMappingBlock block);
+}

+ 97 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/TreeConnectionEndpoint.java

@@ -0,0 +1,97 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.editor;
+
+import com.jme3.gde.core.editor.nodes.ConnectionEndpoint;
+import com.jme3.gde.core.editor.icons.Icons;
+
+/**
+ * This is the input/ouput connector of a ShaderNode.
+ * @author MeFisto94
+ */
+public class TreeConnectionEndpoint extends ConnectionEndpoint {
+    
+    @Override
+    public boolean canConnect(ConnectionEndpoint pair) {
+        // Feature: The Direction is clear here so we can drag them from both sides.
+        // Note to myself: Connections are always formed FROM output TO Input
+        if (pair != null) {
+            if (paramType == ParamType.Input && 
+                    (pair.getParamType() == ParamType.Output ||
+                        pair.getParamType() == ParamType.Both)) {
+                return pair.canConnect(this); // Invert, always call it from Output to Input
+            }
+        }
+        
+        // cannot connect to: nothing || from input panels || to output
+        if (pair == null || getParamType() == ParamType.Input ||
+                pair.getParamType() == ParamType.Output) {
+            setIcon(Icons.imgOrange);
+            return false;
+        } else if (allowConnection(pair)) {
+            setIcon(Icons.imgGreen);
+            return true;
+        } else {
+            setIcon(Icons.imgRed);
+            return false;
+        }
+    }
+    
+    @Override
+    protected boolean allowConnection(ConnectionEndpoint pair) {
+        return !isConnected();
+        // @TODO: Prevent circular connection, that means if pair.getNode()
+        // appears somewhere in "getConnectionsTo(this).getParent().getParent()...."
+        //@TODO: Another feature is swapping: just exchange the froms of both connections
+        // maybe with a confirmation dialog? and or a setting? Or Shift?
+    }
+    
+    /**
+     * This method checks if any of the both strings contain the same substring
+     * seperated by \|
+     * @param type1 The Typelist 1
+     * @param type2 The Typelist 2
+     * @return whether they contain the same substring
+     */
+    private boolean matches(String type1, String type2) {
+        String[] s1 = type1.split("\\|");
+        String[] s2 = type2.split("\\|");
+        for (String string : s1) {
+            for (String string1 : s2) {
+                if (string.equals(string1)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}

+ 284 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/TreeDiagram.java

@@ -0,0 +1,284 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.editor;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.badlogic.gdx.ai.btree.branch.Parallel;
+import com.badlogic.gdx.ai.btree.branch.Selector;
+import com.badlogic.gdx.ai.btree.decorator.Invert;
+import com.jme3.gde.behaviortrees.dialog.AddNodeDialog;
+import com.jme3.gde.behaviortrees.nodes.LeafTreeNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.DecoratorNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.ParallelNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.RootNodePanel;
+import com.jme3.gde.behaviortrees.nodes.impl.SelectorNodePanel;
+import com.jme3.gde.core.assets.ProjectAssetManager;
+import com.jme3.gde.core.editor.nodes.Connection;
+import com.jme3.gde.core.editor.nodes.Diagram;
+import com.jme3.gde.core.editor.nodes.NodePanel;
+import com.jme3.gde.core.editor.nodes.Selectable;
+import com.jme3.gde.core.editor.icons.Icons;
+import com.jme3.gde.core.editor.nodes.ConnectionEndpoint;
+import com.jme3.gde.core.editor.nodes.DraggablePanel;
+//import static com.jme3.gde.materialdefinition.editor.TreeNodePanel.NodeType;
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentListener;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.swing.JMenuItem;
+
+/**
+ * The Diagram is the main canvas where all nodes {@link DraggablePanel} and
+ * their connections {@link ConnectionEndpoint} {@link Connection} are added onto.
+ * @author Nehon
+ */
+public class TreeDiagram extends Diagram implements ComponentListener {
+    
+    int maxWidth = 0;
+
+    @SuppressWarnings("LeakingThisInConstructor")
+    public TreeDiagram() {
+        super();
+        setBackground(new Color(35, 35, 35));
+    }
+    
+    @Override
+    protected boolean mouseLMBPrePressedEvent(MouseEvent e) {
+        return false;
+    }
+    
+    @Override
+    protected void showEdit(NodePanel node) {
+        /*if (node instanceof TreeNodePanel &&
+                parent instanceof MatDefEditorlElement) {
+            ((MatDefEditorlElement)parent).showShaderEditor(node.getName(), 
+                    ((TreeNodePanel)node).getType(), 
+                    ((TreeNodePanel)node).filePaths);
+        }*/
+    }
+
+    @Override
+    public String makeKeyForConnection(Connection con, Object obj) {
+        return "KEY";//MaterialUtils.makeKey((MappingBlock)obj, currentTechniqueName);
+    }
+
+    @Override
+    protected Selectable trySelect(String key) {
+        /*for (ShaderOutBusPanel outBusPanel : outBuses) {
+            if (outBusPanel.getKey().equals(key)) {
+                return outBusPanel;
+            }
+        }*/
+        
+        return null;
+    }
+    
+    @Override
+    protected void createPopupMenu() {
+        super.createPopupMenu();
+        
+        JMenuItem dialogItem = createMenuItem("Add Node", Icons.node);
+        dialogItem.addActionListener((ActionEvent e) -> {
+            AddNodeDialog d = new AddNodeDialog(null, true,
+                    ((BTreeNodeEditorElement)parent).obj.getLookup()
+                            .lookup(ProjectAssetManager.class), 
+                    TreeDiagram.this, clickLoc);
+            d.setLocationRelativeTo(null);
+            d.setVisible(true);
+        });
+        contextMenu.add(dialogItem);
+        contextMenu.add(createSeparator());
+        
+        JMenuItem nodeItem = createMenuItem("Parallel", Icons.node);
+        nodeItem.addActionListener((ActionEvent e) -> {
+            TreeNodePanel tnp = new ParallelNodePanel(TreeDiagram.this, new Parallel(), 1);
+            addNode(tnp);
+            tnp.setLocation(clickLoc);
+            tnp.revalidate();
+            repaint();
+        });
+
+        contextMenu.add(nodeItem);
+        
+        nodeItem = createMenuItem("Invert", Icons.node);
+        nodeItem.addActionListener((ActionEvent e) -> {
+            TreeNodePanel tnp = new DecoratorNodePanel(TreeDiagram.this, new Invert());
+            addNode(tnp);
+            tnp.setLocation(clickLoc);
+            tnp.revalidate();
+            repaint();
+        });
+        contextMenu.add(nodeItem);
+        
+        nodeItem = createMenuItem("Selector", Icons.node);
+        nodeItem.addActionListener((ActionEvent e) -> {
+            TreeNodePanel tnp = new SelectorNodePanel(TreeDiagram.this, new Selector(), 1);
+            addNode(tnp);
+            tnp.setLocation(clickLoc);
+            tnp.revalidate();
+            repaint();
+        });
+
+        contextMenu.add(nodeItem);
+    }
+    
+    @Override
+    public Connection connect(ConnectionEndpoint start, ConnectionEndpoint end) {
+        Connection conn = new Connection(start, end);
+        
+        // see logic in TreeConnectionEndPoint#canConnect
+        // Feature: The Direction is clear here so we can drag them from both sides.
+        if (end != null && start != null) {
+            if (start.getParamType() == ConnectionEndpoint.ParamType.Input && 
+                    (end.getParamType() == ConnectionEndpoint.ParamType.Output ||
+                        end.getParamType() == ConnectionEndpoint.ParamType.Both)) {
+                conn = new Connection(end, start);
+            }
+            
+            start.connect(conn);
+            end.connect(conn);
+            addConnection(conn);
+            return conn;
+        } else {
+            return null;
+        }
+    }
+    
+    public Stream<Connection> getConnectionsFrom(ConnectionEndpoint start) {
+        return connections.stream().filter(con -> con.getStart().equals(start));
+    }
+    
+    public Stream<Connection> getConnectionsFrom(NodePanel pnl) {
+        return connections.stream().filter(con -> con.getStart().getNode().equals(pnl));
+    }
+    
+    public boolean hasConnectionFrom(ConnectionEndpoint start) {
+        return getConnectionsFrom(start).findAny().isPresent();
+    }
+
+    @Override
+    protected int calcMaxWidth() {
+        return 0;
+    }
+    
+    @Override
+    protected int calcMaxHeight() {
+        return 0;
+    }
+    
+    int minWidth = 0;
+    int minHeight = 0;
+
+    @Override
+    public void autoLayout() {
+        RootNodePanel rnp = ((BTreeNodeEditorElement)getEditorParent()).obj.getRootNodePanel();
+        List<NodePanel> npL = new ArrayList<>();
+        List<Integer> maxWidths = new ArrayList<>();
+        maxWidth = 0;
+        int center = 1500; // couldn't get that one dynamically atm
+
+        // Two passes: the first one calculates the maxWidth and the second one
+        // splits everything into left and right
+        // An Enhancement would be to set center as to the parent's position,
+        // but that breaks with our whole "level" system
+        for (int i = 0; i < 2; i++) {
+            npL.add(rnp);
+            int level = 0;
+            
+            while (!npL.isEmpty()) {
+                List<NodePanel> tmpList = new ArrayList<>();
+                tmpList.addAll(npL);
+                int x_level = (i == 0 ? center : center - maxWidths.get(level) / 2);
+
+                for (NodePanel pnl: tmpList) {
+                    pnl.setLocation(x_level, level * 150);
+                    npL.remove(pnl); // Remove this item
+                    // but add all children
+                    npL.addAll(
+                        getConnectionsFrom(pnl)
+                            .map(Connection::getEnd)
+                            .map(ConnectionEndpoint::getNode)
+                            .filter(p -> p instanceof NodePanel)
+                            .map(p -> (NodePanel)p)
+                            .collect(Collectors.toList())
+                    );
+                    
+                    x_level += (pnl.getWidth() + 20);
+                }
+                
+                if (i == 0) {
+                    maxWidths.add(x_level);
+                    maxWidth = Math.max(maxWidth, x_level);
+                } else {
+                    level++;
+                }
+            }
+            
+            if (i == 0) {
+                center = maxWidth/2;
+            }
+        }
+    }
+    
+    public List<NodePanel> getPanelsAt(int x, int y) {
+        return nodes.stream()
+                // Panels extend from their location with "size" size.
+                .filter(n -> n.getLocation().x < x) // is not starting "right" of x
+                .filter(n -> n.getLocation().y < y) // is not starting "above"  of y
+                .filter(n -> n.getLocation().x + n.getSize().width > x)
+                .filter(n -> n.getLocation().y + n.getSize().height > y)
+                .collect(Collectors.toList());
+    }
+
+    public void refreshNavigator(boolean dontRefreshTheDiagram) {
+        ((BTreeNodeEditorElement)getEditorParent()).refreshNavigator(dontRefreshTheDiagram);
+    }
+    
+    public List<TreeNodePanel> getOrphanedNodes() {
+        /* During internal diagram rebuilds, we need to save nodes which are not
+         * yet hooked up into the tree, we hence call them orphaned nodes.
+         */
+        return nodes.stream()
+            .map(n -> (TreeNodePanel)n)
+            .filter(n -> ((TreeNodePanel)n).isOrphan())
+            .collect(Collectors.toList());
+    }
+    
+    public void restoreOrphanedNodes(List<TreeNodePanel> orphans) {
+        orphans.forEach(o -> addNode(o));
+    }
+}

+ 611 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/editor/TreeNodePanel.java

@@ -0,0 +1,611 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.editor;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.InputMappingBlock;
+import com.jme3.gde.behaviortrees.OutputMappingBlock;
+import com.jme3.gde.core.editor.nodes.ConnectionEndpoint;
+import com.jme3.gde.core.editor.nodes.NodePanel;
+import com.jme3.gde.core.editor.icons.Icons;
+import com.jme3.gde.core.editor.nodes.Connection;
+import com.jme3.gde.core.editor.nodes.Diagram;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.event.MouseEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.swing.GroupLayout;
+import javax.swing.Icon;
+import javax.swing.JLabel;
+import javax.swing.SwingConstants;
+
+/**
+ * The TreeNodePanel is the ShaderNode specific implementation of 
+ {@link NodePanel}.
+ * @author MeFisto94
+ */
+public class TreeNodePanel extends NodePanel implements InOut, 
+        PropertyChangeListener {
+    private NodeType type = NodeType.LeafTask;
+    // Required for the Editor
+    protected List<String> filePaths = new ArrayList<>();
+    protected int num = 0;
+    protected Task task;
+    
+    // Guards:
+    // The Panel which represents THIS nodes Guard (may be null)
+    protected TreeNodePanel guardPanel;
+    // The Panel to which THIS Guard belongs to (may be null)
+    protected TreeNodePanel guardedPanel;
+    
+    public enum NodeType {
+        LeafTask(new Color(220, 220, 70)), // yellow
+        Root(new Color(220, 70, 70)), // red
+        Sequential(new Color(114, 200, 220)), // blue
+        Parallel(new Color(70, 220, 70)),//green
+        BuiltinLeafTask(new Color(170, 40, 220)), // purple
+        Decorator(new Color(255, 114, 40)); // orange
+        
+        private Color color;
+
+        private NodeType() {
+        }
+
+        private NodeType(Color color) {
+            this.color = color;
+        }
+
+        public Color getColor() {
+            return color;
+        }
+    }
+
+    public TreeNodePanel(Task task, Diagram dia, NodeType type, int numIns, int numOuts, String nameAndTitle, TreeNodePanel guardedPanel) {
+        super();
+        this.type = type;
+        this.diagram = dia; // required by our swing stuff which we have in the constructor
+        this.task = task;
+        this.guardedPanel = guardedPanel;
+        this.name = nameAndTitle;
+               
+        if (task.getGuard() != null) {
+            setGuardPanel(BTreeNodeEditorElement.taskToPanel(diagram, task.getGuard(), this));
+            guardPanel.setGuardedPanel(this);
+        }
+        
+        /*node.addPropertyChangeListener(WeakListeners.propertyChange(this, node));
+        this.addPropertyChangeListener(WeakListeners.propertyChange(node, this));*/
+        
+        /*
+        //setToolbar(new ShaderNodeToolBar(this));
+        refresh(node);
+        
+        filePaths.addAll(def.getShadersPath());
+        String defPath = ((DefinitionBlock)node.getContents().get(0)).getPath();
+        filePaths.add(defPath);    */
+        init(numIns, numOuts);
+
+    }
+
+    public TreeNodePanel(Task task, Diagram dia, NodeType type, int numIns, int numOuts, String nameAndTitle) {
+        this(task, dia, type, numIns, numOuts, nameAndTitle, null);
+    }
+    
+    public TreeNodePanel(Task task, Diagram dia, NodeType type, int numIns, int numOuts) {
+        this(task, dia, type, numIns, numOuts, null);
+    }
+    
+    public List<TreeNodePanel> getChildren() {
+        return outputDots.stream()
+            .map(ConnectionEndpoint::getConnection)
+            // check for null (unconnected outputs)
+            .filter(c -> c != null)
+            .map(Connection::getEnd)
+            .map(ConnectionEndpoint::getNode)
+            .filter(dp -> dp instanceof TreeNodePanel)
+            .map(dp -> (TreeNodePanel)dp)
+            .collect(Collectors.toList());
+    }
+
+    public Task getTask() {
+        return task;
+    }
+
+    /* All Guard related Setters need a call to init() to refresh the UI */
+    public TreeNodePanel getGuardPanel() {
+        return guardPanel;
+    }
+    
+    public void setGuardPanel(TreeNodePanel guardPanel) {
+        this.guardPanel = guardPanel;
+    }
+
+    public boolean isGuard() {
+        return guardedPanel != null;
+    }
+
+    public void setGuardedPanel(TreeNodePanel guardedPanel) {
+        this.guardedPanel = guardedPanel;
+        if (isGuard()) {
+            color = new Color(40, 40, 255);
+            backgroundColor = color;            
+        } else {
+            updateType();
+        }
+    }
+    
+    public TreeNodePanel getGuardedPanel() {
+        return guardedPanel;
+    }
+    
+    public Task getGuard() {
+        return task.getGuard();
+    }
+    
+    public boolean hasGuard() {
+        return getGuard() != null;
+    }
+    
+    public void attachGuard(TreeNodePanel guardPanel) {
+        setGuardPanel(guardPanel);
+        getGuardPanel().setGuardedPanel(this);
+        
+        ((BTreeNodeEditorElement)diagram.getEditorParent()).onAttachGuard(this, guardPanel);
+    }
+    
+    public void detachGuard() {
+        getGuardPanel().setGuardedPanel(null);
+        setGuardPanel(null);
+        ((BTreeNodeEditorElement)diagram.getEditorParent()).onDetachGuard(this, guardPanel);
+    }
+    
+    /**
+     * Creates a new Input Terminal for this Node (takes care of creating the 
+     * labels etc).
+     * @param reloadUI Whether the UI should be updated automatically or not.
+     * Set this to false when bulk adding and call initComponents manually
+     * @return 
+     */
+    public ConnectionEndpoint createInput(boolean reloadUI) {
+        /* We need to add labels or override NodePanel#initComponents, so we
+         * turn our bug into a feature and use the label to enumerate the 
+         * connectors
+        */
+        String numStr = "" + num++;
+        JLabel label = createLabel(numStr, ConnectionEndpoint.ParamType.Input);
+        //label.setBorder(new LineBorder(Color.CYAN));
+        ConnectionEndpoint dot = createConnectionEndpoint("", ConnectionEndpoint.ParamType.Input, numStr);
+        //dot.setBorder(new LineBorder(Color.GREEN));
+        inputLabels.add(label);
+        inputDots.add(dot);
+        
+        if (reloadUI) {
+            initComponents();
+        }
+        
+        return dot;
+    }
+    
+    public ConnectionEndpoint createOutput(boolean reloadUI) {
+        String numStr = "" + num++;
+        JLabel label = createLabel(numStr, ConnectionEndpoint.ParamType.Output);
+        //label.setBorder(new LineBorder(Color.CYAN));
+        ConnectionEndpoint dot = createConnectionEndpoint("", ConnectionEndpoint.ParamType.Output, numStr);
+        //dot.setBorder(new LineBorder(Color.GREEN));
+        outputLabels.add(label);
+        outputDots.add(dot);
+        
+        if (reloadUI) {
+            initComponents();
+        }
+        
+        return dot;
+    }
+    
+    private void init(int numInputs, int numOutputs) {
+        //setBounds(0, 0, 100, 30 + inputs.size() * 20 + outputs.size() * 20);
+        setBounds(0, 0, 100, 30);
+
+        for (int i = 0; i < numInputs; i++) {
+            createInput(false);
+        }
+        
+        for (int i = 0; i < numOutputs; i++) {
+            createOutput(false);
+        }
+
+        initComponents();
+        setOpaque(false);
+    }
+
+    @Override
+    protected void initComponents() {
+        String oldHeaderText = null;
+        Icon oldHeaderIcon = null;
+        
+        // Support calling initComponents as refresher
+        if (getLayout() instanceof GroupLayout) {
+            removeAll();
+            oldHeaderText = header.getText();
+            oldHeaderIcon = header.getIcon();
+        }
+        
+        header = new JLabel(Icons.vert);
+        header.setForeground(Color.BLACK);
+        header.addMouseListener(labelMouseMotionListener);
+        header.addMouseMotionListener(labelMouseMotionListener);
+        header.setHorizontalAlignment(SwingConstants.CENTER);
+        header.setFont(new Font("Tahoma", Font.BOLD, 11));
+        //header.setBorder(new LineBorder(Color.RED));
+        
+        // Support calling initComponents as refresher
+        if (getLayout() instanceof GroupLayout) {
+            header.setText(oldHeaderText);
+            header.setIcon(oldHeaderIcon);
+        } else {
+            if (name != null) {
+                header.setText(name);
+            }
+            
+            updateType();
+        }
+        
+        GroupLayout layout = new GroupLayout(this);
+        this.setLayout(layout);
+        
+        GroupLayout.SequentialGroup hHeader = layout.createSequentialGroup()
+            .addGap(16) // Support Padding
+            .addComponent(header)
+            .addGap(16);
+        
+        if (inputDots.size() != inputLabels.size() || outputDots.size() != outputLabels.size()) {
+            throw new IllegalArgumentException("Dots and Labels don't match");
+        }
+        
+        final int DOT_SIZE = 10;
+        
+        if (isGuard()) {
+            layout.setVerticalGroup(
+                layout.createSequentialGroup()
+                    .addGap(16)
+                    .addComponent(header)
+                    .addGap(16)
+            );
+
+            layout.setHorizontalGroup(
+                layout.createParallelGroup(GroupLayout.Alignment.CENTER)
+                    .addGroup(GroupLayout.Alignment.CENTER, hHeader)
+            );            
+        } else {        
+            GroupLayout.ParallelGroup vInputGroup = layout.createParallelGroup(GroupLayout.Alignment.CENTER);
+            if (inputDots.isEmpty()) {
+                vInputGroup.addGroup(GroupLayout.Alignment.CENTER,
+                    layout.createSequentialGroup()
+                        .addGap(16)
+                );
+            } else {
+                for (int i = 0; i < inputDots.size(); i++) {
+                    vInputGroup.addGroup(GroupLayout.Alignment.CENTER, 
+                        layout.createSequentialGroup()
+                            .addGap(8) // Padding from the top
+                            .addComponent(inputDots.get(i), GroupLayout.PREFERRED_SIZE,
+                                    DOT_SIZE, GroupLayout.PREFERRED_SIZE)
+                            .addGap(2)
+                            .addComponent(inputLabels.get(i))
+                            .addGap(8) // Padding the bottom
+                    );
+                }
+            }
+
+            GroupLayout.SequentialGroup hInputGroup = layout.createSequentialGroup();
+            hInputGroup.addGap(16);
+            for (int i = 0; i < inputDots.size(); i++) {
+                hInputGroup.addGroup(
+                    layout.createParallelGroup(GroupLayout.Alignment.CENTER)
+                        .addComponent(inputDots.get(i), GroupLayout.PREFERRED_SIZE,
+                                DOT_SIZE, GroupLayout.PREFERRED_SIZE)
+                        .addComponent(inputLabels.get(i), GroupLayout.PREFERRED_SIZE,
+                                GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
+                );
+                hInputGroup.addGap(16);
+            }
+
+            // Output
+            GroupLayout.ParallelGroup vOutputGroup = layout.createParallelGroup(GroupLayout.Alignment.CENTER);
+            if (outputDots.isEmpty()) {
+                vOutputGroup.addGroup(GroupLayout.Alignment.CENTER,
+                    layout.createSequentialGroup()
+                        .addGap(16)
+                );
+            } else {
+                for (int i = 0; i < outputDots.size(); i++) {
+                    vOutputGroup.addGroup(GroupLayout.Alignment.CENTER, 
+                        layout.createSequentialGroup()
+                            .addGap(8) // Padding from the top
+                            .addComponent(outputLabels.get(i))
+                            .addGap(2)
+                            .addComponent(outputDots.get(i), GroupLayout.PREFERRED_SIZE,
+                                    DOT_SIZE, GroupLayout.PREFERRED_SIZE)
+                            .addGap(8) // Padding the bottom
+                    );
+                }
+            }
+
+            GroupLayout.SequentialGroup hOutputGroup = layout.createSequentialGroup();
+            hOutputGroup.addGap(16);
+            for (int i = 0; i < outputDots.size(); i++) {
+                hOutputGroup.addGroup(
+                    layout.createParallelGroup(GroupLayout.Alignment.CENTER)
+                        .addComponent(outputLabels.get(i), GroupLayout.PREFERRED_SIZE,
+                                GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
+                        .addComponent(outputDots.get(i), GroupLayout.PREFERRED_SIZE,
+                                DOT_SIZE, GroupLayout.PREFERRED_SIZE)
+                );
+                hOutputGroup.addGap(16);
+            }
+            
+            // Maybe add some padding to achieve a minimum size (clever padding?)
+            if (guardPanel != null) {
+                layout.setVerticalGroup(
+                    layout.createSequentialGroup()
+                        .addGroup(vInputGroup)
+                        .addComponent(header)
+                        .addComponent(guardPanel)
+                        .addGroup(vOutputGroup)
+                );
+
+                layout.setHorizontalGroup(
+                    layout.createParallelGroup(GroupLayout.Alignment.CENTER)
+                        .addGroup(hInputGroup)
+                        .addGroup(GroupLayout.Alignment.CENTER, hHeader)
+                        .addComponent(guardPanel, GroupLayout.PREFERRED_SIZE,
+                                GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
+                        .addGroup(hOutputGroup)
+                );
+            } else { // Neither HAVING a Guard nor BEING a Guard
+                layout.setVerticalGroup(
+                    layout.createSequentialGroup()
+                        .addGroup(vInputGroup)
+                        .addComponent(header)
+                        .addGroup(vOutputGroup)
+                );
+
+                layout.setHorizontalGroup(
+                    layout.createParallelGroup(GroupLayout.Alignment.CENTER)
+                        .addGroup(hInputGroup)
+                        .addGroup(GroupLayout.Alignment.CENTER, hHeader)
+                        .addGroup(hOutputGroup)
+                );            
+            }
+        }
+    }
+
+    @Override
+    protected void paintTitleBar(Graphics2D g) {
+        // Purposely NO-OP
+    }
+    
+    public boolean isOrphan() {
+        return !(inputDots.stream().anyMatch(d -> d.isConnected()) ||
+            outputDots.stream().anyMatch(d -> d.isConnected()));
+    }
+    
+
+    @Override
+    public void mouseReleased(MouseEvent e) {
+        super.mouseReleased(e);
+        
+        if (svdx != getLocation().x) {
+            //firePropertyChange(ShaderNodeBlock.POSITION, svdx, getLocation().x);
+        }
+        
+        diagram.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+        
+        if (isGuard()) { // has to be before super call or else svdx might get overwritten
+            if (getParent() != null) {
+                Dimension parentSize = getParent().getSize();
+                Dimension thisSize = getSize();
+
+                // If not we don't have enough data available it seems.
+                if (parentSize.height > 0 && parentSize.width > 0 && thisSize.height > 0 && thisSize.width > 0) {
+                    
+                    /* The Usage of LocationOnScreen is probably wrong in the whole DraggablePanel Class,
+                     * as it does not matter on which monitor you have this open, it would've been better
+                     * to have it relative to the parent. Because since I use the SDK on Display 2, I always
+                     * have an x-offset of 1920...
+                     */
+                    int newX = svdx + e.getLocationOnScreen().x - svdex;
+                    int newY = svdy + e.getLocationOnScreen().y - svdey;
+                    
+                    if (newX < 0 || newX > parentSize.width - thisSize.width || newY < 0 || newY > parentSize.height - thisSize.height) {
+                        // Remove the Guard and make a real node out of it
+                        int newPosX = getGuardedPanel().getLocation().x + e.getPoint().x;
+                        int newPosY = getGuardedPanel().getLocation().y + e.getPoint().y;
+                        
+                        setLocation(newPosX, newPosY);    
+                        TreeNodePanel guardedPnl = getGuardedPanel();
+                        getGuardedPanel().detachGuard();
+                        // here, getGuardedPanel is invalid, so we cached it
+                        guardedPnl.initComponents();
+                        guardedPnl.setSize(guardedPnl.getPreferredSize());
+                        initComponents();
+                        setSize(getPreferredSize());
+                        diagram.addNode(this); // Add as a regular node
+                        ((TreeDiagram)diagram).refreshNavigator(true);
+                    }
+                }
+            }
+        } else {
+            List<NodePanel> list = ((TreeDiagram)diagram).getPanelsAt(e.getX() + getLocation().x, e.getY() + getLocation().y);
+            
+            if (!list.isEmpty() && list.get(0) != this) {
+                diagram.removeNode(this);
+                ((TreeNodePanel)list.get(0)).attachGuard(this);
+                initComponents();
+                setSize(getPreferredSize());
+                getGuardedPanel().initComponents();
+                getGuardedPanel().setSize(getGuardedPanel().getPreferredSize());
+                ((TreeDiagram)diagram).refreshNavigator(true);
+            }
+        }
+    }
+    
+    @Override
+    protected void movePanel(int xoffset, int yoffset) {
+        if (isGuard()) { // has to be before super call or else svdx might get overwritten
+            if (getParent() != null) {
+                Dimension parentSize = getParent().getSize();
+                Dimension thisSize = getSize();
+
+                // If not we don't have enough data available it seems.
+                if (parentSize.height > 0 && parentSize.width > 0 && thisSize.height > 0 && thisSize.width > 0) {
+                    int newX = svdx + xoffset;
+                    int newY = svdy + yoffset;
+                    
+                    if (newX < 0 || newX > parentSize.width - thisSize.width || newY < 0 || newY > parentSize.height - thisSize.height) {
+                        diagram.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+                    } else {
+                        diagram.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+                    }
+                }
+            }
+        }
+        
+        // @TODO: we could use TreeDiagram#getNodesAt() to disallow stacking nodes and make them magnetically avoided.
+        
+        super.movePanel(xoffset, yoffset);
+    }
+    
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals("name")) {
+            //refresh((ShaderNodeBlock) evt.getSource());
+        }
+    }
+    
+    @Override
+    /**
+     * Keys are important for Selectable, they have to be unique, so we take toString
+     */
+    public String getKey() {
+        return toString();
+    }
+    
+    public NodeType getType() {
+        return type;
+    }
+    
+    
+    /**
+     * This methods is responsible for setting the correct icon and text
+     * in the header-bar of the node. Call this from your constructor,
+     * _after_ components have been inited.
+     */
+    protected void updateType() {
+        /*switch (type) {
+            case MatParam:
+                header.setIcon(Icons.mat);
+                setNameAndTitle("MatParam");
+                break;
+        }*/
+        
+        color = type.getColor();
+        backgroundColor = color;
+    }
+    
+    /**
+     * Utility method to update the node text when the underlying ShaderNode
+     * has been changed (called by PropertyChangeListeners)
+     * @param node The source shadernode
+     */
+    /*protected final void refresh(ShaderNodeBlock node) {
+        setNameAndTitle(node.getName());
+    }*/
+    
+    @Override
+    protected boolean canEdit() {
+        return false; // We don't use Edit Dialogs yet
+    }
+    
+    /**
+     * Create a Label for a given ShaderNodes' Input/Output
+     * @param type Whether this is the input or output
+     * @return The IO Label
+     */
+    @Override
+    protected JLabel createLabel(String txt, ConnectionEndpoint.ParamType type) {
+        JLabel label = super.createLabel(txt, type);
+        label.setHorizontalAlignment(SwingConstants.CENTER);
+        label.setToolTipText(txt);
+        return label;
+    }
+    
+    @Override
+    public ConnectionEndpoint createConnectionEndpoint(String type, 
+            ConnectionEndpoint.ParamType paramType, String paramName) {
+        TreeConnectionEndpoint con = new TreeConnectionEndpoint();
+        con.setNode(this);
+        con.setText(paramName);
+        con.setParamType(paramType);
+        con.setType(type);
+        return con;
+    }
+    
+    // Callbacks when Connections are formed and released
+    @Override
+    public void addInputMapping(InputMappingBlock block) { }
+
+    @Override
+    public void removeInputMapping(InputMappingBlock block) { }
+
+    @Override
+    public void addOutputMapping(OutputMappingBlock block) { }
+
+    @Override
+    public void removeOutputMapping(OutputMappingBlock block) { }
+
+    public ConnectionEndpoint getOutputByIndex(int idx) {
+        return outputDots.get(idx);
+    }
+    
+    public ConnectionEndpoint getInputByIndex(int idx) {
+        return inputDots.get(idx);
+    }
+
+}

+ 14 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/layer.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "http://www.netbeans.org/dtds/filesystem-1_2.dtd">
+<filesystem>
+    <!--<folder name="Windows2">
+        <folder name="Components">
+            <file name="MaterialEditorTopComponent.settings" url="MaterialEditorTopComponentSettings.xml"/>
+        </folder>
+        <folder name="Modes">
+            <folder name="editor">
+                <file name="MaterialEditorTopComponent.wstcref" url="MaterialEditorTopComponentWstcref.xml"/>
+            </folder>
+        </folder>
+    </folder>-->
+</filesystem>

+ 37 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/BTreeNavigatorPanel.form

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.4" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Component id="jScrollPane1" alignment="0" pref="400" max="32767" attributes="0"/>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Component id="jScrollPane1" alignment="0" pref="300" max="32767" attributes="0"/>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Container class="javax.swing.JScrollPane" name="jScrollPane1">
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new CustomBeanTreeView(this)"/>
+      </AuxValues>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
+    </Container>
+  </SubComponents>
+</Form>

+ 227 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/BTreeNavigatorPanel.java

@@ -0,0 +1,227 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.navigator;
+
+import com.jme3.gde.behaviortrees.BTreeDataObject;
+import com.jme3.gde.behaviortrees.editor.BTreeNodeEditorElement;
+import com.jme3.gde.behaviortrees.navigator.nodes.TreeNodePanelNode;
+import com.jme3.gde.behaviortrees.nodes.CustomBeanTreeView;
+import java.util.Collection;
+import javax.swing.JComponent;
+import org.netbeans.spi.navigator.NavigatorPanel;
+import org.openide.explorer.ExplorerManager;
+import org.openide.explorer.ExplorerUtils;
+import org.openide.nodes.Node;
+import org.openide.util.Lookup;
+import org.openide.util.LookupEvent;
+import org.openide.util.LookupListener;
+import org.openide.windows.TopComponent;
+
+/**
+ * This Panel is responsible for populating the "Navigator" with a view over the
+ * Scene. This means one can select specific nodes in the Navigator, which is
+ * not really practical but required for the Properties to work it seems.
+ * 
+ * @author MeFisto94
+ */
[email protected](mimeType = "text/gdx-ai-btree", displayName = "Behavior Tree")
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class BTreeNavigatorPanel extends TopComponent /* JPanel*/ implements NavigatorPanel, ExplorerManager.Provider, Lookup.Provider {
+
+    /**
+     * template for finding data in given context.
+     */
+    private static final Lookup.Template<BTreeDataObject> MY_DATA = new Lookup.Template<BTreeDataObject>(BTreeDataObject.class);
+
+    /**
+     * current context to work on
+     */
+    private Lookup.Result<BTreeDataObject> curContext;
+    private final Lookup lookup;
+    /**
+     * listener to context changes
+     */
+    private LookupListener contextL;
+    private final ExplorerManager mgr = new ExplorerManager();
+
+    private BTreeDataObject data;
+    private BTreeNodeEditorElement topComponent;
+    
+    /**
+     * Creates new form MatDefNavigatorPanel
+     */
+    public BTreeNavigatorPanel() {
+        initComponents();
+        lookup = ExplorerUtils.createLookup(mgr, getActionMap());
+        associateLookup(lookup);
+    }
+
+    @Override
+    public String getDisplayHint() {
+        return "BehaviorTree outline view";
+    }
+
+    @Override
+    public String getDisplayName() {
+        return "Navigator Panel";
+    }
+
+    @Override
+    public JComponent getComponent() {
+        return this;
+    }
+
+    @Override
+    public void panelActivated(Lookup context) {
+        // lookup context and listen to result to get notified about context changes
+        curContext = context.lookup(MY_DATA);
+        //lookup = context;
+        curContext.addLookupListener(getContextListener());
+        // get actual data and recompute content
+        Collection<? extends BTreeDataObject> data = curContext.allInstances();
+        setNewContent(data);
+        ExplorerUtils.activateActions(mgr, true);
+    }
+
+    @Override
+    public void panelDeactivated() {
+        Collection<? extends BTreeDataObject>  data = curContext.allInstances();
+        if (!data.isEmpty()) {
+            BTreeDataObject obj = (BTreeDataObject) data.iterator().next();
+            obj.getLookupContents().remove(this);
+        }
+        curContext.removeLookupListener(getContextListener());
+        curContext = null;
+        mgr.setRootContext(Node.EMPTY);
+        ExplorerUtils.activateActions(mgr, false);
+    }
+
+    @Override
+    public Lookup getLookup() {
+        // go with default activated Node strategy
+        return lookup;
+    }
+    
+    public void selectionChanged(Node[] nodes, ExplorerManager em) {
+        if (data != null) {
+            BTreeNodeEditorElement ele = data.getTopComponent();
+            if (ele != null) {
+                ele.selectionChanged(nodes);
+            }
+        }
+    }
+
+    /**
+     * *********** non - public part ***********
+     */
+    private void setNewContent(Collection<? extends BTreeDataObject>  newData) {
+        if (!newData.isEmpty()) {
+            BTreeDataObject data = (BTreeDataObject) newData.iterator().next();
+            data.getLookupContents().add(this);
+            if (data.isLoaded()) {
+                updateData(data);
+            } else {
+                mgr.setRootContext(Node.EMPTY);
+            }
+        }
+    }
+
+    /**
+     * Accessor for listener to context
+     */
+    private LookupListener getContextListener() {
+        if (contextL == null) {
+            contextL = new ContextListener();
+        }
+        return contextL;
+    }
+
+    @Override
+    public ExplorerManager getExplorerManager() {
+        return mgr;
+    }
+
+    public void updateData(BTreeDataObject data) {
+        this.data = data;        
+        
+        if (data != null && data.getRootNodePanel() != null) {
+            mgr.setRootContext(new TreeNodePanelNode(data.getRootNodePanel(), data.getLookup()));
+        } else {
+            mgr.setRootContext(Node.EMPTY);
+        }
+        
+        try {
+            mgr.setSelectedNodes(new Node[] {});
+            ExplorerUtils.activateActions(mgr, true);
+        } catch (Exception e) {
+            
+        }
+    }
+
+    /**
+     * Listens to changes of context and triggers proper action
+     */
+    private class ContextListener implements LookupListener {
+        @Override
+        public void resultChanged(LookupEvent ev) {
+            Collection<? extends BTreeDataObject>  data = (Collection<? extends BTreeDataObject>)((Lookup.Result<?> ) ev.getSource()).allInstances();
+            setNewContent(data);
+        }
+    } // end of ContextListener
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        jScrollPane1 = new CustomBeanTreeView(this);
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE)
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 300, Short.MAX_VALUE)
+        );
+    }// </editor-fold>//GEN-END:initComponents
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JScrollPane jScrollPane1;
+    // End of variables declaration//GEN-END:variables
+
+}

+ 136 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/nodes/AbstractTaskNode.java

@@ -0,0 +1,136 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.navigator.nodes;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.BTreeDataObject;
+import com.jme3.gde.behaviortrees.BTreeExporterUtils;
+import com.jme3.gde.behaviortrees.navigator.properties.AttrInfoProperty;
+import com.jme3.gde.core.editor.icons.Icons;
+import java.awt.Image;
+import java.lang.reflect.InvocationTargetException;
+import javax.swing.Action;
+import org.openide.nodes.AbstractNode;
+import org.openide.nodes.Children;
+import org.openide.nodes.PropertySupport;
+import org.openide.nodes.Sheet;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public abstract class AbstractTaskNode extends AbstractNode {
+    protected Lookup lookup;
+    protected Task task;
+
+    public AbstractTaskNode(Task task, Children children, Lookup lookup) {
+        super(children, lookup);
+        this.lookup = lookup;
+        this.task = task;
+        /*block.addPropertyChangeListener((PropertyChangeEvent evt) -> {
+            if (evt.getPropertyName().equals(TreeNodePanel.ADD_MAT_PARAM) ||
+                    evt.getPropertyName().equals(TreeNodePanel.REMOVE_MAT_PARAM)) {
+                setSheet(createSheet());
+                firePropertySetsChange(null, null);
+            }
+        })*/
+    }
+
+    @Override
+    public Action[] getActions(boolean popup) {
+        return new Action[]{};
+    }
+      
+    @Override
+    protected Sheet createSheet() {
+        Sheet sheet = super.createSheet();
+        //Task task = lookup.lookup(Task.class);
+
+        Sheet.Set set = new Sheet.Set();
+        set.setName("TaskInfo");
+        set.setDisplayName("Task Information");
+        set.put(new PropertySupport.ReadOnly<String>("className", String.class, "Class Name", "The Name of the Task") {
+            @Override
+            public String getValue() throws IllegalAccessException, InvocationTargetException {
+                return task.getClass().getSimpleName();
+            }
+        });
+        
+        set.put(new PropertySupport.ReadOnly<String>("package", String.class, "Package", "The Package where the Task resides") {
+            @Override
+            public String getValue() throws IllegalAccessException, InvocationTargetException {
+                return task.getClass().getPackage().getName();
+            }
+        });
+        
+        sheet.put(set);
+        
+        Sheet.Set metadataSet = new Sheet.Set();
+        metadataSet.setName("TaskSpecific");
+        metadataSet.setDisplayName("Task specific settings");
+        
+        BTreeExporterUtils.findMetadata(task.getClass()).values()
+            .forEach(a -> metadataSet.put(
+                    AttrInfoProperty.createNew(a, task, 
+                            lookup.lookup(BTreeDataObject.class)
+                    )
+            )
+        );
+        
+        sheet.put(metadataSet);
+        return sheet;
+    }
+    
+    
+    @Override
+    public Image getIcon(int type) {
+        return Icons.node.getImage();
+    }
+
+    @Override
+    public Image getOpenedIcon(int type) {
+        return Icons.node.getImage();
+    }
+
+    public String getKey() {
+        return getName();
+    }
+
+    /*
+    public void propertyChange(PropertyChangeEvent evt) {
+        if(evt.getPropertyName().equals("name")){
+            setName((String)evt.getNewValue());
+            setDisplayName((String)evt.getNewValue());
+        }
+    }*/
+}

+ 85 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/nodes/TreeNodePanelNode.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2009-2018 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.navigator.nodes;
+
+import com.jme3.gde.behaviortrees.editor.TreeNodePanel;
+import java.util.List;
+import org.openide.nodes.ChildFactory;
+import org.openide.nodes.Children;
+import org.openide.nodes.Node;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public class TreeNodePanelNode extends AbstractTaskNode {
+    TreeNodePanel panel;
+    
+    public TreeNodePanelNode(TreeNodePanel tnp, Children children, Lookup lookup) {
+        super(tnp.getTask(), children, lookup);
+        if (tnp.isGuard()) {
+            setName("[GUARD]: " + tnp.getName());
+        } else {
+            setName(tnp.getName());
+        }
+        this.panel = tnp;
+    }
+
+    public TreeNodePanelNode(TreeNodePanel tnp, Lookup lookup) {
+        this(tnp, Children.create(new ChildFactory<TreeNodePanel>() {
+            @Override
+            protected boolean createKeys(List<TreeNodePanel> list) {
+                if (tnp.getGuardPanel() != null) {
+                    list.add(tnp.getGuardPanel());
+                }
+                
+                list.addAll(tnp.getChildren());
+                return true;
+            }
+
+            @Override
+            protected Node createNodeForKey(TreeNodePanel key) {
+                if (key.getChildren().isEmpty() && key.getGuardPanel() == null) {
+                    return new TreeNodePanelNode(key, Children.LEAF, lookup);
+                } else {
+                    return new TreeNodePanelNode(key, lookup);
+                }
+            }
+        }, false), lookup);
+    }
+
+    public TreeNodePanel getPanel() {
+        return panel;
+    }
+    
+}

+ 156 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/AttrInfoProperty.java

@@ -0,0 +1,156 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.navigator.properties;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.badlogic.gdx.ai.utils.random.Distribution;
+import com.jme3.gde.behaviortrees.BTreeDataObject;
+import com.jme3.gde.behaviortrees.BTreeExporterUtils.AttrInfo;
+import com.jme3.gde.core.errorreport.ExceptionUtils;
+import java.beans.PropertyEditor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Objects;
+import org.openide.nodes.Node;
+
+/**
+ * This property handles parsing the AttribInfo and add a correct instance of 
+ * the Property.
+ * 
+ * @author MeFisto94
+ */
+public class AttrInfoProperty<T> extends Node.Property<T> {
+    Class<? extends PropertyEditor> propertyEditorClass;
+    T instance;
+    AttrInfo aInfo;
+    BTreeDataObject obj;
+    
+    public AttrInfoProperty(T instance, AttrInfo aInfo, BTreeDataObject obj) {
+        super(aInfo.f.getType());
+        this.instance = instance;
+        this.aInfo = aInfo;
+        this.obj = obj;
+        setName(aInfo.fieldName);
+        setDisplayName((aInfo.required? "[!] " : "") + aInfo.name);
+        if (aInfo.required) {
+            setShortDescription("This is a mandatory attribute for the task");
+        }
+    }
+    
+    @Override
+    public PropertyEditor getPropertyEditor() {
+        if (propertyEditorClass != null) {
+            try {
+                return propertyEditorClass.newInstance();
+            } catch (Exception ex) {
+                ExceptionUtils.caughtException(ex, aInfo.toString());
+            }
+        }
+
+        return super.getPropertyEditor();
+    }
+
+    public void setPropertyEditorClass(Class<? extends PropertyEditor> clazz) {
+        propertyEditorClass = clazz;
+    }
+
+    @Override
+    public boolean canRead() {
+        return true;
+    }
+
+    @Override
+    public T getValue() throws IllegalAccessException, InvocationTargetException {
+        try {
+            aInfo.f.setAccessible(true);
+            Object obj = aInfo.f.get(instance);
+            
+            if (obj != null) {
+                return (T)obj;
+            }
+        } catch (Exception ex) {
+            ExceptionUtils.caughtException(ex);
+        }
+        
+        return null;
+    }
+
+    @Override
+    public boolean canWrite() {
+        return true;
+    }
+
+    @Override
+    public void setValue(T t) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        try {
+            aInfo.f.setAccessible(true);
+            Object o = aInfo.f.get(instance);
+            aInfo.f.set(instance, t);
+            
+            if (o != null && t != null) {
+                if (!Objects.equals(o, t)) {
+                    obj.setDirty();
+                }
+            }
+        } catch (Exception e) {
+            ExceptionUtils.caughtException(e);
+        }
+    }
+    
+    public static AttrInfoProperty createNew(AttrInfo a, Task instance, 
+            BTreeDataObject obj) {
+        Class c = a.f.getType();
+        if (Distribution.class.isAssignableFrom(c)) {
+            //return new DistributionProperty(a, instance);
+            AttrInfoProperty ai = new AttrInfoProperty<>(instance, a, obj);
+            ai.setPropertyEditorClass(DistributionEditor.class);
+            return ai;
+        } else {
+            /*if (Float.class.isAssignableFrom(c)) {
+                return new AttrInfoProperty<>((Float)instance, a);
+                //ai.setPropertyEditorClass(PropertyEditorSupport.class);
+            } else if (Double.class.isAssignableFrom(c)) {
+                return new AttrInfoProperty<>((Double)instance, a);
+                //ai.setPropertyEditorClass(PropertyEditorSupport.class);
+            } else if (Integer.class.isAssignableFrom(c)) {
+                return new AttrInfoProperty<>((Integer)instance, a);
+                //ai.setPropertyEditorClass(PropertyEditorSupport.class);
+            } else if (Character.class.isAssignableFrom(c)) {
+                return new AttrInfoProperty<>((Character)instance, a);
+            } else if (String.class.isAssignableFrom(c)) {
+                return new AttrInfoProperty<>((String)instance, a);
+            } else {*/
+                // Unknown Property
+                return new AttrInfoProperty<>(instance, a, obj);
+            //}
+        }
+    }
+}

+ 8 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/Bundle.properties

@@ -0,0 +1,8 @@
+DistributionProperties.distributionBox.toolTipText=This explains the type of Distribution, see libGDX-AI's JavaDoc for explanation
+DistributionProperties.jLabel1.text=Distribution Type:
+DistributionProperties.lblField1.text=Property 1:
+DistributionProperties.lblField2.text=Property 2:
+DistributionProperties.lblField3.text=Property 3:
+DistributionProperties.field1.text=1
+DistributionProperties.field2.text=2
+DistributionProperties.field3.text=3

+ 89 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/DistributionEditor.java

@@ -0,0 +1,89 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.navigator.properties;
+
+import com.badlogic.gdx.ai.utils.random.Distribution;
+import com.jme3.gde.behaviortrees.DistributionStringConverter;
+import java.awt.Component;
+import java.beans.PropertyEditorSupport;
+import org.openide.explorer.propertysheet.ExPropertyEditor;
+import org.openide.explorer.propertysheet.PropertyEnv;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public class DistributionEditor extends PropertyEditorSupport implements ExPropertyEditor {
+    PropertyEnv env;
+    AttrInfoProperty<Distribution> prop;
+    DistributionProperties uiPanel;
+    
+    @Override
+    public boolean supportsCustomEditor() {
+        return true;
+    }
+
+    @Override
+    public Component getCustomEditor() {
+        /* It's important to pass prop, because attachEnv might be called multiple times
+         * for different envs. See  http://bits.netbeans.org/7.4/javadoc/org-openide-explorer/org/openide/explorer/propertysheet/ExPropertyEditor.html#attachEnv(org.openide.explorer.propertysheet.PropertyEnv)
+         */
+        if (uiPanel == null) {
+            uiPanel = new DistributionProperties(env, prop, this);
+        }
+        
+        return uiPanel;
+    }
+
+    @Override
+    public String getAsText() {
+        return DistributionStringConverter.fromDistribution((Distribution)getValue());
+    }
+
+    @Override
+    public void attachEnv(PropertyEnv pe) {
+        this.env = pe;
+        prop = (AttrInfoProperty<Distribution>)pe.getFeatureDescriptor();
+    }
+
+    @Override
+    public Object getValue() {
+        if (uiPanel == null) {
+            return super.getValue();
+        }
+        
+        Distribution dist = uiPanel.toDistribution();
+        return dist;
+    }
+    
+    //@TODO: maybe change setValue to update the property, or does that happen automagically?
+}

File diff suppressed because it is too large
+ 69 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/DistributionProperties.form


+ 665 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/navigator/properties/DistributionProperties.java

@@ -0,0 +1,665 @@
+/*
+ *  Copyright (c) 2009-2018 jMonkeyEngine
+ *  All rights reserved.
+ * 
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ * 
+ *  * Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ *  * Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 
+ *  * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ * 
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ *  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.behaviortrees.navigator.properties;
+
+import com.badlogic.gdx.ai.utils.random.ConstantDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.ConstantFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.ConstantIntegerDistribution;
+import com.badlogic.gdx.ai.utils.random.ConstantLongDistribution;
+import com.badlogic.gdx.ai.utils.random.Distribution;
+import com.badlogic.gdx.ai.utils.random.DoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.FloatDistribution;
+import com.badlogic.gdx.ai.utils.random.GaussianDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.GaussianFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.IntegerDistribution;
+import com.badlogic.gdx.ai.utils.random.LongDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularIntegerDistribution;
+import com.badlogic.gdx.ai.utils.random.TriangularLongDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformDoubleDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformFloatDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformIntegerDistribution;
+import com.badlogic.gdx.ai.utils.random.UniformLongDistribution;
+import java.awt.Dimension;
+import java.awt.event.ItemEvent;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import org.openide.explorer.propertysheet.PropertyEnv;
+
+/**
+ * DistributionProperties is the JPanel displaying the Properties of the LibGDX
+ * AI Distributions.
+ * @author MeFisto94
+ */
+public class DistributionProperties extends JPanel {
+
+    protected DistributionEditor editor;
+    protected DistProperties restriction;
+    protected DistProperties actual;
+    protected AttrInfoProperty<Distribution> prop;
+    protected PropertyEnv env;
+    
+    /**
+     * Creates new form DistributionProperties
+     */
+    public DistributionProperties(PropertyEnv env, AttrInfoProperty<Distribution> prop, DistributionEditor editor) {
+        this.env = env;
+        this.prop = prop;
+        this.editor = editor;
+        //(Distribution)editor.getValue(); This is the actual value
+        // could editor.getValue() be null? @TODO: TEST
+        
+        /* Problem: Upon first invocation the surrounding modal dialog takes
+         * a size. If you have a distribution which is not Triangular, you
+         * lack the space for all 3 labels. Unfortunately dynamic resizing
+         * the dialog doesn't really work and it would be a bad UX anyway.
+         * That's why we set a size here programatically
+         */
+        setSize(431, 176);
+        setPreferredSize(new Dimension(431, 176));
+        
+        initComponents();
+        // Add Input Validation (couldn't be supported through the GUI Builder)
+        field1.getDocument().addDocumentListener(new DocListener());
+        field2.getDocument().addDocumentListener(new DocListener());
+        field3.getDocument().addDocumentListener(new DocListener());
+        
+        fromDistribution((Distribution)editor.getValue());
+        
+    }
+    
+    protected void updateLabelTexts(DistProperties props) {
+        switch(props.type) {
+            case CONSTANT:
+                lblField1.setText("Value: ");
+                break;
+                
+            case GAUSSIAN:
+                lblField1.setText("Mean: ");
+                lblField2.setText("Standard Deviation: ");
+                break;
+                
+            case TRIANGULAR:
+                lblField1.setText("Low: ");
+                lblField2.setText("High: ");
+                lblField3.setText("Mode: ");
+                break;
+                
+            case UNIFORM:
+                lblField1.setText("Low: ");
+                lblField2.setText("High: ");
+                break;
+        }
+    }
+    
+    public void fromDistribution(Distribution dist) {
+        // Find out which distributions are possible for this field
+        restriction = fromClass(prop.aInfo.f.getType());
+        actual = fromClass(dist.getClass());
+        
+        
+        // Only allow the right Distributions based on the restriction
+        jComboBox1.setModel(new DefaultComboBoxModel<>(fromRestriction(restriction)));
+        
+        // Select currently active dist
+        DistributionWrapper[] actualDist = fromRestriction(actual);
+
+        if (actual.value == null || actual.type == null || actualDist.length != 1) {
+            throw new IllegalStateException("Failed to determine the currently active distribution");
+        }
+        
+        for (int i = 0; i < jComboBox1.getItemCount(); i++) {
+            if (jComboBox1.getItemAt(i).getClazz().equals(actualDist[0].getClazz())) {
+                jComboBox1.setSelectedIndex(i);
+                break;
+            }
+        }
+        
+        updateLabelTexts(actual);
+        
+        // Hide the inappropriate labels
+        jComboBox1ItemStateChanged(new ItemEvent(jComboBox1, jComboBox1.getSelectedIndex(), 
+                jComboBox1.getSelectedItem(), ItemEvent.SELECTED));
+        
+        Class c = dist.getClass();
+        if (c.equals(ConstantFloatDistribution.class)) {
+            field1.setText(String.valueOf(((ConstantFloatDistribution)dist).getValue()));
+        } else if (c.equals(ConstantDoubleDistribution.class)) {
+            field1.setText(String.valueOf(((ConstantDoubleDistribution)dist).getValue()));
+        } else if (c.equals(ConstantIntegerDistribution.class)) {
+            field1.setText(String.valueOf(((ConstantIntegerDistribution)dist).getValue()));
+        } else if (c.equals(ConstantLongDistribution.class)) {
+            field1.setText(String.valueOf(((ConstantLongDistribution)dist).getValue()));
+        } else if (c.equals(GaussianDoubleDistribution.class)) {
+            field1.setText(String.valueOf(((GaussianDoubleDistribution)dist).getMean()));
+            field2.setText(String.valueOf(((GaussianDoubleDistribution)dist).getStandardDeviation()));
+        } else if (c.equals(GaussianFloatDistribution.class)) {
+            field1.setText(String.valueOf(((GaussianFloatDistribution)dist).getMean()));
+            field2.setText(String.valueOf(((GaussianFloatDistribution)dist).getStandardDeviation()));
+        } else if (c.equals(TriangularFloatDistribution.class)) {
+            field1.setText(String.valueOf(((TriangularFloatDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((TriangularFloatDistribution)dist).getHigh()));
+            field3.setText(String.valueOf(((TriangularFloatDistribution)dist).getMode()));
+        } else if (c.equals(TriangularDoubleDistribution.class)) {    
+            field1.setText(String.valueOf(((TriangularDoubleDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((TriangularDoubleDistribution)dist).getHigh()));
+            field3.setText(String.valueOf(((TriangularDoubleDistribution)dist).getMode()));
+        } else if (c.equals(TriangularIntegerDistribution.class)) {
+            field1.setText(String.valueOf(((TriangularIntegerDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((TriangularIntegerDistribution)dist).getHigh()));
+            field3.setText(String.valueOf(((TriangularIntegerDistribution)dist).getMode()));
+        } else if (c.equals(TriangularLongDistribution.class)) {
+            field1.setText(String.valueOf(((TriangularLongDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((TriangularLongDistribution)dist).getHigh()));
+            field3.setText(String.valueOf(((TriangularLongDistribution)dist).getMode()));
+        } else if (c.equals(UniformFloatDistribution.class)) {
+            field1.setText(String.valueOf(((UniformFloatDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((UniformFloatDistribution)dist).getHigh()));
+        } else if (c.equals(UniformDoubleDistribution.class)) {
+            field1.setText(String.valueOf(((UniformDoubleDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((UniformDoubleDistribution)dist).getHigh()));
+        } else if (c.equals(UniformIntegerDistribution.class)) {
+            field1.setText(String.valueOf(((UniformIntegerDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((UniformIntegerDistribution)dist).getHigh()));
+        } else if (c.equals(UniformLongDistribution.class)) {
+            field1.setText(String.valueOf(((UniformLongDistribution)dist).getLow()));
+            field2.setText(String.valueOf(((UniformLongDistribution)dist).getHigh()));
+        } else {
+            throw new IllegalStateException("Unknown Distribution, not supported");
+        }
+    }
+    
+    public Distribution toDistribution() {
+        Class c = ((DistributionWrapper)jComboBox1.getSelectedItem()).getClazz();
+        
+        if (c.equals(ConstantFloatDistribution.class)) {
+            return new ConstantFloatDistribution((Float)getValue(field1));
+        } else if (c.equals(ConstantDoubleDistribution.class)) {
+            return new ConstantDoubleDistribution((Double)getValue(field1));
+        } else if (c.equals(ConstantIntegerDistribution.class)) {
+            return new ConstantIntegerDistribution((Integer)getValue(field1));
+        } else if (c.equals(ConstantLongDistribution.class)) {
+            return new ConstantLongDistribution((Long)getValue(field1));
+        } else if (c.equals(GaussianDoubleDistribution.class)) {
+            return new GaussianDoubleDistribution((Double)getValue(field1), (Double)getValue(field2));
+        } else if (c.equals(GaussianFloatDistribution.class)) {
+            return new GaussianFloatDistribution((Float)getValue(field1), (Float)getValue(field2));
+        } else if (c.equals(TriangularFloatDistribution.class)) {
+            return new TriangularFloatDistribution((Float)getValue(field1), (Float)getValue(field2), (Float)getValue(field3));
+        } else if (c.equals(TriangularDoubleDistribution.class)) {
+            return new TriangularDoubleDistribution((Double)getValue(field1), (Double)getValue(field2), (Double)getValue(field3));
+        } else if (c.equals(TriangularIntegerDistribution.class)) {
+            return new TriangularIntegerDistribution((Integer)getValue(field1), (Integer)getValue(field2), (Integer)getValue(field3));
+        } else if (c.equals(TriangularLongDistribution.class)) {
+            return new TriangularLongDistribution((Long)getValue(field1), (Long)getValue(field2), (Long)getValue(field3));
+        } else if (c.equals(UniformFloatDistribution.class)) {
+            return new UniformFloatDistribution((Float)getValue(field1), (Float)getValue(field2));
+        } else if (c.equals(UniformDoubleDistribution.class)) {
+            return new UniformDoubleDistribution((Double)getValue(field1), (Double)getValue(field2));
+        } else if (c.equals(UniformIntegerDistribution.class)) {
+            return new UniformIntegerDistribution((Integer)getValue(field1), (Integer)getValue(field2));
+        } else if (c.equals(UniformLongDistribution.class)) {
+            return new UniformLongDistribution((Long)getValue(field1), (Long)getValue(field2));
+        } else {
+            throw new IllegalStateException("Unknown Distribution, not supported");
+        }
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        jComboBox1 = new javax.swing.JComboBox<>();
+        jLabel1 = new javax.swing.JLabel();
+        field1 = new javax.swing.JTextField();
+        field2 = new javax.swing.JTextField();
+        field3 = new javax.swing.JTextField();
+        lblField1 = new javax.swing.JLabel();
+        lblField2 = new javax.swing.JLabel();
+        lblField3 = new javax.swing.JLabel();
+
+        jComboBox1.setName("distributionBox"); // NOI18N
+        jComboBox1.addItemListener(new java.awt.event.ItemListener() {
+            public void itemStateChanged(java.awt.event.ItemEvent evt) {
+                jComboBox1ItemStateChanged(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(DistributionProperties.class, "DistributionProperties.jLabel1.text")); // NOI18N
+
+        field1.setText(org.openide.util.NbBundle.getMessage(DistributionProperties.class, "DistributionProperties.field1.text")); // NOI18N
+
+        field2.setText(org.openide.util.NbBundle.getMessage(DistributionProperties.class, "DistributionProperties.field2.text")); // NOI18N
+
+        field3.setText(org.openide.util.NbBundle.getMessage(DistributionProperties.class, "DistributionProperties.field3.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(lblField1, org.openide.util.NbBundle.getMessage(DistributionProperties.class, "DistributionProperties.lblField1.text")); // NOI18N
+        lblField1.setName("lblField1"); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(lblField2, org.openide.util.NbBundle.getMessage(DistributionProperties.class, "DistributionProperties.lblField2.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(lblField3, org.openide.util.NbBundle.getMessage(DistributionProperties.class, "DistributionProperties.lblField3.text")); // NOI18N
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addGap(14, 14, 14)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(jLabel1)
+                    .addComponent(lblField1)
+                    .addComponent(lblField2)
+                    .addComponent(lblField3))
+                .addGap(24, 24, 24)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(field3, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 280, Short.MAX_VALUE)
+                    .addComponent(field2, javax.swing.GroupLayout.Alignment.TRAILING)
+                    .addComponent(field1)
+                    .addComponent(jComboBox1, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+                .addGap(16, 16, 16))
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addGap(14, 14, 14)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(jLabel1))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(field1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(lblField1))
+                .addGap(18, 18, 18)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(field2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(lblField2))
+                .addGap(18, 18, 18)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(field3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(lblField3))
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void jComboBox1ItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_jComboBox1ItemStateChanged
+        //@TODO Validators for the fields, labels for the files
+        DistProperties props = fromClass(((DistributionWrapper)evt.getItem()).getClazz());
+        updateLabelTexts(props);
+        switch (props.type) {
+            case CONSTANT:
+                lblField1.setVisible(true);
+                field1.setVisible(true);
+                lblField2.setVisible(false);
+                field2.setVisible(false);
+                lblField3.setVisible(false);
+                field3.setVisible(false);
+                break;
+                
+            case TRIANGULAR:
+                lblField1.setVisible(true);
+                field1.setVisible(true);
+                lblField2.setVisible(true);
+                field2.setVisible(true);
+                lblField3.setVisible(true);
+                field3.setVisible(true);                
+                break;
+                
+            case UNIFORM:
+            case GAUSSIAN:
+                lblField1.setVisible(true);
+                field1.setVisible(true);
+                lblField2.setVisible(true);
+                field2.setVisible(true);
+                lblField3.setVisible(false);
+                field3.setVisible(false);
+                break;
+        }
+    }//GEN-LAST:event_jComboBox1ItemStateChanged
+
+    /**
+     * Returns the value of this TextField parsed as Type given by the restriction.
+     * Returns null when conversation fails.
+     * @param field The relevant text field
+     * @return a Float, Double, Long or Integer 
+     */
+    protected Object getValue(JTextField field) {
+        switch (restriction.value) {
+            case DOUBLE:
+                try {
+                    return Double.parseDouble(field.getText());
+                } catch (NumberFormatException nfe) {
+                    return null;
+                }
+
+            case FLOAT:
+                try {
+                    return Float.parseFloat(field.getText());
+                } catch (NumberFormatException nfe) {
+                    return null;
+                }
+
+            case INTEGER:
+                try {
+                    return Integer.parseInt(field.getText());
+                } catch (NumberFormatException nfe) {
+                    return null;
+                }
+
+            case LONG:
+                try {
+                    return Long.parseLong(field.getText());
+                } catch (NumberFormatException nfe) {
+                    return null;
+                }
+        }
+        
+        return null;
+    }
+    
+    protected void validateProps() {
+        env.setState(PropertyEnv.STATE_VALID);
+        
+        for (JTextField field: new JTextField[] {field1, field2, field3}) {
+            if (field.isVisible()) {
+                Object o = getValue(field);
+                if (o == null) {
+                    env.setState(PropertyEnv.STATE_INVALID);
+                    return;
+                }
+            }
+        }
+        
+        editor.setValue(toDistribution());
+    }
+        
+    protected static enum DistType {
+        CONSTANT,
+        GAUSSIAN, // Only Float and Double
+        TRIANGULAR,
+        UNIFORM
+    }
+    
+    protected static enum DistValue {
+        FLOAT,
+        DOUBLE,
+        INTEGER,
+        LONG
+    }
+    
+    protected static class DistProperties {
+        public DistType type;
+        public DistValue value;
+    }
+    
+    protected static class DistributionWrapper {
+        private final Class clazz;
+
+        public DistributionWrapper(Class clazz) {
+            this.clazz = clazz;
+        }
+
+        @Override
+        public String toString() {
+            String str = clazz.getSimpleName().substring(0, clazz.getSimpleName().indexOf("Distribution"));
+            for (int i = 1; i < str.length(); i++) {
+                if (Character.isUpperCase(str.codePointAt(i))) {
+                    return str.substring(0, i) + " " + str.substring(i, str.length());
+                }
+            }
+            
+            // No second word found to space
+            return str;
+        }
+
+        public Class getClazz() {
+            return clazz;
+        }
+        
+        public Distribution newInstance() {
+            try {
+                return (Distribution)clazz.newInstance();
+            } catch (Exception e) {
+                return null;
+            }
+        }
+    }
+    
+    protected static DistProperties fromClass(Class c) {
+        DistProperties props = new DistProperties();
+
+        // Find the value
+        if (FloatDistribution.class.isAssignableFrom(c)) {
+            props.value = DistValue.FLOAT;
+        } else if (DoubleDistribution.class.isAssignableFrom(c)) {
+            props.value = DistValue.DOUBLE;
+        } else if (IntegerDistribution.class.isAssignableFrom(c)) {
+            props.value = DistValue.INTEGER;
+        } else if (LongDistribution.class.isAssignableFrom(c)) {
+            props.value = DistValue.LONG;
+        }
+
+        // Find the dist, Value is also set as ConstantX extends X.
+        // but there are cases where the actual class really is FloatDistribution
+        if (ConstantFloatDistribution.class.isAssignableFrom(c)   || 
+            ConstantDoubleDistribution.class.isAssignableFrom(c)  ||
+            ConstantIntegerDistribution.class.isAssignableFrom(c) ||
+            ConstantLongDistribution.class.isAssignableFrom(c)) {
+            props.type = DistType.CONSTANT;
+        } else if (GaussianFloatDistribution.class.isAssignableFrom(c)   || 
+                   GaussianDoubleDistribution.class.isAssignableFrom(c)) {
+            props.type = DistType.GAUSSIAN;
+        } else if (TriangularFloatDistribution.class.isAssignableFrom(c)   || 
+                   TriangularDoubleDistribution.class.isAssignableFrom(c)  ||
+                   TriangularIntegerDistribution.class.isAssignableFrom(c) ||
+                   TriangularLongDistribution.class.isAssignableFrom(c)) {
+            props.type = DistType.TRIANGULAR;
+        } else if (UniformFloatDistribution.class.isAssignableFrom(c)   || 
+                   UniformDoubleDistribution.class.isAssignableFrom(c)  ||
+                   UniformIntegerDistribution.class.isAssignableFrom(c) ||
+                   UniformLongDistribution.class.isAssignableFrom(c)) {
+            props.type = DistType.UNIFORM;
+        }
+
+        return props;
+    }
+    
+    protected static DistributionWrapper[] fromRestriction(DistProperties props) {
+        if (props.value != null) {
+            if (props.type == null) {
+                switch (props.value) {
+                    case FLOAT:
+                        return new DistributionWrapper[] { 
+                            new DistributionWrapper(ConstantFloatDistribution.class),
+                            new DistributionWrapper(GaussianFloatDistribution.class),
+                            new DistributionWrapper(TriangularFloatDistribution.class),
+                            new DistributionWrapper(UniformFloatDistribution.class) 
+                        };
+                    case DOUBLE:
+                        return new DistributionWrapper[] { 
+                            new DistributionWrapper(ConstantDoubleDistribution.class),
+                            new DistributionWrapper(GaussianDoubleDistribution.class),
+                            new DistributionWrapper(TriangularDoubleDistribution.class),
+                            new DistributionWrapper(UniformDoubleDistribution.class) 
+                        };
+                    case INTEGER:
+                        return new DistributionWrapper[] { 
+                            new DistributionWrapper(ConstantIntegerDistribution.class),
+                            new DistributionWrapper(TriangularIntegerDistribution.class),
+                            new DistributionWrapper(UniformIntegerDistribution.class)
+                        };
+                    case LONG:
+                        return new DistributionWrapper[] { 
+                            new DistributionWrapper(ConstantLongDistribution.class),
+                            new DistributionWrapper(TriangularLongDistribution.class),
+                            new DistributionWrapper(UniformLongDistribution.class)
+                        };
+                }
+            } else {
+                switch (props.value) {
+                    case FLOAT:
+                        switch (props.type) {
+                            case CONSTANT:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(ConstantFloatDistribution.class)
+                                };
+                            case GAUSSIAN:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(GaussianFloatDistribution.class)
+                                };
+                            case TRIANGULAR:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(TriangularFloatDistribution.class)
+                                };
+                            case UNIFORM:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(UniformFloatDistribution.class)
+                                };                                
+                        }
+                        break;
+                        
+                    case DOUBLE:
+                        switch (props.type) {
+                            case CONSTANT:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(ConstantDoubleDistribution.class)
+                                };
+                            case GAUSSIAN:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(GaussianDoubleDistribution.class)
+                                };
+                            case TRIANGULAR:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(TriangularDoubleDistribution.class)
+                                };
+                            case UNIFORM:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(UniformDoubleDistribution.class)
+                                };                                
+                        }
+                        break;
+                        
+                    case INTEGER:
+                        switch (props.type) {
+                            case CONSTANT:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(ConstantIntegerDistribution.class)
+                                };
+                            case TRIANGULAR:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(TriangularIntegerDistribution.class)
+                                };
+                            case UNIFORM:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(UniformIntegerDistribution.class)
+                                };                                
+                        }
+                        break;
+                        
+                    case LONG:
+                        switch (props.type) {
+                            case CONSTANT:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(ConstantLongDistribution.class)
+                                };
+                            case TRIANGULAR:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(TriangularLongDistribution.class)
+                                };
+                            case UNIFORM:
+                                return new DistributionWrapper[] {
+                                    new DistributionWrapper(UniformLongDistribution.class)
+                                };                                
+                        }
+                        break;
+                        
+                }
+            }
+        }
+        
+        // else everything is possible
+        return new DistributionWrapper[] {
+            new DistributionWrapper(ConstantFloatDistribution.class),
+            new DistributionWrapper(GaussianFloatDistribution.class),
+            new DistributionWrapper(TriangularFloatDistribution.class),
+            new DistributionWrapper(UniformFloatDistribution.class),
+            new DistributionWrapper(ConstantDoubleDistribution.class),
+            new DistributionWrapper(GaussianDoubleDistribution.class),
+            new DistributionWrapper(TriangularDoubleDistribution.class),
+            new DistributionWrapper(UniformDoubleDistribution.class),
+            new DistributionWrapper(ConstantIntegerDistribution.class),
+            new DistributionWrapper(TriangularIntegerDistribution.class),
+            new DistributionWrapper(UniformIntegerDistribution.class),
+            new DistributionWrapper(ConstantLongDistribution.class),
+            new DistributionWrapper(TriangularLongDistribution.class),
+            new DistributionWrapper(UniformLongDistribution.class)
+        };
+    }
+    
+    private final class DocListener implements DocumentListener {
+        @Override
+        public void insertUpdate(DocumentEvent e) {
+            validateProps();
+        }
+
+        @Override
+        public void removeUpdate(DocumentEvent e) {
+            validateProps();
+        }
+
+        @Override
+        public void changedUpdate(DocumentEvent e) {
+            validateProps();
+        }
+        
+    }
+    
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JTextField field1;
+    private javax.swing.JTextField field2;
+    private javax.swing.JTextField field3;
+    private javax.swing.JComboBox<DistributionWrapper> jComboBox1;
+    private javax.swing.JLabel jLabel1;
+    private javax.swing.JLabel lblField1;
+    private javax.swing.JLabel lblField2;
+    private javax.swing.JLabel lblField3;
+    // End of variables declaration//GEN-END:variables
+}

+ 5 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/Bundle.properties

@@ -0,0 +1,5 @@
+# To change this license header, choose License Headers in Project Properties.
+# To change this template file, choose Tools | Templates
+# and open the template in the editor.
+
+Sequential.Description=The Sequential Node executes every task in sequence and returns sucessful when all other subtasks where sucessful.<br>Thus it's comparable to an AND Gate.<br>A Sequential node executes the subtasks from left to right (or random, if that is selected)

+ 31 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/CustomBeanTreeView.java

@@ -0,0 +1,31 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.gde.behaviortrees.nodes;
+
+import com.jme3.gde.behaviortrees.navigator.BTreeNavigatorPanel;
+import java.beans.PropertyVetoException;
+import org.openide.explorer.ExplorerManager;
+import org.openide.explorer.view.BeanTreeView;
+import org.openide.nodes.Node;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public class CustomBeanTreeView extends BeanTreeView {
+    protected BTreeNavigatorPanel nav;
+    
+    public CustomBeanTreeView(BTreeNavigatorPanel nav) {
+        this.nav = nav;
+    }
+    
+    @Override
+    protected void selectionChanged(Node[] nodes, ExplorerManager em) throws PropertyVetoException {
+        super.selectionChanged(nodes, em);
+        nav.selectionChanged(nodes, em);
+    }
+    
+}

+ 85 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/DynamicOutputNodePanel.java

@@ -0,0 +1,85 @@
+package com.jme3.gde.behaviortrees.nodes;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.editor.TreeNodePanel;
+import com.jme3.gde.core.editor.nodes.Connection;
+import com.jme3.gde.core.editor.nodes.ConnectionEndpoint;
+import com.jme3.gde.core.editor.nodes.Diagram;
+import java.util.ResourceBundle;
+import javax.swing.JLabel;
+
+/**
+ * @author MeFisto94
+ */
+public abstract class DynamicOutputNodePanel extends TreeNodePanel {
+    public static final ResourceBundle bundle = ResourceBundle.getBundle("com/jme3/gde/behaviortrees/nodes/Bundle");
+    protected boolean dynamic = true;
+    
+    public DynamicOutputNodePanel(Diagram dia, Task task, NodeType nodeType, int numInputs, int numOutputs, String nameAndTitle) {
+        super(task, dia, nodeType, numInputs, numOutputs, nameAndTitle);
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+    
+    public DynamicOutputNodePanel(Diagram dia, Task task, NodeType nodeType) {
+        this(dia, task, nodeType, 1, 1, "DynamicOutputNodePanel");
+    }
+
+    @Override
+    protected void onDisconnect(Connection conn) {
+        super.onDisconnect(conn);
+        if (dynamic && conn.getStart().getNode().equals(this)) {
+            // Our goal is to find the first unconnected dot which is not the last one
+            int idx = findFirstUnconnected();
+            if (idx != -1) {
+                outputDots.remove(idx);
+                outputLabels.remove(idx);
+                for (int i = idx; i < outputLabels.size(); i++) {
+                    outputLabels.get(i).setText("" + (i + inputLabels.size()));
+                }
+            }
+            
+            initComponents();
+            invalidate();
+            setSize(getPreferredSize());
+        }
+    }
+
+    @Override
+    protected void onConnect(Connection conn) {
+        super.onConnect(conn);
+        if (dynamic && conn.getStart().getNode().equals(this)) {
+            JLabel label = createLabel("" + num++, ConnectionEndpoint.ParamType.Output);
+            ConnectionEndpoint dot = createConnectionEndpoint("", ConnectionEndpoint.ParamType.Output, "");
+            //dot.setBorder(new LineBorder(Color.GREEN));
+            outputLabels.add(label);
+            outputDots.add(dot);
+
+            initComponents();
+            invalidate();
+            setSize(getPreferredSize());
+        }
+    }
+    
+    protected int findFirstUnconnected() {
+        // - 1 so the last entry is not iterated.
+        for (int i = 0; i < outputDots.size() - 1; i++) {
+            ConnectionEndpoint ce = outputDots.get(i);
+            if (!ce.isConnected()) {
+                return i;
+            }
+        }
+        
+        return -1;
+    }
+
+    public void setDynamic(boolean dynamic) {
+        this.dynamic = dynamic;
+    }
+
+    public boolean isDynamic() {
+        return dynamic;
+    }
+
+}

+ 29 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/LeafTreeNodePanel.java

@@ -0,0 +1,29 @@
+package com.jme3.gde.behaviortrees.nodes;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.editor.TreeNodePanel;
+import com.jme3.gde.core.editor.nodes.Diagram;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public class LeafTreeNodePanel extends TreeNodePanel {
+
+    public LeafTreeNodePanel(Diagram dia, Task task, String classPackage, String className) {
+        super(task, dia, NodeType.LeafTask, 1, 0, className);
+        setToolTipText(classPackage + "." + className);
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+    
+    public LeafTreeNodePanel(Diagram dia, Task task, String classPackage, String className, TreeNodePanel guardedPanel) {
+        super(task, dia, NodeType.LeafTask, 1, 0, className, guardedPanel);
+        setToolTipText(classPackage + "." + className);
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+    
+}

+ 33 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/SequentialNodePanel.java

@@ -0,0 +1,33 @@
+package com.jme3.gde.behaviortrees.nodes;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.core.editor.nodes.Diagram;
+
+/**
+ * The Sequential node allows us to do multiple tasks one after another.
+ * There is: Sequence [Until one fails], Selector [Until one succeeds], Parallel
+ * etc (Every task extending com.badlogic.gdx.ai.btree.BranchTask)
+ * @author MeFisto94
+ */
+public class SequentialNodePanel extends DynamicOutputNodePanel {
+    
+    public SequentialNodePanel(Diagram dia, Task task) {
+        this(dia, task, 1);
+    }
+    
+    public SequentialNodePanel(Diagram dia, Task task, NodeType nodeType, int numOutputs, String nameAndTitle) {
+        super(dia, task, nodeType, 1, numOutputs, nameAndTitle);
+        setToolTipText(bundle.getString("Sequential.Description"));
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+    
+    public SequentialNodePanel(Diagram dia, Task task, int numOutputs) {
+        this(dia, task, NodeType.Sequential, numOutputs, task.getClass().getSimpleName());
+    }
+    
+    public SequentialNodePanel(Diagram dia, Task task, int numOutputs, String nameAndTitle) {
+        this(dia, task, NodeType.Sequential, numOutputs, nameAndTitle);
+    }
+}

+ 24 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/BuiltinLeafTask.java

@@ -0,0 +1,24 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.gde.behaviortrees.nodes.impl;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.editor.TreeNodePanel;
+import com.jme3.gde.core.editor.nodes.Diagram;
+
+/**
+ * Wait, Failure, Success.
+ * @author MeFisto94
+ */
+public class BuiltinLeafTask extends TreeNodePanel {
+
+    public BuiltinLeafTask(Diagram dia, Task task) {
+        super(task, dia, NodeType.BuiltinLeafTask, 1, 0, task.getClass().getSimpleName());
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+}

+ 21 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/DecoratorNodePanel.java

@@ -0,0 +1,21 @@
+package com.jme3.gde.behaviortrees.nodes.impl;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.editor.TreeNodePanel;
+import com.jme3.gde.core.editor.nodes.Diagram;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public class DecoratorNodePanel extends TreeNodePanel {
+
+    public DecoratorNodePanel(Diagram dia, Task task) {
+        super(task, dia, NodeType.Decorator, 1, 1, task.getClass().getSimpleName());
+        setToolTipText(task.getClass().getSimpleName());
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+    
+}

+ 19 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/ParallelNodePanel.java

@@ -0,0 +1,19 @@
+package com.jme3.gde.behaviortrees.nodes.impl;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.nodes.SequentialNodePanel;
+import com.jme3.gde.core.editor.nodes.Diagram;
+
+/**
+ *
+ * @author marc
+ */
+public class ParallelNodePanel extends SequentialNodePanel {
+    public ParallelNodePanel(Diagram dia, Task task, int numOutputs) {
+        super(dia, task, NodeType.Parallel, numOutputs, "Parallel");
+        //setToolTipText(bundle.getString("Sequential.Description"));
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+}

+ 21 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/RootNodePanel.java

@@ -0,0 +1,21 @@
+package com.jme3.gde.behaviortrees.nodes.impl;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.editor.TreeNodePanel;
+import com.jme3.gde.core.editor.nodes.Diagram;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public class RootNodePanel extends TreeNodePanel {
+
+    public RootNodePanel(Diagram dia, Task task) {
+        super(task, dia, NodeType.Root, 0, 1, "Root");
+        setToolTipText("Root");
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+    
+}

+ 22 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/SelectorNodePanel.java

@@ -0,0 +1,22 @@
+package com.jme3.gde.behaviortrees.nodes.impl;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.nodes.SequentialNodePanel;
+import com.jme3.gde.core.editor.nodes.Diagram;
+import java.awt.Color;
+
+/**
+ *
+ * @author marc
+ */
+public class SelectorNodePanel extends SequentialNodePanel {
+    
+    public SelectorNodePanel(Diagram dia, Task task, int numOutputs) {
+        super(dia, task, numOutputs, "Selector");
+        backgroundColor = new Color(220, 40, 220);
+        //setToolTipText(bundle.getString("Sequential.Description"));
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+}

+ 20 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/nodes/impl/SequenceNodePanel.java

@@ -0,0 +1,20 @@
+package com.jme3.gde.behaviortrees.nodes.impl;
+
+import com.badlogic.gdx.ai.btree.Task;
+import com.jme3.gde.behaviortrees.nodes.SequentialNodePanel;
+import com.jme3.gde.core.editor.nodes.Diagram;
+
+/**
+ *
+ * @author MeFisto94
+ */
+public class SequenceNodePanel extends SequentialNodePanel {
+    
+    public SequenceNodePanel(Diagram dia, Task task, int numOutputs) {
+        super(dia, task, numOutputs, "Sequence");
+        //setToolTipText(bundle.getString("Sequential.Description"));
+        setSize(getPreferredSize()); // BUT WHY?
+        revalidate();
+        repaint();
+    }
+}

+ 35 - 0
jme3-behaviortrees/src/com/jme3/gde/behaviortrees/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+@TemplateRegistration(folder = "AI", content = "BehaviorTree.btree", displayName="BehaviorTree Template")
+package com.jme3.gde.behaviortrees;
+
+import org.netbeans.api.templates.TemplateRegistration;

Some files were not shown because too many files changed in this diff