Browse Source

Merge branch 'master' into fix_multi_select

Rickard Edén 7 months ago
parent
commit
559890b288
35 changed files with 1645 additions and 658 deletions
  1. 2 1
      BasicGameTemplate/nbproject/project.properties
  2. 1 0
      JME3TestsTemplate/nbproject/project.properties
  3. 1 0
      build.gradle
  4. 1 1
      gradle.properties
  5. 97 48
      jme3-core/src/com/jme3/gde/core/appstates/NewAppStateVisualPanel1.java
  6. 13 43
      jme3-core/src/com/jme3/gde/core/assets/ExternalChangeScanner.java
  7. 22 22
      jme3-core/src/com/jme3/gde/core/assets/ProjectAssetManager.java
  8. 58 0
      jme3-core/src/com/jme3/gde/core/assets/RefreshJmeSpatial.java
  9. 233 0
      jme3-core/src/com/jme3/gde/core/assets/actions/MergeAnimationsAction.java
  10. 1 3
      jme3-core/src/com/jme3/gde/core/sceneexplorer/SceneExplorerTopComponent.java
  11. 83 61
      jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/actions/impl/NewCustomControlVisualPanel1.java
  12. 9 0
      jme3-glsl-highlighter/nbproject/project.xml
  13. 150 0
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/GlslCompletionProvider.java
  14. 123 0
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/DefaultCompletionItem.java
  15. 50 0
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/FunctionCompletionItem.java
  16. 50 0
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/KeywordCompletionItem.java
  17. 50 0
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/TypeCompletionItem.java
  18. 50 0
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/VariableCompletionItem.java
  19. BIN
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/class_16.png
  20. BIN
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/field_16.png
  21. BIN
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/method_16.png
  22. BIN
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/method_static_protected_16.png
  23. 459 447
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/lexer/GlslKeywordLibrary.java
  24. 20 26
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/lexer/GlslLexer.java
  25. 147 0
      jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/util/Trie.java
  26. 1 0
      jme3-scenecomposer/src/com/jme3/gde/scenecomposer/Bundle.properties
  27. 14 1
      jme3-scenecomposer/src/com/jme3/gde/scenecomposer/layer.xml
  28. BIN
      jme3-templates/src/com/jme3/gde/templates/GradleDesktopGameProject.zip
  29. 6 2
      jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/AdditionalLibrary.java
  30. 2 1
      jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/Bundle.properties
  31. 1 1
      jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/NetworkingLibrary.java
  32. 1 1
      jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/PhysicsLibrary.java
  33. BIN
      nbi/antlib/nbi-ant-tasks.jar
  34. BIN
      nbi/antlib/nbi-engine.jar
  35. BIN
      nbi/antlib/nbi-registries-management.jar

+ 2 - 1
BasicGameTemplate/nbproject/project.properties

@@ -44,7 +44,8 @@ javac.classpath=\
     ${libs.jme3-terrain.classpath}:\
     ${libs.jme3-terrain.classpath}:\
     ${libs.jme3-jbullet.classpath}:\
     ${libs.jme3-jbullet.classpath}:\
     ${libs.jme3-awt-dialogs.classpath}:\
     ${libs.jme3-awt-dialogs.classpath}:\
-    ${libs.jme3-plugins-json.classpath}
+    ${libs.jme3-plugins-json.classpath}:\
+	${libs.jme3-plugins-json-gson.classpath}
 # Space-separated list of extra javac options
 # Space-separated list of extra javac options
 javac.compilerargs=
 javac.compilerargs=
 javac.deprecation=false
 javac.deprecation=false

+ 1 - 0
JME3TestsTemplate/nbproject/project.properties

@@ -27,6 +27,7 @@ jar.compress=false
 javac.classpath=\
 javac.classpath=\
     ${libs.jme3-jogg.classpath}:\
     ${libs.jme3-jogg.classpath}:\
     ${libs.jme3-plugins-json.classpath}:\
     ${libs.jme3-plugins-json.classpath}:\
+	${libs.jme3-plugins-json-gson.classpath}:\
     ${libs.jme3-networking.classpath}:\
     ${libs.jme3-networking.classpath}:\
     ${libs.jme3-plugins.classpath}:\
     ${libs.jme3-plugins.classpath}:\
     ${libs.jme3-core.classpath}:\
     ${libs.jme3-core.classpath}:\

+ 1 - 0
build.gradle

@@ -40,6 +40,7 @@ dependencies {
     corelibs dep("org.jmonkeyengine:jme3-desktop:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-desktop:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-awt-dialogs:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-awt-dialogs:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-plugins-json:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-plugins-json:$jmeVersion-$jmeVersionTag", true, true)
+	corelibs dep("org.jmonkeyengine:jme3-plugins-json-gson:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-lwjgl:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-lwjgl:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-effects:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("org.jmonkeyengine:jme3-effects:$jmeVersion-$jmeVersionTag", true, true)
     corelibs dep("com.github.stephengold:Minie:8.2.0", true, true) // replacement for bullet-native
     corelibs dep("com.github.stephengold:Minie:8.2.0", true, true) // replacement for bullet-native

+ 1 - 1
gradle.properties

@@ -8,4 +8,4 @@ jmeVersionTag = stable
 #jmeVersionTagID = 0
 #jmeVersionTagID = 0
 
 
 # Path for downloading NetBeans Base
 # Path for downloading NetBeans Base
-netbeansUrl = https://archive.apache.org/dist/netbeans/netbeans/23/netbeans-23-bin.zip
+netbeansUrl = https://archive.apache.org/dist/netbeans/netbeans/24/netbeans-24-bin.zip

+ 97 - 48
jme3-core/src/com/jme3/gde/core/appstates/NewAppStateVisualPanel1.java

@@ -1,5 +1,5 @@
 /*
 /*
- *  Copyright (c) 2009-2010 jMonkeyEngine
+ *  Copyright (c) 2009-2024 jMonkeyEngine
  *  All rights reserved.
  *  All rights reserved.
  * 
  * 
  *  Redistribution and use in source and binary forms, with or without
  *  Redistribution and use in source and binary forms, with or without
@@ -31,13 +31,16 @@
  */
  */
 package com.jme3.gde.core.appstates;
 package com.jme3.gde.core.appstates;
 
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.EnumSet;
-import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.List;
 import java.util.Set;
 import java.util.Set;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
 import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Types;
 import javax.swing.JPanel;
 import javax.swing.JPanel;
 import org.netbeans.api.java.classpath.ClassPath;
 import org.netbeans.api.java.classpath.ClassPath;
 import org.netbeans.api.java.project.JavaProjectConstants;
 import org.netbeans.api.java.project.JavaProjectConstants;
@@ -48,11 +51,11 @@ import org.netbeans.api.java.source.ClasspathInfo;
 import org.netbeans.api.java.source.CompilationController;
 import org.netbeans.api.java.source.CompilationController;
 import org.netbeans.api.java.source.ElementHandle;
 import org.netbeans.api.java.source.ElementHandle;
 import org.netbeans.api.java.source.JavaSource;
 import org.netbeans.api.java.source.JavaSource;
-import org.netbeans.api.java.source.JavaSource.Phase;
-import org.netbeans.api.java.source.Task;
 import org.netbeans.api.project.Project;
 import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectUtils;
 import org.netbeans.api.project.SourceGroup;
 import org.netbeans.api.project.SourceGroup;
 import org.netbeans.api.project.Sources;
 import org.netbeans.api.project.Sources;
+import org.openide.filesystems.FileObject;
 import org.openide.util.Exceptions;
 import org.openide.util.Exceptions;
 
 
 @SuppressWarnings({"unchecked", "rawtypes"})
 @SuppressWarnings({"unchecked", "rawtypes"})
@@ -82,57 +85,103 @@ public final class NewAppStateVisualPanel1 extends JPanel {
     }
     }
 
 
     private List<String> getSources() {
     private List<String> getSources() {
-        Sources sources = proj.getLookup().lookup(Sources.class);
-        final List<String> list = new LinkedList<String>();
-        if (sources != null) {
+        Project root = ProjectUtils.rootOf(proj);
+        Set<Project> containedProjects = ProjectUtils.getContainedProjects(root, true);
+        List<Project> projects = new ArrayList<>();
+        projects.add(root);
+        if (containedProjects != null) {
+            projects.addAll(containedProjects);
+        }
+        if (projects.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<String> list = new ArrayList<>();
+        for (Project project : projects) {
+            Sources sources = ProjectUtils.getSources(project);
+            if (sources == null) {
+                continue;
+            }
+
             SourceGroup[] groups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
             SourceGroup[] groups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
-            if (groups != null) {
-                for (SourceGroup sourceGroup : groups) {
-                    ClasspathInfo cpInfo = ClasspathInfo.create(ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.BOOT),
-                            ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.COMPILE),
-                            ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.SOURCE));
-
-                    Set<SearchScope> set = EnumSet.of(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();
-                        JavaSource js = JavaSource.create(cpInfo);
-                        try {
-                            js.runUserActionTask(new Task<CompilationController>() {
-                                public void run(CompilationController control)
-                                        throws Exception {
-                                    control.toPhase(Phase.RESOLVED);
-                                    //TODO: check with proper casting check.. gotta get TypeMirror of Control interface..
-//                                    TypeUtilities util = control.getTypeUtilities();//.isCastable(Types., null)
-//                                    util.isCastable(null, null);
-                                    TypeElement elem = elementHandle.resolve(control);
-                                    if (elem != null) {
-                                        List<? extends TypeMirror> interfaces = elem.getInterfaces();
-                                        for (TypeMirror typeMirror : interfaces) {
-                                            String interfaceName = typeMirror.toString();
-                                            if ("com.jme3.app.state.AppState".equals(interfaceName)) {
-                                                list.add(elem.getQualifiedName().toString());
-                                            }
-                                        }
-                                        TypeMirror superClass = elem.getSuperclass();
-                                        String superClassName = superClass.toString();
-                                        if ("com.jme3.app.state.AbstractAppState".equals(superClassName)) {
-                                            list.add(elem.getQualifiedName().toString());
-                                        }
-                                    }
-                                }
-                            }, false);
-                        } catch (Exception ioe) {
-                            Exceptions.printStackTrace(ioe);
-                        }
-                    }
+            if (groups == null) {
+                continue;
+            }
 
 
+            for (SourceGroup sourceGroup : groups) {
+                FileObject rootFolder = sourceGroup.getRootFolder();
+                ClasspathInfo cpInfo = ClasspathInfo.create(
+                        ClassPath.getClassPath(rootFolder, ClassPath.BOOT),
+                        ClassPath.getClassPath(rootFolder, ClassPath.COMPILE),
+                        ClassPath.getClassPath(rootFolder, ClassPath.SOURCE)
+                );
+
+                Set<SearchScope> set = EnumSet.of(ClassIndex.SearchScope.SOURCE);
+                Set<ElementHandle<TypeElement>> types = cpInfo.getClassIndex().getDeclaredTypes("", NameKind.PREFIX, set);
+                for (ElementHandle<TypeElement> elementHandle : types) {
+                    JavaSource js = JavaSource.create(cpInfo);
+                    try {
+                        js.runUserActionTask((CompilationController control) -> {
+                            control.toPhase(JavaSource.Phase.RESOLVED);
+                            TypeElement elem = elementHandle.resolve(control);
+                            if (elem != null && doesInheritFromAppState(elem, control.getTypes())) {
+                                list.add(elem.getQualifiedName().toString());
+                            }
+                        }, false);
+                    } catch (IOException ioe) {
+                        Exceptions.printStackTrace(ioe);
+                    }
                 }
                 }
             }
             }
         }
         }
+
         return list;
         return list;
     }
     }
 
 
+    /**
+     * Checks recursively if a type inherits from or implements any
+     * AppState-related class/interface.
+     */
+    private boolean doesInheritFromAppState(TypeElement type, Types typeUtils) {
+        if (type == null) {
+            return false;
+        }
+
+        // Check interfaces
+        for (TypeMirror iface : type.getInterfaces()) {
+            if (isAppStateType(iface)) {
+                return true;
+            }
+            if (doesInheritFromAppState((TypeElement) typeUtils.asElement(iface), typeUtils)) {
+                return true;
+            }
+        }
+
+        // Check superclass
+        TypeMirror superClass = type.getSuperclass();
+        if (superClass != null && superClass.getKind() != TypeKind.NONE) {
+            if (isAppStateType(superClass)) {
+                return true;
+            }
+            return doesInheritFromAppState((TypeElement) typeUtils.asElement(superClass), typeUtils);
+        }
+
+        return false;
+    }
+
+    /**
+     * Determines if a TypeMirror corresponds to an AppState-related type.
+     */
+    private boolean isAppStateType(TypeMirror typeMirror) {
+        if (typeMirror == null) {
+            return false;
+        }
+        String className = typeMirror.toString();
+        return "com.jme3.app.state.AppState".equals(className)
+                || "com.jme3.app.state.AbstractAppState".equals(className)
+                || "com.jme3.app.state.BaseAppState".equals(className);
+    }
+
     public void load(Project proj) {
     public void load(Project proj) {
         this.proj = proj;
         this.proj = proj;
         scanControls();
         scanControls();

+ 13 - 43
jme3-core/src/com/jme3/gde/core/assets/ExternalChangeScanner.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2003-2012 jMonkeyEngine
+ * Copyright (c) 2003-2024 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -36,7 +36,6 @@ import com.jme3.gde.core.scene.ApplicationLogHandler;
 import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.gde.core.sceneexplorer.SceneExplorerTopComponent;
 import com.jme3.gde.core.sceneexplorer.SceneExplorerTopComponent;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
 import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
-import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
 import com.jme3.gde.core.util.SpatialUtil;
 import com.jme3.gde.core.util.SpatialUtil;
 import com.jme3.gde.core.util.TaggedSpatialFinder;
 import com.jme3.gde.core.util.TaggedSpatialFinder;
 import com.jme3.gde.core.util.datatransfer.CopyAnimationDataFromOriginal;
 import com.jme3.gde.core.util.datatransfer.CopyAnimationDataFromOriginal;
@@ -186,9 +185,7 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
     
     
     private void applyExternalData(final boolean onlyMeshData, 
     private void applyExternalData(final boolean onlyMeshData, 
             final boolean onlyAnimData) {
             final boolean onlyAnimData) {
-        final ProgressHandle handle = ProgressHandle.createHandle("Updating "
-                + "file "
-                + "data");
+        final ProgressHandle handle = ProgressHandle.createHandle("Updating file data");
         handle.start();
         handle.start();
         try {
         try {
             final Spatial original = loadOriginalSpatial();
             final Spatial original = loadOriginalSpatial();
@@ -207,13 +204,11 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
                 new CopyTransformDataFromOriginal(finder).update(spat, original);
                 new CopyTransformDataFromOriginal(finder).update(spat, original);
                 new CopyMaterialDataFromOriginal(finder).update(spat, original);
                 new CopyMaterialDataFromOriginal(finder).update(spat, original);
             }
             }
-            // Do a complicated recurse refresh since AbstractSceneExplorerNode:refresh() isn't working
+            
             SwingUtilities.invokeLater(() -> {
             SwingUtilities.invokeLater(() -> {
                 Node rootNode = SceneExplorerTopComponent.findInstance().getExplorerManager().getRootContext();
                 Node rootNode = SceneExplorerTopComponent.findInstance().getExplorerManager().getRootContext();
-                if (rootNode instanceof JmeNode) {
-                    SceneApplication.getApplication().enqueue((Runnable) () -> {
-                    refreshNamedSpatial((JmeNode) rootNode, spat.getName());
-                    });
+                if (rootNode instanceof JmeNode jmeNode) {
+                    SceneApplication.getApplication().enqueue(new RefreshJmeSpatial(jmeNode, spat.getName()));
                 }
                 }
             });
             });
                 
                 
@@ -228,37 +223,6 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
         }
         }
     }
     }
     
     
-    /**
-     * Look for the spatial to update using the name of the asset
-     * @param spatial
-     * @param name 
-     */
-    private void refreshNamedSpatial(JmeSpatial spatial, String name){
-        if(spatial.getName().equals(name)){
-            recurseRefresh(spatial);
-        } else {
-            for(Node s: spatial.getChildren().getNodes()){
-                if(s instanceof JmeSpatial){
-                    refreshNamedSpatial((JmeSpatial) s, name);
-                }
-                
-            }
-        }
-    }
-    
-    /**
-     * Refreshes the spatial and all children
-     * @param spatial 
-     */
-    private void recurseRefresh(JmeSpatial spatial){
-        spatial.refresh(false);
-        for(Node s: spatial.getChildren().getNodes()){
-            if(s instanceof JmeSpatial){
-                recurseRefresh((JmeSpatial) s);
-            }
-        }
-    }
-
     private Spatial loadOriginalSpatial() {
     private Spatial loadOriginalSpatial() {
         try {
         try {
             final DataObject dobj = DataObject.find(originalObject);
             final DataObject dobj = DataObject.find(originalObject);
@@ -266,8 +230,8 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
                     dobj.getLookup().lookup(AssetData.class);
                     dobj.getLookup().lookup(AssetData.class);
             if (originalAssetData != null) {
             if (originalAssetData != null) {
                 final Savable sav = originalAssetData.loadAsset();
                 final Savable sav = originalAssetData.loadAsset();
-                if (sav instanceof Spatial) {
-                    return (Spatial) sav;
+                if (sav instanceof Spatial spatial) {
+                    return spatial;
                 } else {
                 } else {
                     LOGGER.log(Level.SEVERE, "Trying to load original for {0}"
                     LOGGER.log(Level.SEVERE, "Trying to load original for {0}"
                                     + " but it is not a Spatial: {1}",
                                     + " but it is not a Spatial: {1}",
@@ -352,18 +316,22 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
         }
         }
     }
     }
 
 
+    @Override
     public void fileFolderCreated(FileEvent fe) {
     public void fileFolderCreated(FileEvent fe) {
     }
     }
 
 
+    @Override
     public void fileDataCreated(FileEvent fe) {
     public void fileDataCreated(FileEvent fe) {
     }
     }
 
 
+    @Override
     public void fileChanged(FileEvent fe) {
     public void fileChanged(FileEvent fe) {
         LOGGER.log(Level.INFO, "External file {0} for {1} changed!",
         LOGGER.log(Level.INFO, "External file {0} for {1} changed!",
                 new Object[]{fe.getFile(), assetDataObject.getName()});
                 new Object[]{fe.getFile(), assetDataObject.getName()});
         notifyUser();
         notifyUser();
     }
     }
 
 
+    @Override
     public void fileDeleted(FileEvent fe) {
     public void fileDeleted(FileEvent fe) {
         LOGGER.log(Level.INFO, "External file {0} for {1} deleted!",
         LOGGER.log(Level.INFO, "External file {0} for {1} deleted!",
                 new Object[]{fe.getFile(), assetDataObject.getName()});
                 new Object[]{fe.getFile(), assetDataObject.getName()});
@@ -377,6 +345,7 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
         //TODO: add folder listener for when recreated
         //TODO: add folder listener for when recreated
     }
     }
 
 
+    @Override
     public void fileRenamed(FileRenameEvent fe) {
     public void fileRenamed(FileRenameEvent fe) {
         LOGGER.log(Level.INFO, "External file {0} for {1} renamed!",
         LOGGER.log(Level.INFO, "External file {0} for {1} renamed!",
                 new Object[]{fe.getFile(), assetDataObject.getName()});
                 new Object[]{fe.getFile(), assetDataObject.getName()});
@@ -388,6 +357,7 @@ public class ExternalChangeScanner implements AssetDataPropertyChangeListener,
         }
         }
     }
     }
 
 
+    @Override
     public void fileAttributeChanged(FileAttributeEvent fe) {
     public void fileAttributeChanged(FileAttributeEvent fe) {
     }
     }
 }
 }

+ 22 - 22
jme3-core/src/com/jme3/gde/core/assets/ProjectAssetManager.java

@@ -166,30 +166,30 @@ public class ProjectAssetManager extends DesktopAssetManager {
         }
         }
         SourceGroup[] groups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
         SourceGroup[] groups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
         List<URL> urls = new LinkedList<>();
         List<URL> urls = new LinkedList<>();
-        for (SourceGroup sourceGroup : groups) {
-            ClassPath path = ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.EXECUTE);
-            if (path == null) {
-                continue;
-            }
-
-            classPaths.add(path);
-            path.addPropertyChangeListener(classPathListener);
-            FileObject[] roots = path.getRoots();
-            for (FileObject fileObject : roots) {
-                if (!fileObject.equals(getAssetFolder())) {
-                    fileObject.addRecursiveListener(listener);
-                    logger.log(Level.FINE, "Add classpath:{0}", fileObject);
-                    classPathItems.add(new ClassPathItem(fileObject, listener));
-                    urls.add(fileObject.toURL());
+            for (SourceGroup sourceGroup : groups) {
+                ClassPath path = ClassPath.getClassPath(sourceGroup.getRootFolder(), ClassPath.EXECUTE);
+                if (path == null) {
+                    continue;
                 }
                 }
-                if (fileObject.toURL().toExternalForm().startsWith("jar")) {
-                    logger.log(Level.FINE, "Add locator:{0}", fileObject.toURL());
-                    jarItems.add(fileObject);
-                    registerLocator(fileObject.toURL().toExternalForm(),
-                            "com.jme3.asset.plugins.UrlLocator");
+
+                classPaths.add(path);
+                path.addPropertyChangeListener(classPathListener);
+                FileObject[] roots = path.getRoots();
+                for (FileObject fileObject : roots) {
+                    if (!fileObject.equals(getAssetFolder())) {
+                        fileObject.addRecursiveListener(listener);
+                        logger.log(Level.FINE, "Add classpath:{0}", fileObject);
+                        classPathItems.add(new ClassPathItem(fileObject, listener));
+                        urls.add(fileObject.toURL());
+                    }
+                    if (fileObject.toURL().toExternalForm().startsWith("jar")) {
+                        logger.log(Level.FINE, "Add locator:{0}", fileObject.toURL());
+                        jarItems.add(fileObject);
+                        registerLocator(fileObject.toURL().toExternalForm(),
+                                "com.jme3.asset.plugins.UrlLocator");
+                    }
                 }
                 }
             }
             }
-        }
 
 
         loadGradleClassLoader(urls);
         loadGradleClassLoader(urls);
 
 
@@ -223,7 +223,7 @@ public class ProjectAssetManager extends DesktopAssetManager {
         for (File file : runtimeFiles) {
         for (File file : runtimeFiles) {
             // logger.info(file.getName() + " : "  + file.getAbsolutePath());
             // logger.info(file.getName() + " : "  + file.getAbsolutePath());
             FileObject fo = FileUtil.toFileObject(file);
             FileObject fo = FileUtil.toFileObject(file);
-            if (fo != null && !fo.isFolder()) {
+            if (fo != null) {
                 logger.info(fo.toURL().toExternalForm());
                 logger.info(fo.toURL().toExternalForm());
                 if (!fo.equals(getAssetFolder())) {
                 if (!fo.equals(getAssetFolder())) {
                     fo.addRecursiveListener(listener);
                     fo.addRecursiveListener(listener);

+ 58 - 0
jme3-core/src/com/jme3/gde/core/assets/RefreshJmeSpatial.java

@@ -0,0 +1,58 @@
+
+package com.jme3.gde.core.assets;
+
+import com.jme3.gde.core.sceneexplorer.nodes.JmeNode;
+import org.openide.nodes.Node;
+
+import com.jme3.gde.core.sceneexplorer.nodes.JmeSpatial;
+
+/**
+ * Work around for refresh not working recursively on JmeSpatial
+ * @author rickard
+ */
+public class RefreshJmeSpatial implements Runnable {
+    
+    private final JmeNode rootNode;
+    private final String spatialName;
+    
+    public RefreshJmeSpatial(JmeNode rootNode, String spatialName) {
+        this.rootNode = rootNode;
+        this.spatialName = spatialName;
+    }
+    
+    @Override
+    public void run() {
+        refreshNamedSpatial(rootNode, spatialName);
+    }
+    /**
+     * Look for the spatial to update using the name of the asset
+     * @param spatial
+     * @param name 
+     */
+    private void refreshNamedSpatial(JmeSpatial spatial, String name){
+        if(spatial.getName().equals(name)){
+            recurseRefresh(spatial);
+        } else {
+            for(Node s: spatial.getChildren().getNodes()){
+                if(s instanceof JmeSpatial jmeSpatial){
+                    refreshNamedSpatial(jmeSpatial, name);
+                }
+                
+            }
+        }
+    }
+    
+    /**
+     * Refreshes the spatial and all children
+     * @param spatial 
+     */
+    private void recurseRefresh(JmeSpatial spatial){
+        spatial.refresh(false);
+        for(Node s: spatial.getChildren().getNodes()){
+            if(s instanceof JmeSpatial jmeSpatial){
+                recurseRefresh(jmeSpatial);
+            }
+        }
+    }
+
+}

+ 233 - 0
jme3-core/src/com/jme3/gde/core/assets/actions/MergeAnimationsAction.java

@@ -0,0 +1,233 @@
+/*
+ *  Copyright (c) 2009-2024 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.core.assets.actions;
+
+import com.jme3.anim.AnimClip;
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.AnimTrack;
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.anim.SkinningControl;
+import com.jme3.anim.TransformTrack;
+import com.jme3.anim.util.HasLocalTransform;
+import com.jme3.gde.core.assets.SpatialAssetDataObject;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.util.SafeArrayList;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.NotifyDescriptor.Confirmation;
+import org.openide.util.Exceptions;
+
+/**
+ * Action for merging one or more spatials' animation to another. Same rig
+ * required.
+ *
+ * @author rickard
+ */
+public class MergeAnimationsAction implements ActionListener {
+
+    private static final String CANCEL = "Cancel";
+    private final List<SpatialAssetDataObject> spatials;
+    private final Logger logger;
+
+    public MergeAnimationsAction(List<SpatialAssetDataObject> context) {
+        this.spatials = context;
+        logger = Logger.getLogger(MergeAnimationsAction.class.getName());
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        if (spatials.size() == 1) {
+            DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(
+                    "Must select more than one spatial",
+                    NotifyDescriptor.ERROR_MESSAGE));
+            return;
+        }
+
+        final Object selectedSpatial = createSelector().getValue();
+
+        if (selectedSpatial == null || selectedSpatial.equals(CANCEL)) {
+            logger.log(Level.INFO, "Operation cancelled by user.");
+            return;
+        }
+
+        final SpatialAssetDataObject targetAsset = findSpatial(selectedSpatial.toString());
+
+        if (targetAsset == null) {
+            logger.log(Level.INFO, "Operation failed. No spatial.");
+            return;
+        }
+
+        final Spatial targetSpatial = targetAsset.loadAsset();
+        final AnimComposer targetAnimComposer = findAnimComposer(targetSpatial, null);
+
+        if (targetAnimComposer == null) {
+            DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(
+                    String.format("%s has no AnimComposer.", targetSpatial),
+                    NotifyDescriptor.ERROR_MESSAGE));
+            return;
+        }
+        final Spatial targetAnimComposerSpatial = targetAnimComposer.getSpatial();
+        for (final Iterator<SpatialAssetDataObject> it = spatials.iterator(); it.hasNext();) {
+            final SpatialAssetDataObject spatialAssetDataObject = it.next();
+            if (spatialAssetDataObject.getName().equals(selectedSpatial)) {
+                continue;
+            }
+            Spatial sourceSpatial = spatialAssetDataObject.loadAsset();
+            final AnimComposer sourceAnimComposer = findAnimComposer(sourceSpatial, null);
+            if (sourceAnimComposer == null) {
+                DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(
+                        String.format("%s has no AnimComposer.", sourceSpatial),
+                        NotifyDescriptor.ERROR_MESSAGE));
+                return;
+            }
+            copyClips(sourceAnimComposer, targetAnimComposer, targetAnimComposerSpatial.getControl(SkinningControl.class).getArmature());
+        }
+        logger.log(Level.INFO, "Merging animations done. Saving.");
+        try {
+            targetAsset.saveAsset();
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    private void copyClips(final AnimComposer from, final AnimComposer to, Armature toArmature) {
+        final Collection<AnimClip> animClips = from.getAnimClips();
+        for (AnimClip animClip : animClips) {
+            to.addAnimClip(retargetClip(animClip, toArmature, animClip.getName()));
+            logger.log(Level.FINE, String.format("Added anim clip %s", animClip.getName()));
+        }
+    }
+
+    private Confirmation createSelector() {
+        final Object[] spatials = new Object[this.spatials.size() + 1];
+        int index = 0;
+        for (Iterator<SpatialAssetDataObject> it = this.spatials.iterator(); it.hasNext();) {
+            final SpatialAssetDataObject spatialAssetDataObject = it.next();
+            spatials[index++] = spatialAssetDataObject.getName();
+        }
+        spatials[index++] = CANCEL;
+        final NotifyDescriptor.Confirmation message
+                = new NotifyDescriptor.Confirmation(
+                        "Select spatial to copy animations to.");
+        message.setOptions(spatials);
+
+        DialogDisplayer.getDefault().notify(message);
+
+        return message;
+    }
+
+    private AnimComposer findAnimComposer(Spatial spatial, AnimComposer animComposer) {
+        if (spatial.getControl(AnimComposer.class) != null) {
+            return spatial.getControl(AnimComposer.class);
+        }
+        if (animComposer == null && spatial instanceof Node node) {
+            for (Spatial child : node.getChildren()) {
+                animComposer = findAnimComposer(child, animComposer);
+                if (animComposer != null) {
+                    return animComposer;
+                }
+            }
+        }
+        return animComposer;
+    }
+
+    private SpatialAssetDataObject findSpatial(String selected) {
+        for (Iterator<SpatialAssetDataObject> it = spatials.iterator(); it.hasNext();) {
+            final SpatialAssetDataObject spatialAssetDataObject = it.next();
+            if (spatialAssetDataObject.getName().equals(selected)) {
+                return spatialAssetDataObject;
+            }
+        }
+        NotifyDescriptor.Message msg = new NotifyDescriptor.Message(
+                "Main asset to copy to not found. This is likely an issue with the tool itself.",
+                NotifyDescriptor.ERROR_MESSAGE);
+        DialogDisplayer.getDefault().notify(msg);
+        return null;
+    }
+
+    private AnimClip retargetClip(AnimClip sourceClip, Armature targetArmature, String clipName) {
+
+        // Create a list to hold the new tracks
+        SafeArrayList<AnimTrack> tracks = new SafeArrayList<>(AnimTrack.class);
+
+        // Iterate through each track in the source clip
+        for (AnimTrack animTrack : sourceClip.getTracks()) {
+
+            TransformTrack sourceTrack = (TransformTrack) animTrack;
+            String targetName = getTargetName(sourceTrack.getTarget());
+            if (targetName == null) {
+                logger.log(Level.SEVERE, String.format("Unsupported target for: %s. Skipping.", animTrack));
+                continue;
+            }
+            Joint target = targetArmature.getJoint(targetName);
+
+            if (target != null) {
+                // Clone the source track and set the new target joint
+                TransformTrack newTrack = sourceTrack.jmeClone();
+                newTrack.setTarget(target);
+                tracks.add(newTrack);
+
+            } else {
+                logger.log(Level.WARNING, "Joint not found in the target Armature: {0}", targetName);
+            }
+        }
+
+        // Create a new animation clip with the specified name and set its tracks
+        AnimClip newClip = new AnimClip(clipName);
+        newClip.setTracks(tracks.getArray());
+
+        logger.log(Level.INFO, "Created new AnimClip {0} with {1} tracks out of {2} from the source clip",
+                new Object[]{clipName, tracks.size(), sourceClip.getTracks().length});
+        return newClip;
+    }
+
+    private String getTargetName(final HasLocalTransform target) {
+        if (target instanceof Node node) {
+            return node.getName();
+        }
+        if (target instanceof Joint joint) {
+            return joint.getName();
+        }
+        return null;
+    }
+
+}

+ 1 - 3
jme3-core/src/com/jme3/gde/core/sceneexplorer/SceneExplorerTopComponent.java

@@ -79,7 +79,7 @@ public final class SceneExplorerTopComponent extends TopComponent implements Exp
     private static final Logger logger = Logger.getLogger(SceneExplorerTopComponent.class.getName());
     private static final Logger logger = Logger.getLogger(SceneExplorerTopComponent.class.getName());
     private static SceneExplorerTopComponent instance;
     private static SceneExplorerTopComponent instance;
     private static final String PREFERRED_ID = "SceneExplorerTopComponent";
     private static final String PREFERRED_ID = "SceneExplorerTopComponent";
-    
+
     private AbstractSceneExplorerNode[] selectedSpatials;
     private AbstractSceneExplorerNode[] selectedSpatials;
     private AbstractSceneExplorerNode[] lastSelected;
     private AbstractSceneExplorerNode[] lastSelected;
     private final Map<String, MaterialChangeProvider> materialChangeProviders = new HashMap<>();
     private final Map<String, MaterialChangeProvider> materialChangeProviders = new HashMap<>();
@@ -156,8 +156,6 @@ public final class SceneExplorerTopComponent extends TopComponent implements Exp
         for (AbstractSceneExplorerNode node: selectedSpatials) {
         for (AbstractSceneExplorerNode node: selectedSpatials) {
             node.refresh(false);
             node.refresh(false);
         }
         }
-        
-           
     }//GEN-LAST:event_jButton1ActionPerformed
     }//GEN-LAST:event_jButton1ActionPerformed
     // Variables declaration - do not modify//GEN-BEGIN:variables
     // Variables declaration - do not modify//GEN-BEGIN:variables
     private javax.swing.JScrollPane explorerScrollPane;
     private javax.swing.JScrollPane explorerScrollPane;

+ 83 - 61
jme3-core/src/com/jme3/gde/core/sceneexplorer/nodes/actions/impl/NewCustomControlVisualPanel1.java

@@ -31,9 +31,11 @@
  */
  */
 package com.jme3.gde.core.sceneexplorer.nodes.actions.impl;
 package com.jme3.gde.core.sceneexplorer.nodes.actions.impl;
 
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.List;
 import java.util.Set;
 import java.util.Set;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.TypeElement;
@@ -53,8 +55,10 @@ import org.netbeans.api.java.source.JavaSource;
 import org.netbeans.api.java.source.JavaSource.Phase;
 import org.netbeans.api.java.source.JavaSource.Phase;
 import org.netbeans.api.java.source.Task;
 import org.netbeans.api.java.source.Task;
 import org.netbeans.api.project.Project;
 import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectUtils;
 import org.netbeans.api.project.SourceGroup;
 import org.netbeans.api.project.SourceGroup;
 import org.netbeans.api.project.Sources;
 import org.netbeans.api.project.Sources;
+import org.openide.filesystems.FileObject;
 import org.openide.util.Exceptions;
 import org.openide.util.Exceptions;
 
 
 @SuppressWarnings({"unchecked", "rawtypes"})
 @SuppressWarnings({"unchecked", "rawtypes"})
@@ -84,74 +88,92 @@ public final class NewCustomControlVisualPanel1 extends JPanel {
     }
     }
 
 
     private List<String> getSources() {
     private List<String> getSources() {
-        Sources sources = proj.getLookup().lookup(Sources.class);
-        final List<String> list = new LinkedList<String>();
-        if (sources != null) {
+        Project root = ProjectUtils.rootOf(proj);
+        Set<Project> containedProjects = ProjectUtils.getContainedProjects(root, true);
+        List<Project> projects = new ArrayList<>();
+        projects.add(root);
+        if (containedProjects != null) {
+            projects.addAll(containedProjects);
+        }
+        if (projects.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Set<String> list = new HashSet<>();
+        for (Project project : projects) {
+            Sources sources = project.getLookup().lookup(Sources.class);
+            if (sources == null) {
+                continue;
+            }
+
             SourceGroup[] groups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
             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));
-
-                    Set<SearchScope> set = EnumSet.of(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();
-                        JavaSource js = JavaSource.create(cpInfo);
-                        try {
-                            js.runUserActionTask(new Task<CompilationController>() {
-                                @Override
-                                public void run(CompilationController control)
-                                        throws Exception {
-                                    control.toPhase(Phase.RESOLVED);
-                                    //TODO: check with proper casting check.. gotta get TypeMirror of Control interface..
-//                                    TypeUtilities util = control.getTypeUtilities();//.isCastable(Types., null)
-//                                    util.isCastable(null, null);
-                                    TypeElement elem = elementHandle.resolve(control);
-                                    if (elem == null)
-                                        return;
-                                    
-                                    String elementName = elem.getQualifiedName().toString();
-                                    
-                                    if (list.contains(elementName)) /* No duplicates */
-                                        return;
-                                    
-                                    do {
-                                        //Check if it implements control interface
-                                        for (TypeMirror typeMirror : elem.getInterfaces()) {
-                                            String interfaceName = typeMirror.toString();
-                                            if ("com.jme3.scene.control.Control".equals(interfaceName)) {
-                                                if (!list.contains(elementName))
-                                                    list.add(elementName);
-                                                break;
-                                            }
-                                        }
-                                        //Check if it is an AbstractControl
-                                        String className = elem.toString();
-                                        if ("com.jme3.scene.control.AbstractControl".equals(className)) {
-                                            if (!list.contains(elementName))
-                                                list.add(elementName);
-                                        }
+            if (groups == null) {
+                continue;
+            }
 
 
-                                        TypeMirror superClass = elem.getSuperclass();
-                                        if (superClass == null || superClass.getKind() == TypeKind.NONE) {
-                                            break;
-                                        }
+            for (SourceGroup sourceGroup : groups) {
+                FileObject rootFolder = sourceGroup.getRootFolder();
+                final ClasspathInfo cpInfo = ClasspathInfo.create(ClassPath.getClassPath(rootFolder, ClassPath.BOOT),
+                        ClassPath.getClassPath(rootFolder, ClassPath.COMPILE),
+                        ClassPath.getClassPath(rootFolder, ClassPath.SOURCE));
+
+                Set<SearchScope> set = EnumSet.of(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();
+                    JavaSource js = JavaSource.create(cpInfo);
+                    try {
+                        js.runUserActionTask(new Task<CompilationController>() {
+                            @Override
+                            public void run(CompilationController control)
+                                    throws Exception {
+                                control.toPhase(Phase.RESOLVED);
+                                //TODO: check with proper casting check.. gotta get TypeMirror of Control interface..
+                                //                                    TypeUtilities util = control.getTypeUtilities();//.isCastable(Types., null)
+                                //                                    util.isCastable(null, null);
+                                TypeElement elem = elementHandle.resolve(control);
+                                if (elem == null) {
+                                    return;
+                                }
 
 
-                                        elem = (TypeElement)((DeclaredType)superClass).asElement(); // Iterate deeper
-                                    } while (elem != null);
+                                String elementName = elem.getQualifiedName().toString();
+
+                                if (list.contains(elementName)) /* No duplicates */ {
+                                    return;
                                 }
                                 }
-                            }, false);
-                        } catch (Exception ioe) {
-                            Exceptions.printStackTrace(ioe);
-                        }
-                    }
 
 
+                                do {
+                                    //Check if it implements control interface
+                                    for (TypeMirror typeMirror : elem.getInterfaces()) {
+                                        String interfaceName = typeMirror.toString();
+                                        if ("com.jme3.scene.control.Control".equals(interfaceName) && !list.contains(elementName)) {
+                                            list.add(elementName);
+                                            break;
+                                        }
+                                    }
+                                    //Check if it is an AbstractControl
+                                    String className = elem.toString();
+                                    if ("com.jme3.scene.control.AbstractControl".equals(className) && !list.contains(elementName)) {
+                                        list.add(elementName);
+                                    }
+
+                                    TypeMirror superClass = elem.getSuperclass();
+                                    if (superClass == null || superClass.getKind() == TypeKind.NONE) {
+                                        break;
+                                    }
+
+                                    elem = (TypeElement) ((DeclaredType) superClass).asElement(); // Iterate deeper
+                                } while (elem != null);
+                            }
+                        }, false);
+                    } catch (Exception ioe) {
+                        Exceptions.printStackTrace(ioe);
+                    }
                 }
                 }
             }
             }
         }
         }
-        return list;
+
+        return new ArrayList<>(list);
     }
     }
 
 
     public void load(Project proj) {
     public void load(Project proj) {

+ 9 - 0
jme3-glsl-highlighter/nbproject/project.xml

@@ -50,6 +50,15 @@
                         <specification-version>1.39.1.55</specification-version>
                         <specification-version>1.39.1.55</specification-version>
                     </run-dependency>
                     </run-dependency>
                 </dependency>
                 </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.editor.completion</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.71.0.2</specification-version>
+                    </run-dependency>
+                </dependency>
                 <dependency>
                 <dependency>
                     <code-name-base>org.netbeans.modules.editor.indent</code-name-base>
                     <code-name-base>org.netbeans.modules.editor.indent</code-name-base>
                     <build-prerequisite/>
                     <build-prerequisite/>

+ 150 - 0
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/GlslCompletionProvider.java

@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2003-2024 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.glsl.highlighter.editor;
+
+import com.jme3.gde.glsl.highlighter.editor.completion.FunctionCompletionItem;
+import com.jme3.gde.glsl.highlighter.editor.completion.KeywordCompletionItem;
+import com.jme3.gde.glsl.highlighter.editor.completion.TypeCompletionItem;
+import com.jme3.gde.glsl.highlighter.editor.completion.VariableCompletionItem;
+import com.jme3.gde.glsl.highlighter.lexer.GlslKeywordLibrary;
+import java.util.List;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.Element;
+import javax.swing.text.JTextComponent;
+import javax.swing.text.StyledDocument;
+import org.netbeans.api.editor.mimelookup.MimeRegistration;
+import org.netbeans.spi.editor.completion.CompletionItem;
+import org.netbeans.spi.editor.completion.CompletionProvider;
+import org.netbeans.spi.editor.completion.CompletionResultSet;
+import org.netbeans.spi.editor.completion.CompletionTask;
+import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery;
+import org.netbeans.spi.editor.completion.support.AsyncCompletionTask;
+import org.openide.util.Exceptions;
+
+@MimeRegistration(mimeType = "text/x-glsl", service = CompletionProvider.class)
+public class GlslCompletionProvider implements CompletionProvider {
+
+    @Override
+    public CompletionTask createTask(int queryType, JTextComponent component) {
+        if (queryType != CompletionProvider.COMPLETION_QUERY_TYPE) {
+            return null;
+        }
+
+        return new AsyncCompletionTask(new AsyncCompletionQuery() {
+            @Override
+            protected void query(CompletionResultSet completionResultSet,
+                    Document document, int caretOffset) {
+
+                String filter = "";
+                int startOffset = caretOffset - 1;
+
+                try {
+                    final StyledDocument bDoc = (StyledDocument) document;
+                    final int lineStartOffset = getRowFirstNonWhite(bDoc, caretOffset);
+                    final char[] line = bDoc.getText(lineStartOffset, caretOffset - lineStartOffset).toCharArray();
+                    final int whiteOffset = indexOfWhite(line);
+                    filter = new String(line, whiteOffset + 1, line.length - whiteOffset - 1);
+                    if (whiteOffset > 0) {
+                        startOffset = lineStartOffset + whiteOffset + 1;
+                    } else {
+                        startOffset = lineStartOffset;
+                    }
+                } catch (BadLocationException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+
+                setCompletionItems(filter, completionResultSet, startOffset, caretOffset);
+
+                completionResultSet.finish();
+            }
+
+            private void setCompletionItems(String filter, CompletionResultSet completionResultSet, int startOffset, int caretOffset) {
+                List<GlslKeywordLibrary.Keyword> keywords = GlslKeywordLibrary.lookupAll(filter);
+                completionResultSet.addAllItems(keywords.stream().map((keyword) -> createCompletionItem(keyword, startOffset, caretOffset)).toList());
+            }
+
+            private CompletionItem createCompletionItem(GlslKeywordLibrary.Keyword keyword, int dotOffset, int caretOffset) {
+                return switch (keyword.keywordType()) {
+                    case KEYWORD ->
+                        new KeywordCompletionItem(keyword.keyword(), dotOffset, caretOffset);
+                    case BUILTIN_FUNCTION ->
+                        new FunctionCompletionItem(keyword.keyword(), dotOffset, caretOffset);
+                    case BUILTIN_VARIABLE ->
+                        new VariableCompletionItem(keyword.keyword(), dotOffset, caretOffset);
+                    case BASIC_TYPE ->
+                        new TypeCompletionItem(keyword.keyword(), dotOffset, caretOffset);
+                    case UNFINISHED ->
+                        throw new AssertionError("Keyword type invalid");
+                    default ->
+                        throw new AssertionError("Keyword type not implemented");
+                };
+            }
+        }, component);
+    }
+
+    private static int getRowFirstNonWhite(StyledDocument doc, int offset)
+            throws BadLocationException {
+        Element lineElement = doc.getParagraphElement(offset);
+        int start = lineElement.getStartOffset();
+        while (start + 1 < lineElement.getEndOffset()) {
+            try {
+                if (doc.getText(start, 1).charAt(0) != ' ') {
+                    break;
+                }
+            } catch (BadLocationException ex) {
+                throw (BadLocationException) new BadLocationException(
+                        "calling getText(" + start + ", " + (start + 1)
+                        + ") on doc of length: " + doc.getLength(), start
+                ).initCause(ex);
+            }
+            start++;
+        }
+        return start;
+    }
+
+    private static int indexOfWhite(char[] line) {
+        int i = line.length;
+        while (--i > -1) {
+            final char c = line[i];
+            if (Character.isWhitespace(c)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public int getAutoQueryTypes(JTextComponent jtc, String string) {
+        return 0;
+    }
+}

+ 123 - 0
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/DefaultCompletionItem.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2003-2024 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.glsl.highlighter.editor.completion;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.event.KeyEvent;
+import javax.swing.ImageIcon;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.JTextComponent;
+import javax.swing.text.StyledDocument;
+import org.netbeans.api.editor.completion.Completion;
+import org.netbeans.spi.editor.completion.CompletionItem;
+import org.netbeans.spi.editor.completion.CompletionTask;
+import org.netbeans.spi.editor.completion.support.CompletionUtilities;
+import org.openide.util.Exceptions;
+
+public abstract class DefaultCompletionItem implements CompletionItem {
+
+    private final String keyword;
+    private final int caretOffset;
+    private final int dotOffset;
+
+    public DefaultCompletionItem(String keyword, int dotOffset, int caretOffset) {
+        this.keyword = keyword;
+        this.dotOffset = dotOffset;
+        this.caretOffset = caretOffset;
+    }
+
+    @Override
+    public void defaultAction(JTextComponent component) {
+        try {
+            StyledDocument doc = (StyledDocument) component.getDocument();
+            //Here we remove the characters starting at the start offset
+            //and ending at the point where the caret is currently found:
+            doc.remove(dotOffset, caretOffset - dotOffset);
+            doc.insertString(dotOffset, keyword, null);
+            Completion.get().hideAll();
+        } catch (BadLocationException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public void processKeyEvent(KeyEvent ke) {
+
+    }
+
+    @Override
+    public int getPreferredWidth(Graphics graphics, Font font) {
+        return CompletionUtilities.getPreferredWidth(keyword, null, graphics, font);
+    }
+
+    @Override
+    public void render(Graphics graphics, Font font, Color defaultColor,
+            Color backgroundColor, int width, int height, boolean selected) {
+        CompletionUtilities.renderHtml(getIcon(), keyword, null, graphics, font,
+                (selected ? Color.white : null), width, height, selected);
+    }
+
+    @Override
+    public CompletionTask createDocumentationTask() {
+        return null;
+    }
+
+    @Override
+    public CompletionTask createToolTipTask() {
+        return null;
+    }
+
+    @Override
+    public boolean instantSubstitution(JTextComponent jtc) {
+        return false;
+    }
+
+    @Override
+    public int getSortPriority() {
+        return 0;
+    }
+
+    @Override
+    public CharSequence getSortText() {
+        return keyword;
+    }
+
+    @Override
+    public CharSequence getInsertPrefix() {
+        return keyword;
+    }
+
+    protected abstract ImageIcon getIcon();
+
+}

+ 50 - 0
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/FunctionCompletionItem.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2003-2024 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.glsl.highlighter.editor.completion;
+
+import javax.swing.ImageIcon;
+import org.openide.util.ImageUtilities;
+
+public class FunctionCompletionItem extends DefaultCompletionItem {
+
+    private static final ImageIcon fieldIcon
+            = new ImageIcon(ImageUtilities.loadImage("com/jme3/gde/glsl/highlighter/editor/completion/method_16.png"));
+
+    public FunctionCompletionItem(String keyword, int dotOffset, int caretOffset) {
+        super(keyword, dotOffset, caretOffset);
+    }
+
+    @Override
+    protected ImageIcon getIcon() {
+        return fieldIcon;
+    }
+}

+ 50 - 0
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/KeywordCompletionItem.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2003-2024 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.glsl.highlighter.editor.completion;
+
+import javax.swing.ImageIcon;
+import org.openide.util.ImageUtilities;
+
+public class KeywordCompletionItem extends DefaultCompletionItem {
+
+    private static final ImageIcon fieldIcon
+            = new ImageIcon(ImageUtilities.loadImage("com/jme3/gde/glsl/highlighter/editor/completion/method_static_protected_16.png"));
+
+    public KeywordCompletionItem(String keyword, int dotOffset, int caretOffset) {
+        super(keyword, dotOffset, caretOffset);
+    }
+
+    @Override
+    protected ImageIcon getIcon() {
+        return fieldIcon;
+    }
+}

+ 50 - 0
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/TypeCompletionItem.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2003-2024 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.glsl.highlighter.editor.completion;
+
+import javax.swing.ImageIcon;
+import org.openide.util.ImageUtilities;
+
+public class TypeCompletionItem extends DefaultCompletionItem {
+
+    private static final ImageIcon fieldIcon
+            = new ImageIcon(ImageUtilities.loadImage("com/jme3/gde/glsl/highlighter/editor/completion/class_16.png"));
+
+    public TypeCompletionItem(String keyword, int dotOffset, int caretOffset) {
+        super(keyword, dotOffset, caretOffset);
+    }
+
+    @Override
+    protected ImageIcon getIcon() {
+        return fieldIcon;
+    }
+}

+ 50 - 0
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/VariableCompletionItem.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2003-2024 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.glsl.highlighter.editor.completion;
+
+import javax.swing.ImageIcon;
+import org.openide.util.ImageUtilities;
+
+public class VariableCompletionItem extends DefaultCompletionItem {
+
+    private static final ImageIcon fieldIcon
+            = new ImageIcon(ImageUtilities.loadImage("com/jme3/gde/glsl/highlighter/editor/completion/field_16.png"));
+
+    public VariableCompletionItem(String keyword, int dotOffset, int caretOffset) {
+        super(keyword, dotOffset, caretOffset);
+    }
+
+    @Override
+    protected ImageIcon getIcon() {
+        return fieldIcon;
+    }
+}

BIN
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/class_16.png


BIN
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/field_16.png


BIN
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/method_16.png


BIN
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/editor/completion/method_static_protected_16.png


+ 459 - 447
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/lexer/GlslKeywordLibrary.java

@@ -31,6 +31,7 @@
  */
  */
 package com.jme3.gde.glsl.highlighter.lexer;
 package com.jme3.gde.glsl.highlighter.lexer;
 
 
+import com.jme3.gde.glsl.highlighter.util.Trie;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
@@ -40,477 +41,488 @@ import java.util.List;
  *
  *
  * @author grizeldi
  * @author grizeldi
  */
  */
-class GlslKeywordLibrary {
-
+public final class GlslKeywordLibrary {
+    
     public enum KeywordType {
     public enum KeywordType {
         KEYWORD, BUILTIN_FUNCTION, BUILTIN_VARIABLE, BASIC_TYPE, UNFINISHED;
         KEYWORD, BUILTIN_FUNCTION, BUILTIN_VARIABLE, BASIC_TYPE, UNFINISHED;
     }
     }
-    private static final List<String> keywords = new ArrayList<>(),
-            builtinFunctions = new ArrayList<>(),
-            builtinVariables = new ArrayList<>(),
-            basicTypes = new ArrayList<>();
+
+    public record Keyword(String keyword, KeywordType keywordType) {
+
+    }
+    
+    private static final Trie keywords = new Trie();
+    private static final Trie builtinFunctions = new Trie();
+    private static final Trie builtinVariables = new Trie();
+    private static final Trie basicTypes = new Trie();
 
 
     static {
     static {
         //keywords
         //keywords
-        keywords.add("attribute");
-        keywords.add("const");
-        keywords.add("uniform");
-        keywords.add("varying");
-        keywords.add("buffer");
-        keywords.add("shared");
-        keywords.add("coherent");
-        keywords.add("volatile");
-        keywords.add("restrict");
-        keywords.add("readonly");
-        keywords.add("writeonly");
-        keywords.add("atomic_uint");
-        keywords.add("layout");
-        keywords.add("centroid");
-        keywords.add("flat");
-        keywords.add("smooth");
-        keywords.add("noperspective");
-        keywords.add("patch");
-        keywords.add("sample");
-        keywords.add("break");
-        keywords.add("continue");
-        keywords.add("do");
-        keywords.add("for");
-        keywords.add("while");
-        keywords.add("switch");
-        keywords.add("case");
-        keywords.add("default");
-        keywords.add("if");
-        keywords.add("else");
-        keywords.add("subroutine");
-        keywords.add("in");
-        keywords.add("out");
-        keywords.add("inout");
-        keywords.add("void");
-        keywords.add("true");
-        keywords.add("false");
-        keywords.add("invariant");
-        keywords.add("precise");
-        keywords.add("discard");
-        keywords.add("return");
+        keywords.insert("attribute");
+        keywords.insert("const");
+        keywords.insert("uniform");
+        keywords.insert("varying");
+        keywords.insert("buffer");
+        keywords.insert("shared");
+        keywords.insert("coherent");
+        keywords.insert("volatile");
+        keywords.insert("restrict");
+        keywords.insert("readonly");
+        keywords.insert("writeonly");
+        keywords.insert("atomic_uint");
+        keywords.insert("layout");
+        keywords.insert("centroid");
+        keywords.insert("flat");
+        keywords.insert("smooth");
+        keywords.insert("noperspective");
+        keywords.insert("patch");
+        keywords.insert("sample");
+        keywords.insert("break");
+        keywords.insert("continue");
+        keywords.insert("do");
+        keywords.insert("for");
+        keywords.insert("while");
+        keywords.insert("switch");
+        keywords.insert("case");
+        keywords.insert("default");
+        keywords.insert("if");
+        keywords.insert("else");
+        keywords.insert("subroutine");
+        keywords.insert("in");
+        keywords.insert("out");
+        keywords.insert("inout");
+        keywords.insert("void");
+        keywords.insert("true");
+        keywords.insert("false");
+        keywords.insert("invariant");
+        keywords.insert("precise");
+        keywords.insert("discard");
+        keywords.insert("return");
         //primitives and other types
         //primitives and other types
-        basicTypes.add("float");
-        basicTypes.add("double");
-        basicTypes.add("int");
-        basicTypes.add("bool");
-        basicTypes.add("mat2");
-        basicTypes.add("mat3");
-        basicTypes.add("mat4");
-        basicTypes.add("dmat2");
-        basicTypes.add("dmat3");
-        basicTypes.add("dmat4");
-        basicTypes.add("mat2x2");
-        basicTypes.add("mat2x3");
-        basicTypes.add("mat2x4");
-        basicTypes.add("dmat2x2");
-        basicTypes.add("dmat2x3");
-        basicTypes.add("dmat2x4");
-        basicTypes.add("mat3x2");
-        basicTypes.add("mat3x3");
-        basicTypes.add("mat3x4");
-        basicTypes.add("dmat3x2");
-        basicTypes.add("dmat3x3");
-        basicTypes.add("dmat3x4");
-        basicTypes.add("mat4x2");
-        basicTypes.add("mat4x3");
-        basicTypes.add("mat4x4");
-        basicTypes.add("dmat4x2");
-        basicTypes.add("dmat4x3");
-        basicTypes.add("dmat4x4");
-        basicTypes.add("vec2");
-        basicTypes.add("vec3");
-        basicTypes.add("vec4");
-        basicTypes.add("ivec2");
-        basicTypes.add("ivec3");
-        basicTypes.add("ivec4");
-        basicTypes.add("bvec2");
-        basicTypes.add("bvec3");
-        basicTypes.add("bvec4");
-        basicTypes.add("dvec2");
-        basicTypes.add("dvec3");
-        basicTypes.add("dvec4");
-        basicTypes.add("uint");
-        basicTypes.add("uvec2");
-        basicTypes.add("uvec3");
-        basicTypes.add("uvec4");
-        basicTypes.add("lowp");
-        basicTypes.add("mediump");
-        basicTypes.add("highp");
-        basicTypes.add("precision");
-        basicTypes.add("sampler1D");
-        basicTypes.add("sampler2D");
-        basicTypes.add("sampler3D");
-        basicTypes.add("samplerCube");
-        basicTypes.add("sampler1DShadow");
-        basicTypes.add("sampler2DShadow");
-        basicTypes.add("samplerCubeShadow");
-        basicTypes.add("sampler1DArray");
-        basicTypes.add("sampler2DArray");
-        basicTypes.add("sampler1DArrayShadow");
-        basicTypes.add("sampler2DArrayShadow");
-        basicTypes.add("isampler1D");
-        basicTypes.add("isampler2D");
-        basicTypes.add("isampler3D");
-        basicTypes.add("isamplerCube");
-        basicTypes.add("isampler1DArray");
-        basicTypes.add("isampler2DArray");
-        basicTypes.add("usampler1D");
-        basicTypes.add("usampler2D");
-        basicTypes.add("usampler3D");
-        basicTypes.add("usamplerCube");
-        basicTypes.add("usampler1DArray");
-        basicTypes.add("usampler2DArray");
-        basicTypes.add("sampler2DRect");
-        basicTypes.add("sampler2DRectShadow");
-        basicTypes.add("isampler2DRect");
-        basicTypes.add("usampler2DRect");
-        basicTypes.add("samplerBuffer");
-        basicTypes.add("isamplerBuffer");
-        basicTypes.add("usamplerBuffer");
-        basicTypes.add("sampler2DMS");
-        basicTypes.add("isampler2DMS");
-        basicTypes.add("usampler2DMS");
-        basicTypes.add("sampler2DMSArray");
-        basicTypes.add("isampler2DMSArray");
-        basicTypes.add("usampler2DMSArray");
-        basicTypes.add("samplerCubeArray");
-        basicTypes.add("samplerCubeArrayShadow");
-        basicTypes.add("isamplerCubeArray");
-        basicTypes.add("usamplerCubeArray");
-        basicTypes.add("image1D");
-        basicTypes.add("iimage1D");
-        basicTypes.add("uimage1D");
-        basicTypes.add("image2D");
-        basicTypes.add("iimage2D");
-        basicTypes.add("uimage2D");
-        basicTypes.add("image3D");
-        basicTypes.add("iimage3D");
-        basicTypes.add("uimage3D");
-        basicTypes.add("image2DRect");
-        basicTypes.add("iimage2DRect");
-        basicTypes.add("uimage2DRect");
-        basicTypes.add("imageCube");
-        basicTypes.add("iimageCube");
-        basicTypes.add("uimageCube");
-        basicTypes.add("imageBuffer");
-        basicTypes.add("iimageBuffer");
-        basicTypes.add("uimageBuffer");
-        basicTypes.add("image1DArray");
-        basicTypes.add("iimage1DArray");
-        basicTypes.add("uimage1DArray");
-        basicTypes.add("image2DArray");
-        basicTypes.add("iimage2DArray");
-        basicTypes.add("uimage2DArray");
-        basicTypes.add("imageCubeArray");
-        basicTypes.add("iimageCubeArray");
-        basicTypes.add("uimageCubeArray");
-        basicTypes.add("image2DMS");
-        basicTypes.add("iimage2DMS");
-        basicTypes.add("uimage2DMS");
-        basicTypes.add("image2DMSArray");
-        basicTypes.add("iimage2DMSArray");
-        basicTypes.add("uimage2DMSArray");
-        basicTypes.add("struct");
+        basicTypes.insert("float");
+        basicTypes.insert("double");
+        basicTypes.insert("int");
+        basicTypes.insert("bool");
+        basicTypes.insert("mat2");
+        basicTypes.insert("mat3");
+        basicTypes.insert("mat4");
+        basicTypes.insert("dmat2");
+        basicTypes.insert("dmat3");
+        basicTypes.insert("dmat4");
+        basicTypes.insert("mat2x2");
+        basicTypes.insert("mat2x3");
+        basicTypes.insert("mat2x4");
+        basicTypes.insert("dmat2x2");
+        basicTypes.insert("dmat2x3");
+        basicTypes.insert("dmat2x4");
+        basicTypes.insert("mat3x2");
+        basicTypes.insert("mat3x3");
+        basicTypes.insert("mat3x4");
+        basicTypes.insert("dmat3x2");
+        basicTypes.insert("dmat3x3");
+        basicTypes.insert("dmat3x4");
+        basicTypes.insert("mat4x2");
+        basicTypes.insert("mat4x3");
+        basicTypes.insert("mat4x4");
+        basicTypes.insert("dmat4x2");
+        basicTypes.insert("dmat4x3");
+        basicTypes.insert("dmat4x4");
+        basicTypes.insert("vec2");
+        basicTypes.insert("vec3");
+        basicTypes.insert("vec4");
+        basicTypes.insert("ivec2");
+        basicTypes.insert("ivec3");
+        basicTypes.insert("ivec4");
+        basicTypes.insert("bvec2");
+        basicTypes.insert("bvec3");
+        basicTypes.insert("bvec4");
+        basicTypes.insert("dvec2");
+        basicTypes.insert("dvec3");
+        basicTypes.insert("dvec4");
+        basicTypes.insert("uint");
+        basicTypes.insert("uvec2");
+        basicTypes.insert("uvec3");
+        basicTypes.insert("uvec4");
+        basicTypes.insert("lowp");
+        basicTypes.insert("mediump");
+        basicTypes.insert("highp");
+        basicTypes.insert("precision");
+        basicTypes.insert("sampler1D");
+        basicTypes.insert("sampler2D");
+        basicTypes.insert("sampler3D");
+        basicTypes.insert("samplerCube");
+        basicTypes.insert("sampler1DShadow");
+        basicTypes.insert("sampler2DShadow");
+        basicTypes.insert("samplerCubeShadow");
+        basicTypes.insert("sampler1DArray");
+        basicTypes.insert("sampler2DArray");
+        basicTypes.insert("sampler1DArrayShadow");
+        basicTypes.insert("sampler2DArrayShadow");
+        basicTypes.insert("isampler1D");
+        basicTypes.insert("isampler2D");
+        basicTypes.insert("isampler3D");
+        basicTypes.insert("isamplerCube");
+        basicTypes.insert("isampler1DArray");
+        basicTypes.insert("isampler2DArray");
+        basicTypes.insert("usampler1D");
+        basicTypes.insert("usampler2D");
+        basicTypes.insert("usampler3D");
+        basicTypes.insert("usamplerCube");
+        basicTypes.insert("usampler1DArray");
+        basicTypes.insert("usampler2DArray");
+        basicTypes.insert("sampler2DRect");
+        basicTypes.insert("sampler2DRectShadow");
+        basicTypes.insert("isampler2DRect");
+        basicTypes.insert("usampler2DRect");
+        basicTypes.insert("samplerBuffer");
+        basicTypes.insert("isamplerBuffer");
+        basicTypes.insert("usamplerBuffer");
+        basicTypes.insert("sampler2DMS");
+        basicTypes.insert("isampler2DMS");
+        basicTypes.insert("usampler2DMS");
+        basicTypes.insert("sampler2DMSArray");
+        basicTypes.insert("isampler2DMSArray");
+        basicTypes.insert("usampler2DMSArray");
+        basicTypes.insert("samplerCubeArray");
+        basicTypes.insert("samplerCubeArrayShadow");
+        basicTypes.insert("isamplerCubeArray");
+        basicTypes.insert("usamplerCubeArray");
+        basicTypes.insert("image1D");
+        basicTypes.insert("iimage1D");
+        basicTypes.insert("uimage1D");
+        basicTypes.insert("image2D");
+        basicTypes.insert("iimage2D");
+        basicTypes.insert("uimage2D");
+        basicTypes.insert("image3D");
+        basicTypes.insert("iimage3D");
+        basicTypes.insert("uimage3D");
+        basicTypes.insert("image2DRect");
+        basicTypes.insert("iimage2DRect");
+        basicTypes.insert("uimage2DRect");
+        basicTypes.insert("imageCube");
+        basicTypes.insert("iimageCube");
+        basicTypes.insert("uimageCube");
+        basicTypes.insert("imageBuffer");
+        basicTypes.insert("iimageBuffer");
+        basicTypes.insert("uimageBuffer");
+        basicTypes.insert("image1DArray");
+        basicTypes.insert("iimage1DArray");
+        basicTypes.insert("uimage1DArray");
+        basicTypes.insert("image2DArray");
+        basicTypes.insert("iimage2DArray");
+        basicTypes.insert("uimage2DArray");
+        basicTypes.insert("imageCubeArray");
+        basicTypes.insert("iimageCubeArray");
+        basicTypes.insert("uimageCubeArray");
+        basicTypes.insert("image2DMS");
+        basicTypes.insert("iimage2DMS");
+        basicTypes.insert("uimage2DMS");
+        basicTypes.insert("image2DMSArray");
+        basicTypes.insert("iimage2DMSArray");
+        basicTypes.insert("uimage2DMSArray");
+        basicTypes.insert("struct");
         //builtin variables
         //builtin variables
         //compute shaders
         //compute shaders
-        builtinVariables.add("gl_NumWorkGroups");
-        builtinVariables.add("gl_WorkGroupSize");
-        builtinVariables.add("gl_WorkGroupID");
-        builtinVariables.add("gl_LocalInvocationID");
-        builtinVariables.add("gl_GlobalInvocationID");
-        builtinVariables.add("gl_LocalInvocationIndex");
+        builtinVariables.insert("gl_NumWorkGroups");
+        builtinVariables.insert("gl_WorkGroupSize");
+        builtinVariables.insert("gl_WorkGroupID");
+        builtinVariables.insert("gl_LocalInvocationID");
+        builtinVariables.insert("gl_GlobalInvocationID");
+        builtinVariables.insert("gl_LocalInvocationIndex");
         //vertex shaders
         //vertex shaders
-        builtinVariables.add("gl_VertexID");
-        builtinVariables.add("gl_InstanceID");
-        builtinVariables.add("gl_Position");
+        builtinVariables.insert("gl_VertexID");
+        builtinVariables.insert("gl_InstanceID");
+        builtinVariables.insert("gl_Position");
         //geometry shaders
         //geometry shaders
-        builtinVariables.add("gl_PrimitiveIDIn");
-        builtinVariables.add("gl_Layer");
-        builtinVariables.add("gl_ViewportIndex");
+        builtinVariables.insert("gl_PrimitiveIDIn");
+        builtinVariables.insert("gl_Layer");
+        builtinVariables.insert("gl_ViewportIndex");
         //tesselation shaders
         //tesselation shaders
-        builtinVariables.add("gl_MaxPatchVertices");
-        builtinVariables.add("gl_PatchVerticesIn");
-        builtinVariables.add("gl_TessLevelOuter");
-        builtinVariables.add("gl_TessLevelInner");
-        builtinVariables.add("gl_TessCoord");
+        builtinVariables.insert("gl_MaxPatchVertices");
+        builtinVariables.insert("gl_PatchVerticesIn");
+        builtinVariables.insert("gl_TessLevelOuter");
+        builtinVariables.insert("gl_TessLevelInner");
+        builtinVariables.insert("gl_TessCoord");
         //fragment shaders
         //fragment shaders
-        builtinVariables.add("gl_FragCoord");
-        builtinVariables.add("gl_FrontFacing");
-        builtinVariables.add("gl_PointCoord");
-        builtinVariables.add("gl_SampleID");
-        builtinVariables.add("gl_SamplePosition");
-        builtinVariables.add("gl_SampleMaskIn");
-        builtinVariables.add("gl_Layer");
-        builtinVariables.add("gl_ViewportIndex");
-        builtinVariables.add("gl_FragColor");
+        builtinVariables.insert("gl_FragCoord");
+        builtinVariables.insert("gl_FrontFacing");
+        builtinVariables.insert("gl_PointCoord");
+        builtinVariables.insert("gl_SampleID");
+        builtinVariables.insert("gl_SamplePosition");
+        builtinVariables.insert("gl_SampleMaskIn");
+        builtinVariables.insert("gl_Layer");
+        builtinVariables.insert("gl_ViewportIndex");
+        builtinVariables.insert("gl_FragColor");
         //general
         //general
-        builtinVariables.add("gl_Position");
-        builtinVariables.add("gl_PointSize");
-        builtinVariables.add("gl_ClipDistance");
-        builtinVariables.add("gl_InvocationID");
-        builtinVariables.add("gl_PrimitiveID");
+        builtinVariables.insert("gl_Position");
+        builtinVariables.insert("gl_PointSize");
+        builtinVariables.insert("gl_ClipDistance");
+        builtinVariables.insert("gl_InvocationID");
+        builtinVariables.insert("gl_PrimitiveID");
         //jme variables - this is why we build custom plugins :) (apart from existing being under GPL)
         //jme variables - this is why we build custom plugins :) (apart from existing being under GPL)
-        builtinVariables.add("inPosition");
-        builtinVariables.add("inNormal");
-        builtinVariables.add("inColor");
-        builtinVariables.add("inTextCoord");
-        builtinVariables.add("g_WorldMatrix");
-        builtinVariables.add("g_ViewMatrix");
-        builtinVariables.add("g_ProjectionMatrix");
-        builtinVariables.add("g_WorldViewMatrix");
-        builtinVariables.add("g_WorldViewProjectionMatrix");
-        builtinVariables.add("g_WorldNormalMatrix");
-        builtinVariables.add("g_NormalMatrix");
-        builtinVariables.add("g_ViewProjectionMatrix");
-        builtinVariables.add("g_WorldMatrixInverseTranspose");
-        builtinVariables.add("g_WorldMatrixInverse");
-        builtinVariables.add("g_ViewMatrixInverse");
-        builtinVariables.add("g_ProjectionMatrixInverse");
-        builtinVariables.add("g_ViewProjectionMatrixInverse");
-        builtinVariables.add("g_WorldViewMatrixInverse");
-        builtinVariables.add("g_NormalMatrixInverse");
-        builtinVariables.add("g_WorldViewProjectionMatrixInverse");
-        builtinVariables.add("g_ViewPort");
-        builtinVariables.add("g_FrustumNearFar");
-        builtinVariables.add("g_Resolution");
-        builtinVariables.add("g_ResolutionInverse");
-        builtinVariables.add("g_Aspect");
-        builtinVariables.add("g_CameraPosition");
-        builtinVariables.add("g_CameraDirection");
-        builtinVariables.add("g_CameraLeft");
-        builtinVariables.add("g_CameraUp");
-        builtinVariables.add("g_Time");
-        builtinVariables.add("g_Tpf");
-        builtinVariables.add("g_FrameRate");
-        builtinVariables.add("g_LightDirection");
-        builtinVariables.add("g_LightPosition");
-        builtinVariables.add("g_LightColor");
-        builtinVariables.add("g_AmbientLightColor");
+        builtinVariables.insert("inPosition");
+        builtinVariables.insert("inNormal");
+        builtinVariables.insert("inColor");
+        builtinVariables.insert("inTextCoord");
+        builtinVariables.insert("g_WorldMatrix");
+        builtinVariables.insert("g_ViewMatrix");
+        builtinVariables.insert("g_ProjectionMatrix");
+        builtinVariables.insert("g_WorldViewMatrix");
+        builtinVariables.insert("g_WorldViewProjectionMatrix");
+        builtinVariables.insert("g_WorldNormalMatrix");
+        builtinVariables.insert("g_NormalMatrix");
+        builtinVariables.insert("g_ViewProjectionMatrix");
+        builtinVariables.insert("g_WorldMatrixInverseTranspose");
+        builtinVariables.insert("g_WorldMatrixInverse");
+        builtinVariables.insert("g_ViewMatrixInverse");
+        builtinVariables.insert("g_ProjectionMatrixInverse");
+        builtinVariables.insert("g_ViewProjectionMatrixInverse");
+        builtinVariables.insert("g_WorldViewMatrixInverse");
+        builtinVariables.insert("g_NormalMatrixInverse");
+        builtinVariables.insert("g_WorldViewProjectionMatrixInverse");
+        builtinVariables.insert("g_ViewPort");
+        builtinVariables.insert("g_FrustumNearFar");
+        builtinVariables.insert("g_Resolution");
+        builtinVariables.insert("g_ResolutionInverse");
+        builtinVariables.insert("g_Aspect");
+        builtinVariables.insert("g_CameraPosition");
+        builtinVariables.insert("g_CameraDirection");
+        builtinVariables.insert("g_CameraLeft");
+        builtinVariables.insert("g_CameraUp");
+        builtinVariables.insert("g_Time");
+        builtinVariables.insert("g_Tpf");
+        builtinVariables.insert("g_FrameRate");
+        builtinVariables.insert("g_LightDirection");
+        builtinVariables.insert("g_LightPosition");
+        builtinVariables.insert("g_LightColor");
+        builtinVariables.insert("g_AmbientLightColor");
         //builtin functions
         //builtin functions
-        builtinFunctions.add("radians");
-        builtinFunctions.add("degrees");
-        builtinFunctions.add("sin");
-        builtinFunctions.add("cos");
-        builtinFunctions.add("tan");
-        builtinFunctions.add("asin");
-        builtinFunctions.add("acos");
-        builtinFunctions.add("atan");
-        builtinFunctions.add("sinh");
-        builtinFunctions.add("cosh");
-        builtinFunctions.add("tanh");
-        builtinFunctions.add("asinh");
-        builtinFunctions.add("acosh");
-        builtinFunctions.add("atanh");
-        builtinFunctions.add("pow");
-        builtinFunctions.add("exp");
-        builtinFunctions.add("log");
-        builtinFunctions.add("exp2");
-        builtinFunctions.add("log2");
-        builtinFunctions.add("sqrt");
-        builtinFunctions.add("inversesqrt");
-        builtinFunctions.add("abs");
-        builtinFunctions.add("sign");
-        builtinFunctions.add("floor");
-        builtinFunctions.add("trunc");
-        builtinFunctions.add("round");
-        builtinFunctions.add("roundEven");
-        builtinFunctions.add("ceil");
-        builtinFunctions.add("fract");
-        builtinFunctions.add("mod");
-        builtinFunctions.add("modf");
-        builtinFunctions.add("min");
-        builtinFunctions.add("max");
-        builtinFunctions.add("clamp");
-        builtinFunctions.add("mix");
-        builtinFunctions.add("step");
-        builtinFunctions.add("smoothstep");
-        builtinFunctions.add("isnan");
-        builtinFunctions.add("isinf");
-        builtinFunctions.add("floatBitsToInt");
-        builtinFunctions.add("floatBitsToUInt");
-        builtinFunctions.add("intBitsToFloat");
-        builtinFunctions.add("uintBitsToFloat");
-        builtinFunctions.add("fma");
-        builtinFunctions.add("frexp");
-        builtinFunctions.add("packUnorm2x16");
-        builtinFunctions.add("packSnorm2x16");
-        builtinFunctions.add("packUnorm4x8");
-        builtinFunctions.add("packSnorm4x8");
-        builtinFunctions.add("unpackUnorm2x16");
-        builtinFunctions.add("unpackSnorm2x16");
-        builtinFunctions.add("unpackUnorm4x8");
-        builtinFunctions.add("unpackSnorm4x8");
-        builtinFunctions.add("packDouble2x32");
-        builtinFunctions.add("unpackDouble2x32");
-        builtinFunctions.add("packHalf2x16");
-        builtinFunctions.add("unpackHalf2x16");
-        builtinFunctions.add("length");
-        builtinFunctions.add("distance");
-        builtinFunctions.add("dot");
-        builtinFunctions.add("cross");
-        builtinFunctions.add("normalize");
-        builtinFunctions.add("ftransform");
-        builtinFunctions.add("faceforward");
-        builtinFunctions.add("reflect");
-        builtinFunctions.add("refract");
-        builtinFunctions.add("matrixCompMult");
-        builtinFunctions.add("outerProduct");
-        builtinFunctions.add("transpose");
-        builtinFunctions.add("determinant");
-        builtinFunctions.add("inverse");
-        builtinFunctions.add("lessThan");
-        builtinFunctions.add("lessThanEqual");
-        builtinFunctions.add("greaterThan");
-        builtinFunctions.add("greaterThanEqual");
-        builtinFunctions.add("equal");
-        builtinFunctions.add("notEqual");
-        builtinFunctions.add("any");
-        builtinFunctions.add("all");
-        builtinFunctions.add("not");
-        builtinFunctions.add("uaddCarry");
-        builtinFunctions.add("usubBorrow");
-        builtinFunctions.add("umulExtended");
-        builtinFunctions.add("imulExtended");
-        builtinFunctions.add("bitfieldExtract");
-        builtinFunctions.add("bitfieldInsert");
-        builtinFunctions.add("bitfieldReverse");
-        builtinFunctions.add("bitCount");
-        builtinFunctions.add("findLSB");
-        builtinFunctions.add("findMSB");
-        builtinFunctions.add("textureSize");
-        builtinFunctions.add("textureQueryLod");
-        builtinFunctions.add("textureQueryLevels");
-        builtinFunctions.add("texture");
-        builtinFunctions.add("textureProj");
-        builtinFunctions.add("textureLod");
-        builtinFunctions.add("textureOffset");
-        builtinFunctions.add("texelFetch");
-        builtinFunctions.add("texelFetchOffset");
-        builtinFunctions.add("textureProjOffset");
-        builtinFunctions.add("textureLodOffset");
-        builtinFunctions.add("textureProjLod");
-        builtinFunctions.add("textureProjLodOffset");
-        builtinFunctions.add("textureGrad");
-        builtinFunctions.add("textureGradOffset");
-        builtinFunctions.add("textureProjGrad");
-        builtinFunctions.add("textureProjGradOffset");
-        builtinFunctions.add("textureGather");
-        builtinFunctions.add("textureGatherOffset");
-        builtinFunctions.add("textureGatherOffsets");
-        builtinFunctions.add("texture1D");
-        builtinFunctions.add("texture1DProj");
-        builtinFunctions.add("texture1DLod");
-        builtinFunctions.add("texture1DProjLod");
-        builtinFunctions.add("texture2D");
-        builtinFunctions.add("texture2DProj");
-        builtinFunctions.add("texture2DLod");
-        builtinFunctions.add("texture2DProjLod");
-        builtinFunctions.add("texture3D");
-        builtinFunctions.add("texture3DProj");
-        builtinFunctions.add("texture3DLod");
-        builtinFunctions.add("texture3DProjLod");
-        builtinFunctions.add("textureCube");
-        builtinFunctions.add("textureCubeLod");
-        builtinFunctions.add("shadow1D");
-        builtinFunctions.add("shadow2D");
-        builtinFunctions.add("shadow1DProj");
-        builtinFunctions.add("shadow2DProj");
-        builtinFunctions.add("shadow1DLod");
-        builtinFunctions.add("shadow2DLod");
-        builtinFunctions.add("shadow1DProjLod");
-        builtinFunctions.add("shadow2DProjLod");
-        builtinFunctions.add("atomicCounterIncrement");
-        builtinFunctions.add("atomicCounterDecrement");
-        builtinFunctions.add("atomicCounter");
-        builtinFunctions.add("atomicAdd");
-        builtinFunctions.add("atomicMin");
-        builtinFunctions.add("atomicMax");
-        builtinFunctions.add("atomicAnd");
-        builtinFunctions.add("atomicOr");
-        builtinFunctions.add("atomicXor");
-        builtinFunctions.add("atomicExchange");
-        builtinFunctions.add("atomicCompSwap");
-        builtinFunctions.add("imageSize");
-        builtinFunctions.add("imageLoad");
-        builtinFunctions.add("imageStore");
-        builtinFunctions.add("imageAtomicAdd");
-        builtinFunctions.add("imageAtomicMin");
-        builtinFunctions.add("imageAtomicMax");
-        builtinFunctions.add("imageAtomicAnd");
-        builtinFunctions.add("imageAtomicOr");
-        builtinFunctions.add("imageAtomicXor");
-        builtinFunctions.add("imageAtomicExchange");
-        builtinFunctions.add("imageAtomicCompSwap");
-        builtinFunctions.add("dFdx");
-        builtinFunctions.add("dFdy");
-        builtinFunctions.add("fwidth");
-        builtinFunctions.add("interpolateAtCentroid");
-        builtinFunctions.add("interpolateAtSample");
-        builtinFunctions.add("interpolateAtOffset");
-        builtinFunctions.add("noise1");
-        builtinFunctions.add("noise2");
-        builtinFunctions.add("noise3");
-        builtinFunctions.add("noise4");
-        builtinFunctions.add("EmitStreamVertex");
-        builtinFunctions.add("EndStreamPrimitive");
-        builtinFunctions.add("EmitVertex");
-        builtinFunctions.add("EndPrimitive");
-        builtinFunctions.add("barrier");
-        builtinFunctions.add("memoryBarrier");
-        builtinFunctions.add("memoryBarrierAtomicCounter");
-        builtinFunctions.add("memoryBarrierBuffer");
-        builtinFunctions.add("memoryBarrierShared");
-        builtinFunctions.add("memoryBarrierImage");
-        builtinFunctions.add("groupMemoryBarrier");
+        builtinFunctions.insert("radians");
+        builtinFunctions.insert("degrees");
+        builtinFunctions.insert("sin");
+        builtinFunctions.insert("cos");
+        builtinFunctions.insert("tan");
+        builtinFunctions.insert("asin");
+        builtinFunctions.insert("acos");
+        builtinFunctions.insert("atan");
+        builtinFunctions.insert("sinh");
+        builtinFunctions.insert("cosh");
+        builtinFunctions.insert("tanh");
+        builtinFunctions.insert("asinh");
+        builtinFunctions.insert("acosh");
+        builtinFunctions.insert("atanh");
+        builtinFunctions.insert("pow");
+        builtinFunctions.insert("exp");
+        builtinFunctions.insert("log");
+        builtinFunctions.insert("exp2");
+        builtinFunctions.insert("log2");
+        builtinFunctions.insert("sqrt");
+        builtinFunctions.insert("inversesqrt");
+        builtinFunctions.insert("abs");
+        builtinFunctions.insert("sign");
+        builtinFunctions.insert("floor");
+        builtinFunctions.insert("trunc");
+        builtinFunctions.insert("round");
+        builtinFunctions.insert("roundEven");
+        builtinFunctions.insert("ceil");
+        builtinFunctions.insert("fract");
+        builtinFunctions.insert("mod");
+        builtinFunctions.insert("modf");
+        builtinFunctions.insert("min");
+        builtinFunctions.insert("max");
+        builtinFunctions.insert("clamp");
+        builtinFunctions.insert("mix");
+        builtinFunctions.insert("step");
+        builtinFunctions.insert("smoothstep");
+        builtinFunctions.insert("isnan");
+        builtinFunctions.insert("isinf");
+        builtinFunctions.insert("floatBitsToInt");
+        builtinFunctions.insert("floatBitsToUInt");
+        builtinFunctions.insert("intBitsToFloat");
+        builtinFunctions.insert("uintBitsToFloat");
+        builtinFunctions.insert("fma");
+        builtinFunctions.insert("frexp");
+        builtinFunctions.insert("packUnorm2x16");
+        builtinFunctions.insert("packSnorm2x16");
+        builtinFunctions.insert("packUnorm4x8");
+        builtinFunctions.insert("packSnorm4x8");
+        builtinFunctions.insert("unpackUnorm2x16");
+        builtinFunctions.insert("unpackSnorm2x16");
+        builtinFunctions.insert("unpackUnorm4x8");
+        builtinFunctions.insert("unpackSnorm4x8");
+        builtinFunctions.insert("packDouble2x32");
+        builtinFunctions.insert("unpackDouble2x32");
+        builtinFunctions.insert("packHalf2x16");
+        builtinFunctions.insert("unpackHalf2x16");
+        builtinFunctions.insert("length");
+        builtinFunctions.insert("distance");
+        builtinFunctions.insert("dot");
+        builtinFunctions.insert("cross");
+        builtinFunctions.insert("normalize");
+        builtinFunctions.insert("ftransform");
+        builtinFunctions.insert("faceforward");
+        builtinFunctions.insert("reflect");
+        builtinFunctions.insert("refract");
+        builtinFunctions.insert("matrixCompMult");
+        builtinFunctions.insert("outerProduct");
+        builtinFunctions.insert("transpose");
+        builtinFunctions.insert("determinant");
+        builtinFunctions.insert("inverse");
+        builtinFunctions.insert("lessThan");
+        builtinFunctions.insert("lessThanEqual");
+        builtinFunctions.insert("greaterThan");
+        builtinFunctions.insert("greaterThanEqual");
+        builtinFunctions.insert("equal");
+        builtinFunctions.insert("notEqual");
+        builtinFunctions.insert("any");
+        builtinFunctions.insert("all");
+        builtinFunctions.insert("not");
+        builtinFunctions.insert("uaddCarry");
+        builtinFunctions.insert("usubBorrow");
+        builtinFunctions.insert("umulExtended");
+        builtinFunctions.insert("imulExtended");
+        builtinFunctions.insert("bitfieldExtract");
+        builtinFunctions.insert("bitfieldInsert");
+        builtinFunctions.insert("bitfieldReverse");
+        builtinFunctions.insert("bitCount");
+        builtinFunctions.insert("findLSB");
+        builtinFunctions.insert("findMSB");
+        builtinFunctions.insert("textureSize");
+        builtinFunctions.insert("textureQueryLod");
+        builtinFunctions.insert("textureQueryLevels");
+        builtinFunctions.insert("texture");
+        builtinFunctions.insert("textureProj");
+        builtinFunctions.insert("textureLod");
+        builtinFunctions.insert("textureOffset");
+        builtinFunctions.insert("texelFetch");
+        builtinFunctions.insert("texelFetchOffset");
+        builtinFunctions.insert("textureProjOffset");
+        builtinFunctions.insert("textureLodOffset");
+        builtinFunctions.insert("textureProjLod");
+        builtinFunctions.insert("textureProjLodOffset");
+        builtinFunctions.insert("textureGrad");
+        builtinFunctions.insert("textureGradOffset");
+        builtinFunctions.insert("textureProjGrad");
+        builtinFunctions.insert("textureProjGradOffset");
+        builtinFunctions.insert("textureGather");
+        builtinFunctions.insert("textureGatherOffset");
+        builtinFunctions.insert("textureGatherOffsets");
+        builtinFunctions.insert("texture1D");
+        builtinFunctions.insert("texture1DProj");
+        builtinFunctions.insert("texture1DLod");
+        builtinFunctions.insert("texture1DProjLod");
+        builtinFunctions.insert("texture2D");
+        builtinFunctions.insert("texture2DProj");
+        builtinFunctions.insert("texture2DLod");
+        builtinFunctions.insert("texture2DProjLod");
+        builtinFunctions.insert("texture3D");
+        builtinFunctions.insert("texture3DProj");
+        builtinFunctions.insert("texture3DLod");
+        builtinFunctions.insert("texture3DProjLod");
+        builtinFunctions.insert("textureCube");
+        builtinFunctions.insert("textureCubeLod");
+        builtinFunctions.insert("shadow1D");
+        builtinFunctions.insert("shadow2D");
+        builtinFunctions.insert("shadow1DProj");
+        builtinFunctions.insert("shadow2DProj");
+        builtinFunctions.insert("shadow1DLod");
+        builtinFunctions.insert("shadow2DLod");
+        builtinFunctions.insert("shadow1DProjLod");
+        builtinFunctions.insert("shadow2DProjLod");
+        builtinFunctions.insert("atomicCounterIncrement");
+        builtinFunctions.insert("atomicCounterDecrement");
+        builtinFunctions.insert("atomicCounter");
+        builtinFunctions.insert("atomicAdd");
+        builtinFunctions.insert("atomicMin");
+        builtinFunctions.insert("atomicMax");
+        builtinFunctions.insert("atomicAnd");
+        builtinFunctions.insert("atomicOr");
+        builtinFunctions.insert("atomicXor");
+        builtinFunctions.insert("atomicExchange");
+        builtinFunctions.insert("atomicCompSwap");
+        builtinFunctions.insert("imageSize");
+        builtinFunctions.insert("imageLoad");
+        builtinFunctions.insert("imageStore");
+        builtinFunctions.insert("imageAtomicAdd");
+        builtinFunctions.insert("imageAtomicMin");
+        builtinFunctions.insert("imageAtomicMax");
+        builtinFunctions.insert("imageAtomicAnd");
+        builtinFunctions.insert("imageAtomicOr");
+        builtinFunctions.insert("imageAtomicXor");
+        builtinFunctions.insert("imageAtomicExchange");
+        builtinFunctions.insert("imageAtomicCompSwap");
+        builtinFunctions.insert("dFdx");
+        builtinFunctions.insert("dFdy");
+        builtinFunctions.insert("fwidth");
+        builtinFunctions.insert("interpolateAtCentroid");
+        builtinFunctions.insert("interpolateAtSample");
+        builtinFunctions.insert("interpolateAtOffset");
+        builtinFunctions.insert("noise1");
+        builtinFunctions.insert("noise2");
+        builtinFunctions.insert("noise3");
+        builtinFunctions.insert("noise4");
+        builtinFunctions.insert("EmitStreamVertex");
+        builtinFunctions.insert("EndStreamPrimitive");
+        builtinFunctions.insert("EmitVertex");
+        builtinFunctions.insert("EndPrimitive");
+        builtinFunctions.insert("barrier");
+        builtinFunctions.insert("memoryBarrier");
+        builtinFunctions.insert("memoryBarrierAtomicCounter");
+        builtinFunctions.insert("memoryBarrierBuffer");
+        builtinFunctions.insert("memoryBarrierShared");
+        builtinFunctions.insert("memoryBarrierImage");
+        builtinFunctions.insert("groupMemoryBarrier");
     }
     }
 
 
+    /**
+     * Finds if given string is either a partial match, full match or nothing at
+     * all
+     *
+     * @param s string to search for
+     * @return returns the status of the string
+     */
     public static KeywordType lookup(String s) {
     public static KeywordType lookup(String s) {
         KeywordType returnType = null;
         KeywordType returnType = null;
-        for (String primitive : basicTypes) {
-            if (primitive.startsWith(s)) {
-                if (primitive.equals(s)) {
-                    returnType = KeywordType.BASIC_TYPE;
-                    break;
-                } else {
-                    returnType = KeywordType.UNFINISHED;
-                }
-            }
+        returnType = lookup(s, returnType, KeywordType.BASIC_TYPE, basicTypes);
+        if (returnType == KeywordType.UNFINISHED || returnType == null) {
+            returnType = lookup(s, returnType, KeywordType.BUILTIN_VARIABLE, builtinVariables);
         }
         }
-        for (String var : builtinVariables) {
-            if (var.startsWith(s)) {
-                if (var.equals(s)) {
-                    returnType = KeywordType.BUILTIN_VARIABLE;
-                    break;
-                } else {
-                    returnType = KeywordType.UNFINISHED;
-                }
-            }
+        if (returnType == KeywordType.UNFINISHED || returnType == null) {
+            returnType = lookup(s, returnType, KeywordType.BUILTIN_FUNCTION, builtinFunctions);
         }
         }
-        for (String func : builtinFunctions) {
-            if (func.startsWith(s) && (returnType == KeywordType.UNFINISHED || returnType == null)) {
-                if (func.equals(s)) {
-                    returnType = KeywordType.BUILTIN_FUNCTION;
-                    break;
-                } else {
-                    returnType = KeywordType.UNFINISHED;
-                }
-            }
-        }
-        for (String keyword : keywords) {
-            if (keyword.startsWith(s) && (returnType == KeywordType.UNFINISHED || returnType == null)) {
-                if (keyword.equals(s)) {
-                    returnType = KeywordType.KEYWORD;
-                    break;
-                } else {
-                    returnType = KeywordType.UNFINISHED;
-                }
-            }
+        if (returnType == KeywordType.UNFINISHED || returnType == null) {
+            returnType = lookup(s, returnType, KeywordType.KEYWORD, keywords);
         }
         }
 
 
         return returnType;
         return returnType;
     }
     }
+
+    /**
+     * Gets all the possible matches for given string (auto-complete)
+     *
+     * @param s string to search for
+     * @return all the matches for given string
+     */
+    public static List<Keyword> lookupAll(String s) {
+        List<Keyword> matches = new ArrayList<>();
+
+        matches.addAll(basicTypes.searchAll(s).stream().map((keyword) -> new Keyword(keyword, KeywordType.BASIC_TYPE)).toList());
+        matches.addAll(builtinVariables.searchAll(s).stream().map((keyword) -> new Keyword(keyword, KeywordType.BUILTIN_VARIABLE)).toList());
+        matches.addAll(builtinFunctions.searchAll(s).stream().map((keyword) -> new Keyword(keyword, KeywordType.BUILTIN_FUNCTION)).toList());
+        matches.addAll(keywords.searchAll(s).stream().map((keyword) -> new Keyword(keyword, KeywordType.KEYWORD)).toList());
+
+        return matches;
+    }
+
+    private static KeywordType lookup(String s, KeywordType currentType, KeywordType matchType, Trie searchTrie) {
+        Trie.MatchType match = searchTrie.search(s);
+        if (match == Trie.MatchType.FULL_MATCH) {
+            return matchType;
+        }
+        if (match == Trie.MatchType.PARTIAL_MATCH) {
+            return KeywordType.UNFINISHED;
+        }
+
+        return currentType;
+    }
 }
 }

+ 20 - 26
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/lexer/GlslLexer.java

@@ -190,21 +190,26 @@ public class GlslLexer implements Lexer<GlslTokenID> {
                             }
                             }
                         }
                         }
                     }
                     }
-                    if (current == null) {
-                    }
-                    switch (current) {
-                        case BASIC_TYPE:
-                            return token(GlslTokenID.PRIMITIVE);
-                        case KEYWORD:
-                            return token(GlslTokenID.KEYWORD);
-                        case BUILTIN_VARIABLE:
-                            return token(GlslTokenID.BUILTIN_VARIABLE);
-                        case BUILTIN_FUNCTION:
-                            return token(GlslTokenID.BUILTIN_FUNCTION);
+                    if (current != null) {
+                        switch (current) {
+                            case BASIC_TYPE -> {
+                                return token(GlslTokenID.PRIMITIVE);
+                            }
+                            case KEYWORD -> {
+                                return token(GlslTokenID.KEYWORD);
+                            }
+                            case BUILTIN_VARIABLE -> {
+                                return token(GlslTokenID.BUILTIN_VARIABLE);
+                            }
+                            case BUILTIN_FUNCTION -> {
+                                return token(GlslTokenID.BUILTIN_FUNCTION);
+                            }
+                        }
                     }
                     }
                 }
                 }
             }
             }
         }
         }
+        
         //Those have to be recognized separately for closing bracket recognition
         //Those have to be recognized separately for closing bracket recognition
         return token(GlslTokenID.TEXT);
         return token(GlslTokenID.TEXT);
     }
     }
@@ -224,21 +229,10 @@ public class GlslLexer implements Lexer<GlslTokenID> {
     }
     }
 
 
     private boolean isDigit(int c) {
     private boolean isDigit(int c) {
-        switch (c) {
-            case '0':
-            case '1':
-            case '2':
-            case '3':
-            case '4':
-            case '5':
-            case '6':
-            case '7':
-            case '8':
-            case '9':
-                return true;
-            default:
-                return false;
-        }
+        return switch (c) {
+            case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> true;
+            default -> false;
+        };
     }
     }
 
 
     private void readTillNewLine() {
     private void readTillNewLine() {

+ 147 - 0
jme3-glsl-highlighter/src/com/jme3/gde/glsl/highlighter/util/Trie.java

@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2003-2024 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.glsl.highlighter.util;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Simple and efficient text search
+ */
+public class Trie {
+
+    private final TrieNode root;
+
+    private static class TrieNode {
+
+        Map<Character, TrieNode> children = new HashMap<>();
+        boolean isEndOfWord = false;
+    }
+
+    private record Pair(TrieNode node, StringBuilder prefix) {
+
+    }
+
+    public enum MatchType {
+        NO_MATCH,
+        PARTIAL_MATCH,
+        FULL_MATCH
+    }
+
+    public Trie() {
+        root = new TrieNode();
+    }
+
+    /**
+     * Insert word to the Trie structure
+     *
+     * @param word word to insert
+     */
+    public void insert(String word) {
+        TrieNode current = root;
+        for (char ch : word.toCharArray()) {
+            current = current.children.computeIfAbsent(ch, c -> new TrieNode());
+        }
+        current.isEndOfWord = true;
+    }
+
+    private TrieNode findNode(String word) {
+        TrieNode current = root;
+        for (char ch : word.toCharArray()) {
+            if (!current.children.containsKey(ch)) {
+                return null;
+            }
+            current = current.children.get(ch);
+        }
+
+        return current;
+    }
+
+    /**
+     * Searches for the string
+     *
+     * @param word word to search for
+     * @return match type
+     */
+    public MatchType search(String word) {
+        TrieNode node = findNode(word);
+        if (node == null) {
+            return MatchType.NO_MATCH;
+        }
+
+        return node.isEndOfWord ? MatchType.FULL_MATCH : MatchType.PARTIAL_MATCH;
+    }
+
+    /**
+     * Searches for the string and gives out all the possible matches
+     *
+     * @param word word to search for
+     */
+    public List<String> searchAll(String word) {
+        TrieNode node = findNode(word);
+        if (node == null) {
+            return Collections.emptyList();
+        }
+
+        return collectAllWords(node, word);
+    }
+
+    private static List<String> collectAllWords(TrieNode startNode, String prefix) {
+        List<String> results = new ArrayList<>();
+        Deque<Pair> stack = new ArrayDeque<>();
+        stack.push(new Pair(startNode, new StringBuilder(prefix)));
+
+        while (!stack.isEmpty()) {
+            Pair pair = stack.pop();
+            TrieNode node = pair.node;
+            StringBuilder currentPrefix = pair.prefix;
+
+            if (node.isEndOfWord) {
+                results.add(currentPrefix.toString());
+            }
+
+            for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
+                char nextChar = entry.getKey();
+                TrieNode childNode = entry.getValue();
+                stack.push(new Pair(childNode, new StringBuilder(currentPrefix).append(nextChar)));
+            }
+        }
+
+        return results;
+    }
+
+}

+ 1 - 0
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/Bundle.properties

@@ -4,6 +4,7 @@ CTL_OpenSceneComposer=Edit in SceneComposer
 CTL_SceneComposerAction=SceneComposer
 CTL_SceneComposerAction=SceneComposer
 CTL_SceneComposerTopComponent=SceneComposer Window
 CTL_SceneComposerTopComponent=SceneComposer Window
 CTL_SomeAction=SomeAction
 CTL_SomeAction=SomeAction
+CTL_MergeAnimationsAction=Merge Animations
 HINT_SceneComposerTopComponent=This is a SceneComposer window
 HINT_SceneComposerTopComponent=This is a SceneComposer window
 OpenIDE-Module-Display-Category=jMonkeyEngine
 OpenIDE-Module-Display-Category=jMonkeyEngine
 OpenIDE-Module-Long-Description=\
 OpenIDE-Module-Long-Description=\

+ 14 - 1
jme3-scenecomposer/src/com/jme3/gde/scenecomposer/layer.xml

@@ -33,6 +33,15 @@
                 <attr name="selectionType" stringvalue="ANY"/>
                 <attr name="selectionType" stringvalue="ANY"/>
                 <attr name="type" stringvalue="com.jme3.gde.core.assets.SpatialAssetDataObject"/>
                 <attr name="type" stringvalue="com.jme3.gde.core.assets.SpatialAssetDataObject"/>
             </file>
             </file>
+            <file name="com-jme3-gde-core-assets-actions-MergeAnimationsAction.instance">
+                <attr name="delegate" methodvalue="org.openide.awt.Actions.inject"/>
+                <attr name="displayName" bundlevalue="com.jme3.gde.scenecomposer.Bundle#CTL_MergeAnimationsAction"/>
+                <attr name="injectable" stringvalue="com.jme3.gde.core.assets.actions.MergeAnimationsAction"/>
+                <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
+                <attr name="noIconInMenu" boolvalue="false"/>
+                <attr name="selectionType" stringvalue="ANY"/>
+                <attr name="type" stringvalue="com.jme3.gde.core.assets.SpatialAssetDataObject"/>
+            </file>
         </folder>
         </folder>
         <folder name="Window">
         <folder name="Window">
             <file name="com-jme3-gde-scenecomposer-SceneComposerAction.instance">
             <file name="com-jme3-gde-scenecomposer-SceneComposerAction.instance">
@@ -59,9 +68,13 @@
                         <attr name="originalFile" stringvalue="Actions/SceneComposer/com-jme3-gde-scenecomposer-LinkSceneComposer.instance"/>
                         <attr name="originalFile" stringvalue="Actions/SceneComposer/com-jme3-gde-scenecomposer-LinkSceneComposer.instance"/>
                         <attr name="position" intvalue="30"/>
                         <attr name="position" intvalue="30"/>
                     </file>
                     </file>
+                    <file name="com-jme3-gde-core-assets-actions-animation-MergeAnimationsAction.shadow">
+                        <attr name="originalFile" stringvalue="Actions/SceneComposer/com-jme3-gde-core-assets-actions-MergeAnimationsAction.instance"/>
+                        <attr name="position" intvalue="40"/>
+                    </file>
                     <file name="scenecomposer_sep-2.instance">
                     <file name="scenecomposer_sep-2.instance">
                         <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/>
                         <attr name="instanceClass" stringvalue="javax.swing.JSeparator"/>
-                        <attr name="position" intvalue="40"/>
+                        <attr name="position" intvalue="45"/>
                     </file>
                     </file>
                 </folder>
                 </folder>
             </folder>
             </folder>

BIN
jme3-templates/src/com/jme3/gde/templates/GradleDesktopGameProject.zip


+ 6 - 2
jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/AdditionalLibrary.java

@@ -90,7 +90,7 @@ public enum AdditionalLibrary implements TemplateLibrary {
     HEART("Heart Library", NbBundle.getMessage(AdditionalLibrary.class,
     HEART("Heart Library", NbBundle.getMessage(AdditionalLibrary.class,
             "additionalLibrary.heart.description"),
             "additionalLibrary.heart.description"),
             "com.github.stephengold", "Heart",
             "com.github.stephengold", "Heart",
-            "8.1.0", false),
+            "9.0.0", false),
     PARTICLE_MONKEY("Particle Monkey",
     PARTICLE_MONKEY("Particle Monkey",
             NbBundle.getMessage(AdditionalLibrary.class,
             NbBundle.getMessage(AdditionalLibrary.class,
             "additionalLibrary.particlemonkey.description"),
             "additionalLibrary.particlemonkey.description"),
@@ -111,7 +111,11 @@ public enum AdditionalLibrary implements TemplateLibrary {
     ZAY_ES_NET("Zay-ES-Net Networking Extension",
     ZAY_ES_NET("Zay-ES-Net Networking Extension",
             NbBundle.getMessage(AdditionalLibrary.class,
             NbBundle.getMessage(AdditionalLibrary.class,
             "additionalLibrary.zayesnet.description"),
             "additionalLibrary.zayesnet.description"),
-            "com.simsilica", "zay-es-net", "1.5.0", false);
+            "com.simsilica", "zay-es-net", "1.5.0", false),
+    WES("Wes Library", NbBundle.getMessage(AdditionalLibrary.class,
+            "additionalLibrary.wes.description"),
+            "com.github.stephengold", "Wes",
+            "0.8.1", false),;
 
 
     /**
     /**
      * The name of the library. This will be displayed in the jComboBox in the
      * The name of the library. This will be displayed in the jComboBox in the

+ 2 - 1
jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/Bundle.properties

@@ -5,7 +5,8 @@ additionalLibrary.jme3-vr.description=Core jMonkeyEngine library providing Virtu
 additionalLibrary.heart.description=The Heart Library provides an assortment of useful classes and assets to augment jMonkeyEngine.
 additionalLibrary.heart.description=The Heart Library provides an assortment of useful classes and assets to augment jMonkeyEngine.
 additionalLibrary.particlemonkey.description=Particle Monkey is a more modern particle system with better artistic controls.
 additionalLibrary.particlemonkey.description=Particle Monkey is a more modern particle system with better artistic controls.
 additionalLibrary.shaderblowex.description=Extended filters library for JMonkey Game Engine.
 additionalLibrary.shaderblowex.description=Extended filters library for JMonkey Game Engine.
-additionalLibrary.sio2.description=A base library of useful utility code for JME-based games. \
+additionalLibrary.sio2.description=A base library of useful utility code for JME-based games. 
+additionalLibrary.wes.description=An animation editing and retargeting library for jMonkeyEngine. \
 Includes game system management infrastructure, useful base app states, an event bus, and useful Zay-ES utilities. \
 Includes game system management infrastructure, useful base app states, an event bus, and useful Zay-ES utilities. \
 This is a useful base library for any JME game.
 This is a useful base library for any JME game.
 additionalLibrary.zayes.description=Zay-ES (pronounced like Doctor Zaius from Planet of the Apes) is a high-performance \
 additionalLibrary.zayes.description=Zay-ES (pronounced like Doctor Zaius from Planet of the Apes) is a high-performance \

+ 1 - 1
jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/NetworkingLibrary.java

@@ -79,7 +79,7 @@ public enum NetworkingLibrary implements TemplateLibrary {
     SIMETHEREAL("SimEthereal", NbBundle.getMessage(NetworkingLibrary.class,
     SIMETHEREAL("SimEthereal", NbBundle.getMessage(NetworkingLibrary.class,
             "networkinglibrary.simethereal.description"),
             "networkinglibrary.simethereal.description"),
             "com.simsilica", "sim-ethereal",
             "com.simsilica", "sim-ethereal",
-            "1.7.0", false);
+            "1.8.0", false);
 
 
     /**
     /**
      * The name of the library. This will be displayed in the jComboBox in the
      * The name of the library. This will be displayed in the jComboBox in the

+ 1 - 1
jme3-templates/src/com/jme3/gde/templates/gradledesktop/options/PhysicsLibrary.java

@@ -75,7 +75,7 @@ public enum PhysicsLibrary implements TemplateLibrary {
     MINIE("Minie", NbBundle.getMessage(PhysicsLibrary.class,
     MINIE("Minie", NbBundle.getMessage(PhysicsLibrary.class,
             "physicslibrary.minie.description"),
             "physicslibrary.minie.description"),
             "com.github.stephengold", "Minie",
             "com.github.stephengold", "Minie",
-            "5.0.0", false);
+            "8.2.0", false);
 
 
     /**
     /**
      * The name of the library. This will be displayed in the jComboBox in the
      * The name of the library. This will be displayed in the jComboBox in the

BIN
nbi/antlib/nbi-ant-tasks.jar


BIN
nbi/antlib/nbi-engine.jar


BIN
nbi/antlib/nbi-registries-management.jar