Browse Source

add templates for MonkeyZone and JaimesAscent

rickard 7 months ago
parent
commit
9f1cdedaaf
24 changed files with 2776 additions and 2 deletions
  1. 3 1
      jme3-templates/src/com/jme3/gde/templates/Bundle.properties
  2. 16 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/Bundle.properties
  3. 13 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDescription.html
  4. 189 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDownloadPanel.java
  5. 120 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDownloadPanelVisual.form
  6. 194 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDownloadPanelVisual.java
  7. 122 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentPanelVisual.form
  8. 291 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentPanelVisual.java
  9. 221 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentWizardIterator.java
  10. 126 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentWizardPanel.java
  11. 65 0
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/build.gradle.ftl
  12. BIN
      jme3-templates/src/com/jme3/gde/templates/jaimesascent/jaimesascent.png
  13. 26 1
      jme3-templates/src/com/jme3/gde/templates/layer.xml
  14. 16 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/Bundle.properties
  15. 13 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDescription.html
  16. 189 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDownloadPanel.java
  17. 120 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDownloadPanelVisual.form
  18. 194 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDownloadPanelVisual.java
  19. 122 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZonePanelVisual.form
  20. 291 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZonePanelVisual.java
  21. 254 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneWizardIterator.java
  22. 126 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneWizardPanel.java
  23. 65 0
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/build.gradle.ftl
  24. BIN
      jme3-templates/src/com/jme3/gde/templates/monkeyzone/monkeyzone.png

+ 3 - 1
jme3-templates/src/com/jme3/gde/templates/Bundle.properties

@@ -6,4 +6,6 @@ OpenIDE-Module-Name=Project Templates
 OpenIDE-Module-Short-Description=Provides Project Templates
 Templates/Project/JME3/BasicGameProject.zip=Basic Game (with Ant)
 Templates/Project/JME3/GradleDesktopGameProject.zip=Basic Game (with Gradle)
-Templates/Project/JME3/RollingTheMonkeyProject.zip=Rolling The Monkey
+Templates/Project/JME3/Examples/RollingTheMonkeyProject.zip=Rolling The Monkey
+Templates/Project/JME3/Examples/MonkeyZone=Monkey Zone
+Templates/Project/JME3/Examples/JaimesAscent=Jaimes Ascent

+ 16 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/Bundle.properties

@@ -0,0 +1,16 @@
+LBL_DownloadProjectStep=Download project
+LBL_CreateProjectStep=Name and Location
+JaimesAscentPanelVisual.browseButton.text=Br&owse...
+JaimesAscentPanelVisual.createdFolderLabel.text=Project &Folder:
+JaimesAscentPanelVisual.projectLocationLabel.text=Project &Location:
+JaimesAscentDownloadPanelVisual.downloadButton.text=Download project
+JaimesAscentDownloadPanelVisual.downloadButton.actionCommand=DOWNLOAD
+JaimesAscentPanelVisual.projectNameLabel.text=Project &Name:
+JaimesAscentPanelVisual.browseButton.text=Browse
+JaimesAscentPanelVisual.browseButton.actionCommand=BROWSE
+JaimesAscentDownloadPanelVisual.jTextArea1.text=Pressing the button below will download the project from Github, to a temporary location from which it will be installed in the next step\n
+JaimesAscentDownloadPanelVisual.statusField.text=
+JaimesAscentDownloadPanelVisual.downloading=Downloading... Please wait
+JaimesAscentDownloadPanelVisual.downloadSuccess=Download complete. Press 'next' to proceed.
+JaimesAscentDownloadPanelVisual.downloadFailed=Download failed.
+JaimesAscentDownloadPanelVisual.jLabel1.text=

+ 13 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDescription.html

@@ -0,0 +1,13 @@
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+        <title>Jaimes Ascent</title>
+    </head>
+    <body>
+        A sample application demonstrating a chase cam with mouse look,<br>
+        physics, moving objects and animations.
+        
+        Easily extendable classes and architecture using AppStates and Controls.<br>
+        Suitable for beginners.<br>
+    </body>
+</html>

+ 189 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDownloadPanel.java

@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 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.templates.jaimesascent;
+
+import java.awt.Component;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.Set;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+import org.openide.util.HelpCtx;
+import org.openide.util.NbBundle;
+
+/**
+ * Panel just asking for basic info.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class JaimesAscentDownloadPanel implements WizardDescriptor.Panel,
+        WizardDescriptor.ValidatingPanel, WizardDescriptor.FinishablePanel {
+
+    private WizardDescriptor wizardDescriptor;
+    private JaimesAscentDownloadPanelVisual component;
+    
+    static String ZIP_NAME = "JaimesAscent.zip";
+    static String DOWNLOAD_FOLDER = System.getProperty("java.io.tmpdir");
+
+    public JaimesAscentDownloadPanel() {
+    }
+    
+    public int doDownloadZip() {
+        return downloadFile("https://github.com/neph1/JaimesAscent/archive/refs/tags/v1.1.1.zip", DOWNLOAD_FOLDER, ZIP_NAME);
+    }
+    
+    private int downloadFile(String fileURL, String saveDir, String fileName) {
+        HttpURLConnection httpConn = null;
+        BufferedInputStream inputStream = null;
+        FileOutputStream fileOutputStream = null;
+        
+        final File outputFile = new File(saveDir, fileName);
+        
+        if (outputFile.exists()) {
+            return 1;
+        }
+
+        try {
+            // Create URL object
+            URL url = new URL(fileURL);
+            httpConn = (HttpURLConnection) url.openConnection();
+
+            // Check HTTP response code
+            int responseCode = httpConn.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) {
+                // Open input stream from the HTTP connection
+                inputStream = new BufferedInputStream(httpConn.getInputStream());
+                
+                
+
+                // Create output stream to save the file
+                fileOutputStream = new FileOutputStream(outputFile);
+
+                byte[] buffer = new byte[4096];
+                int bytesRead;
+                while ((bytesRead = inputStream.read(buffer)) != -1) {
+                    fileOutputStream.write(buffer, 0, bytesRead);
+                }
+
+                return 1;
+            }
+        } catch (IOException e) {
+            return 0;
+        } finally {
+            // Close resources
+            try {
+                if (inputStream != null) inputStream.close();
+                if (fileOutputStream != null) fileOutputStream.close();
+                if (httpConn != null) httpConn.disconnect();
+            } catch (IOException ex) {
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public Component getComponent() {
+        if (component == null) {
+            component = new JaimesAscentDownloadPanelVisual(this);
+            component.setName(NbBundle.getMessage(JaimesAscentDownloadPanel.class, "LBL_DownloadProjectStep"));
+        }
+        return component;
+    }
+
+    @Override
+    public HelpCtx getHelp() {
+        return new HelpCtx("sdk.download_project");
+    }
+
+    @Override
+    public boolean isValid() {
+        getComponent();
+        return component.valid(wizardDescriptor);
+    }
+    
+    private final Set<ChangeListener> listeners = new HashSet<>(1); // or can use ChangeSupport in NB 6.0
+
+    @Override
+    public final void addChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.add(l);
+        }
+    }
+
+    @Override
+    public final void removeChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.remove(l);
+        }
+    }
+
+    protected final void fireChangeEvent() {
+        Set<ChangeListener> ls;
+        synchronized (listeners) {
+            ls = new HashSet<>(listeners);
+        }
+        ChangeEvent ev = new ChangeEvent(this);
+        for (ChangeListener l : ls) {
+            l.stateChanged(ev);
+        }
+    }
+
+    @Override
+    public void readSettings(Object settings) {
+        wizardDescriptor = (WizardDescriptor) settings;
+        component.read(wizardDescriptor);
+    }
+
+    @Override
+    public void storeSettings(Object settings) {
+        WizardDescriptor d = (WizardDescriptor) settings;
+        component.store(d);
+    }
+
+    @Override
+    public boolean isFinishPanel() {
+        return false;
+    }
+
+    @Override
+    public void validate() throws WizardValidationException {
+        getComponent();
+        component.validate(wizardDescriptor);
+    }
+}

+ 120 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDownloadPanelVisual.form

@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.5" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="0"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" alignment="0" attributes="0">
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Group type="102" attributes="0">
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Group type="103" groupAlignment="0" attributes="0">
+                          <Component id="jScrollPane1" alignment="0" min="-2" pref="307" max="-2" attributes="0"/>
+                          <Component id="statusField" alignment="1" min="-2" pref="313" max="-2" attributes="0"/>
+                      </Group>
+                  </Group>
+                  <Group type="102" alignment="0" attributes="0">
+                      <EmptySpace min="-2" pref="99" max="-2" attributes="0"/>
+                      <Component id="downloadButton" min="-2" max="-2" attributes="0"/>
+                  </Group>
+              </Group>
+              <EmptySpace max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Group type="102" alignment="0" attributes="0">
+                      <EmptySpace min="-2" pref="71" max="-2" attributes="0"/>
+                      <Component id="jScrollPane1" min="-2" max="-2" attributes="0"/>
+                      <EmptySpace min="-2" pref="21" max="-2" attributes="0"/>
+                      <Component id="downloadButton" min="-2" max="-2" attributes="0"/>
+                      <EmptySpace type="separate" max="-2" attributes="0"/>
+                      <Component id="statusField" min="-2" pref="52" max="-2" attributes="0"/>
+                  </Group>
+                  <Group type="102" alignment="0" attributes="0">
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
+                  </Group>
+              </Group>
+              <EmptySpace max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="javax.swing.JButton" name="downloadButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentDownloadPanelVisual.downloadButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="actionCommand" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentDownloadPanelVisual.downloadButton.actionCommand" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="downloadButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Container class="javax.swing.JScrollPane" name="jScrollPane1">
+      <AuxValues>
+        <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
+      </AuxValues>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
+      <SubComponents>
+        <Component class="javax.swing.JTextArea" name="jTextArea1">
+          <Properties>
+            <Property name="editable" type="boolean" value="false"/>
+            <Property name="columns" type="int" value="20"/>
+            <Property name="lineWrap" type="boolean" value="true"/>
+            <Property name="rows" type="int" value="5"/>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentDownloadPanelVisual.jTextArea1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+            <Property name="wrapStyleWord" type="boolean" value="true"/>
+            <Property name="enabled" type="boolean" value="false"/>
+          </Properties>
+        </Component>
+      </SubComponents>
+    </Container>
+    <Component class="javax.swing.JTextField" name="statusField">
+      <Properties>
+        <Property name="editable" type="boolean" value="false"/>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentDownloadPanelVisual.statusField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="enabled" type="boolean" value="false"/>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="statusFieldActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="jLabel1">
+      <Properties>
+        <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
+          <Image iconType="3" name="/com/jme3/gde/templates/jaimesascent/jaimesascent.png"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentDownloadPanelVisual.jLabel1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+  </SubComponents>
+</Form>

+ 194 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentDownloadPanelVisual.java

@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 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.templates.jaimesascent;
+
+import javax.swing.JPanel;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+
+public class JaimesAscentDownloadPanelVisual extends JPanel implements DocumentListener {
+
+    public static final String PROP_PROJECT_NAME = "projectName";
+    private final JaimesAscentDownloadPanel panel;
+
+    public JaimesAscentDownloadPanelVisual(JaimesAscentDownloadPanel panel) {
+        initComponents();
+        this.panel = panel;
+    }
+
+    /** This method is called from within the constructor to
+     * initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is
+     * always regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        downloadButton = new javax.swing.JButton();
+        jScrollPane1 = new javax.swing.JScrollPane();
+        jTextArea1 = new javax.swing.JTextArea();
+        statusField = new javax.swing.JTextField();
+        jLabel1 = new javax.swing.JLabel();
+
+        org.openide.awt.Mnemonics.setLocalizedText(downloadButton, org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.downloadButton.text")); // NOI18N
+        downloadButton.setActionCommand(org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.downloadButton.actionCommand")); // NOI18N
+        downloadButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                downloadButtonActionPerformed(evt);
+            }
+        });
+
+        jTextArea1.setEditable(false);
+        jTextArea1.setColumns(20);
+        jTextArea1.setLineWrap(true);
+        jTextArea1.setRows(5);
+        jTextArea1.setText(org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.jTextArea1.text")); // NOI18N
+        jTextArea1.setWrapStyleWord(true);
+        jTextArea1.setEnabled(false);
+        jScrollPane1.setViewportView(jTextArea1);
+
+        statusField.setEditable(false);
+        statusField.setText(org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.statusField.text")); // NOI18N
+        statusField.setEnabled(false);
+        statusField.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                statusFieldActionPerformed(evt);
+            }
+        });
+
+        jLabel1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/com/jme3/gde/templates/jaimesascent/jaimesascent.png"))); // NOI18N
+        org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.jLabel1.text")); // NOI18N
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addComponent(jLabel1)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addGroup(layout.createSequentialGroup()
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                            .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 307, javax.swing.GroupLayout.PREFERRED_SIZE)
+                            .addComponent(statusField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 313, javax.swing.GroupLayout.PREFERRED_SIZE)))
+                    .addGroup(layout.createSequentialGroup()
+                        .addGap(99, 99, 99)
+                        .addComponent(downloadButton)))
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addGroup(layout.createSequentialGroup()
+                        .addGap(71, 71, 71)
+                        .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                        .addGap(21, 21, 21)
+                        .addComponent(downloadButton)
+                        .addGap(18, 18, 18)
+                        .addComponent(statusField, javax.swing.GroupLayout.PREFERRED_SIZE, 52, javax.swing.GroupLayout.PREFERRED_SIZE))
+                    .addGroup(layout.createSequentialGroup()
+                        .addContainerGap()
+                        .addComponent(jLabel1)))
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void downloadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_downloadButtonActionPerformed
+        statusField.setText(org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.downloading"));
+        final int result = this.panel.doDownloadZip();
+        if (result == 1) {
+            statusField.setText(org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.downloadSuccess"));
+        } else {
+            statusField.setText(org.openide.util.NbBundle.getMessage(JaimesAscentDownloadPanelVisual.class, "JaimesAscentDownloadPanelVisual.downloadFailed"));
+        }
+    }//GEN-LAST:event_downloadButtonActionPerformed
+
+    private void statusFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_statusFieldActionPerformed
+        
+    }//GEN-LAST:event_statusFieldActionPerformed
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    javax.swing.JButton downloadButton;
+    javax.swing.JLabel jLabel1;
+    javax.swing.JScrollPane jScrollPane1;
+    javax.swing.JTextArea jTextArea1;
+    javax.swing.JTextField statusField;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public void addNotify() {
+        super.addNotify();
+    }
+
+    boolean valid(WizardDescriptor wizardDescriptor) {
+
+        return true;
+    }
+
+    void store(WizardDescriptor d) {
+        
+    }
+
+    void read(WizardDescriptor settings) {
+        
+    }
+
+    void validate(WizardDescriptor d) throws WizardValidationException {
+        // nothing to validate
+    }
+
+    // Implementation of DocumentListener --------------------------------------
+    @Override
+    public void changedUpdate(DocumentEvent e) {
+        
+    }
+
+    @Override
+    public void insertUpdate(DocumentEvent e) {
+        updateTexts(e);
+    }
+
+    @Override
+    public void removeUpdate(DocumentEvent e) {
+        updateTexts(e);
+    }
+
+    /** Handles changes in the Project name and project directory, */
+    private void updateTexts(DocumentEvent e) {
+
+        panel.fireChangeEvent(); // Notify that the panel changed
+    }
+}

+ 122 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentPanelVisual.form

@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.5" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="0"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="projectNameLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="projectLocationLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="createdFolderLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="projectNameTextField" alignment="1" pref="191" max="32767" attributes="0"/>
+                  <Component id="projectLocationTextField" alignment="1" pref="191" max="32767" attributes="0"/>
+                  <Component id="createdFolderTextField" alignment="1" pref="191" max="32767" attributes="0"/>
+              </Group>
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+              <Component id="browseButton" min="-2" max="-2" attributes="0"/>
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="projectNameLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="projectNameTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="projectLocationLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="projectLocationTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="browseButton" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="createdFolderLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="createdFolderTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace pref="213" max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="javax.swing.JLabel" name="projectNameLabel">
+      <Properties>
+        <Property name="labelFor" type="java.awt.Component" editor="org.netbeans.modules.form.ComponentChooserEditor">
+          <ComponentRef name="projectNameTextField"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentPanelVisual.projectNameLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+      </AuxValues>
+    </Component>
+    <Component class="javax.swing.JTextField" name="projectNameTextField">
+    </Component>
+    <Component class="javax.swing.JLabel" name="projectLocationLabel">
+      <Properties>
+        <Property name="labelFor" type="java.awt.Component" editor="org.netbeans.modules.form.ComponentChooserEditor">
+          <ComponentRef name="projectLocationTextField"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentPanelVisual.projectLocationLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+      </AuxValues>
+    </Component>
+    <Component class="javax.swing.JTextField" name="projectLocationTextField">
+    </Component>
+    <Component class="javax.swing.JButton" name="browseButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentPanelVisual.browseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="actionCommand" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentPanelVisual.browseButton.actionCommand" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="browseButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="createdFolderLabel">
+      <Properties>
+        <Property name="labelFor" type="java.awt.Component" editor="org.netbeans.modules.form.ComponentChooserEditor">
+          <ComponentRef name="createdFolderTextField"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="JaimesAscentPanelVisual.createdFolderLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JTextField" name="createdFolderTextField">
+      <Properties>
+        <Property name="editable" type="boolean" value="false"/>
+      </Properties>
+    </Component>
+  </SubComponents>
+</Form>

+ 291 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentPanelVisual.java

@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 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.templates.jaimesascent;
+
+import java.io.File;
+import javax.swing.JFileChooser;
+import javax.swing.JPanel;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.Document;
+import org.netbeans.spi.project.ui.support.ProjectChooser;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+import org.openide.filesystems.FileUtil;
+
+public class JaimesAscentPanelVisual extends JPanel implements DocumentListener {
+
+    public static final String PROP_PROJECT_NAME = "projectName";
+    static final String PROJECT_NAME = "JaimesAscent";
+    private final JaimesAscentWizardPanel panel;
+
+    public JaimesAscentPanelVisual(JaimesAscentWizardPanel panel) {
+        initComponents();
+        this.panel = panel;
+        // Register listener on the textFields to make the automatic updates
+        projectNameTextField.getDocument().addDocumentListener(this);
+        projectLocationTextField.getDocument().addDocumentListener(this);
+    }
+
+    public String getProjectName() {
+        return this.projectNameTextField.getText();
+    }
+
+    /** This method is called from within the constructor to
+     * initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is
+     * always regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        projectNameLabel = new javax.swing.JLabel();
+        projectNameTextField = new javax.swing.JTextField();
+        projectLocationLabel = new javax.swing.JLabel();
+        projectLocationTextField = new javax.swing.JTextField();
+        browseButton = new javax.swing.JButton();
+        createdFolderLabel = new javax.swing.JLabel();
+        createdFolderTextField = new javax.swing.JTextField();
+
+        projectNameLabel.setLabelFor(projectNameTextField);
+        org.openide.awt.Mnemonics.setLocalizedText(projectNameLabel, org.openide.util.NbBundle.getMessage(JaimesAscentPanelVisual.class, "JaimesAscentPanelVisual.projectNameLabel.text")); // NOI18N
+
+        projectLocationLabel.setLabelFor(projectLocationTextField);
+        org.openide.awt.Mnemonics.setLocalizedText(projectLocationLabel, org.openide.util.NbBundle.getMessage(JaimesAscentPanelVisual.class, "JaimesAscentPanelVisual.projectLocationLabel.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(JaimesAscentPanelVisual.class, "JaimesAscentPanelVisual.browseButton.text")); // NOI18N
+        browseButton.setActionCommand(org.openide.util.NbBundle.getMessage(JaimesAscentPanelVisual.class, "JaimesAscentPanelVisual.browseButton.actionCommand")); // NOI18N
+        browseButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                browseButtonActionPerformed(evt);
+            }
+        });
+
+        createdFolderLabel.setLabelFor(createdFolderTextField);
+        org.openide.awt.Mnemonics.setLocalizedText(createdFolderLabel, org.openide.util.NbBundle.getMessage(JaimesAscentPanelVisual.class, "JaimesAscentPanelVisual.createdFolderLabel.text")); // NOI18N
+
+        createdFolderTextField.setEditable(false);
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(projectNameLabel)
+                    .addComponent(projectLocationLabel)
+                    .addComponent(createdFolderLabel))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(projectNameTextField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 191, Short.MAX_VALUE)
+                    .addComponent(projectLocationTextField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 191, Short.MAX_VALUE)
+                    .addComponent(createdFolderTextField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 191, Short.MAX_VALUE))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(browseButton)
+                .addContainerGap())
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(projectNameLabel)
+                    .addComponent(projectNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(projectLocationLabel)
+                    .addComponent(projectLocationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(browseButton))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(createdFolderLabel)
+                    .addComponent(createdFolderTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+                .addContainerGap(213, Short.MAX_VALUE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed
+        String command = evt.getActionCommand();
+        if ("BROWSE".equals(command)) {
+            JFileChooser chooser = new JFileChooser();
+            chooser.setCurrentDirectory(null);
+            chooser.setDialogTitle("Select Project Location");
+            chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+            String path = this.projectLocationTextField.getText();
+            if (path.length() > 0) {
+                File f = new File(path);
+                if (f.exists()) {
+                    chooser.setSelectedFile(f);
+                }
+            }
+            if (JFileChooser.APPROVE_OPTION == chooser.showOpenDialog(this)) {
+                File projectDir = chooser.getSelectedFile();
+                projectLocationTextField.setText(FileUtil.normalizeFile(projectDir).getAbsolutePath());
+            }
+            panel.fireChangeEvent();
+        }
+
+    }//GEN-LAST:event_browseButtonActionPerformed
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    javax.swing.JButton browseButton;
+    javax.swing.JLabel createdFolderLabel;
+    javax.swing.JTextField createdFolderTextField;
+    javax.swing.JLabel projectLocationLabel;
+    javax.swing.JTextField projectLocationTextField;
+    javax.swing.JLabel projectNameLabel;
+    javax.swing.JTextField projectNameTextField;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public void addNotify() {
+        super.addNotify();
+        //same problem as in 31086, initial focus on Cancel button
+        projectNameTextField.requestFocus();
+    }
+
+    boolean valid(WizardDescriptor wizardDescriptor) {
+
+        if (projectNameTextField.getText().length() == 0) {
+            // TODO if using org.openide.dialogs >= 7.8, can use WizardDescriptor.PROP_ERROR_MESSAGE:
+            wizardDescriptor.putProperty("WizardPanel_errorMessage",
+                    "Project Name is not a valid folder name.");
+            return false; // Display name not specified
+        }
+        File f = FileUtil.normalizeFile(new File(projectLocationTextField.getText()).getAbsoluteFile());
+        if (!f.isDirectory()) {
+            String message = "Project Folder is not a valid path.";
+            wizardDescriptor.putProperty("WizardPanel_errorMessage", message);
+            return false;
+        }
+        final File destFolder = FileUtil.normalizeFile(new File(createdFolderTextField.getText()).getAbsoluteFile());
+
+        File projLoc = destFolder;
+        while (projLoc != null && !projLoc.exists()) {
+            projLoc = projLoc.getParentFile();
+        }
+        if (projLoc == null || !projLoc.canWrite()) {
+            wizardDescriptor.putProperty("WizardPanel_errorMessage",
+                    "Project Folder cannot be created.");
+            return false;
+        }
+
+        if (FileUtil.toFileObject(projLoc) == null) {
+            String message = "Project Folder is not a valid path.";
+            wizardDescriptor.putProperty("WizardPanel_errorMessage", message);
+            return false;
+        }
+
+        File[] kids = destFolder.listFiles();
+        if (destFolder.exists() && kids != null && kids.length > 0) {
+            // Folder exists and is not empty
+            wizardDescriptor.putProperty("WizardPanel_errorMessage",
+                    "Project Folder already exists and is not empty.");
+            return false;
+        }
+        wizardDescriptor.putProperty("WizardPanel_errorMessage", "");
+        return true;
+    }
+
+    void store(WizardDescriptor d) {
+        String name = projectNameTextField.getText().trim();
+        String folder = createdFolderTextField.getText().trim();
+
+        d.putProperty("projdir", new File(folder));
+        d.putProperty("name", name);
+    }
+
+    void read(WizardDescriptor settings) {
+        File projectLocation = (File) settings.getProperty("projdir");
+        if (projectLocation == null || projectLocation.getParentFile() == null || !projectLocation.getParentFile().isDirectory()) {
+            projectLocation = ProjectChooser.getProjectsFolder();
+        } else {
+            projectLocation = projectLocation.getParentFile();
+        }
+        this.projectLocationTextField.setText(projectLocation.getAbsolutePath());
+
+        String projectName = (String) settings.getProperty("name");
+        if (projectName == null) {
+            projectName = PROJECT_NAME;
+        }
+        this.projectNameTextField.setText(projectName);
+        this.projectNameTextField.selectAll();
+    }
+
+    void validate(WizardDescriptor d) throws WizardValidationException {
+        // nothing to validate
+    }
+
+    // Implementation of DocumentListener --------------------------------------
+    @Override
+    public void changedUpdate(DocumentEvent e) {
+        updateTexts(e);
+        if (this.projectNameTextField.getDocument() == e.getDocument()) {
+            firePropertyChange(PROP_PROJECT_NAME, null, this.projectNameTextField.getText());
+        }
+    }
+
+    @Override
+    public void insertUpdate(DocumentEvent e) {
+        updateTexts(e);
+        if (this.projectNameTextField.getDocument() == e.getDocument()) {
+            firePropertyChange(PROP_PROJECT_NAME, null, this.projectNameTextField.getText());
+        }
+    }
+
+    @Override
+    public void removeUpdate(DocumentEvent e) {
+        updateTexts(e);
+        if (this.projectNameTextField.getDocument() == e.getDocument()) {
+            firePropertyChange(PROP_PROJECT_NAME, null, this.projectNameTextField.getText());
+        }
+    }
+
+    /** Handles changes in the Project name and project directory, */
+    private void updateTexts(DocumentEvent e) {
+
+        Document doc = e.getDocument();
+
+        if (doc == projectNameTextField.getDocument() || doc == projectLocationTextField.getDocument()) {
+            // Change in the project name
+
+            String projectName = projectNameTextField.getText();
+            String projectFolder = projectLocationTextField.getText();
+
+            //if (projectFolder.trim().length() == 0 || projectFolder.equals(oldName)) {
+            createdFolderTextField.setText(projectFolder + File.separatorChar + projectName);
+            //}
+
+        }
+        panel.fireChangeEvent(); // Notify that the panel changed
+    }
+}

+ 221 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentWizardIterator.java

@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 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.templates.jaimesascent;
+
+import com.jme3.gde.templates.gradledesktop.options.CachedOptionsContainer;
+import java.awt.Component;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import javax.swing.JComponent;
+import javax.swing.event.ChangeListener;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.spi.project.ui.support.ProjectChooser;
+import org.openide.WizardDescriptor;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.util.NbBundle;
+
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class JaimesAscentWizardIterator implements WizardDescriptor.InstantiatingIterator {
+
+    private int index;
+    private WizardDescriptor.Panel[] panels;
+    private WizardDescriptor wiz;
+
+    final static String master = "JaimesAscent-1.1.1/";
+    
+    public JaimesAscentWizardIterator() {
+
+        // Initiate the options getting...
+        CachedOptionsContainer.getInstance();
+    }
+
+    public static JaimesAscentWizardIterator createIterator() {
+        return new JaimesAscentWizardIterator();
+    }
+
+    private WizardDescriptor.Panel[] createPanels() {
+        return new WizardDescriptor.Panel[]{
+                    new JaimesAscentDownloadPanel(),
+                    new JaimesAscentWizardPanel()
+        };
+    }
+
+    private String[] createSteps() {
+        return new String[]{
+                    NbBundle.getMessage(JaimesAscentWizardIterator.class, "LBL_DownloadProjectStep"),
+                    NbBundle.getMessage(JaimesAscentWizardIterator.class, "LBL_CreateProjectStep"),
+                };
+    }
+
+    @Override
+    public Set/*<FileObject>*/ instantiate(/*ProgressHandle handle*/) throws IOException {
+        Set<FileObject> resultSet = new LinkedHashSet<>();
+        File dirF = FileUtil.normalizeFile((File) wiz.getProperty("projdir"));
+        dirF.mkdirs();
+
+        FileObject template = FileUtil.toFileObject(new File(
+                JaimesAscentDownloadPanel.DOWNLOAD_FOLDER, 
+                JaimesAscentDownloadPanel.ZIP_NAME));
+        
+        FileObject dir = FileUtil.toFileObject(dirF);
+        unZipFile(template.getInputStream(), dir);
+        
+        // Always open top dir as a project:
+        resultSet.add(dir);
+        // Look for nested projects to open as well:
+        Enumeration<? extends FileObject> e = dir.getFolders(true);
+        while (e.hasMoreElements()) {
+            FileObject subfolder = e.nextElement();
+            if (ProjectManager.getDefault().isProject(subfolder)) {
+                resultSet.add(subfolder);
+            }
+        }
+
+        File parent = dirF.getParentFile();
+        if (parent != null && parent.exists()) {
+            ProjectChooser.setProjectsFolder(parent);
+        }
+
+        return resultSet;
+    }
+
+    @Override
+    public void initialize(WizardDescriptor wiz) {
+        this.wiz = wiz;
+        index = 0;
+        panels = createPanels();
+        // Make sure list of steps is accurate.
+        String[] steps = createSteps();
+        for (int i = 0; i < panels.length; i++) {
+            Component c = panels[i].getComponent();
+            if (steps[i] == null) {
+                // Default step name to component name of panel.
+                // Mainly useful for getting the name of the target
+                // chooser to appear in the list of steps.
+                steps[i] = c.getName();
+            }
+            if (c instanceof JComponent jc) {                 // Step #.
+                // TODO if using org.openide.dialogs >= 7.8, can use WizardDescriptor.PROP_*:
+                jc.putClientProperty("WizardPanel_contentSelectedIndex", i);
+                // Step name (actually the whole list for reference).
+                jc.putClientProperty("WizardPanel_contentData", steps);
+            }
+        }
+    }
+
+    @Override
+    public void uninitialize(WizardDescriptor wiz) {
+        this.wiz.putProperty("projdir", null);
+        this.wiz.putProperty("name", null);
+        this.wiz = null;
+        panels = null;
+    }
+
+    @Override
+    public String name() {
+        return MessageFormat.format("{0} of {1}",
+                new Object[]{index + 1, panels.length});
+    }
+
+    @Override
+    public boolean hasNext() {
+        return index < panels.length - 1;
+    }
+
+    @Override
+    public boolean hasPrevious() {
+        return index > 0;
+    }
+
+    @Override
+    public void nextPanel() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+        index++;
+    }
+
+    @Override
+    public void previousPanel() {
+        if (!hasPrevious()) {
+            throw new NoSuchElementException();
+        }
+        index--;
+    }
+
+    @Override
+    public WizardDescriptor.Panel current() {
+        return panels[index];
+    }
+
+    // If nothing unusual changes in the middle of the wizard, simply:
+    @Override
+    public final void addChangeListener(ChangeListener l) {
+    }
+
+    @Override
+    public final void removeChangeListener(ChangeListener l) {
+    }
+
+    private static void unZipFile(InputStream source, FileObject projectRoot) throws IOException {
+        try (source) {
+            ZipInputStream str = new ZipInputStream(source);
+            ZipEntry entry;
+            
+            while ((entry = str.getNextEntry()) != null) {
+                if (entry.getName().endsWith(master)) {
+                    continue;
+                }
+                final String entryName = entry.getName().replace(master, "");
+                if (entry.isDirectory()) {
+                    FileUtil.createFolder(projectRoot, entryName);
+                } else {
+                    FileObject fo = FileUtil.createData(projectRoot, entryName);
+                    try (OutputStream out = fo.getOutputStream()) {
+                        FileUtil.copy(str, out);
+                    }
+                }
+            }
+        }
+    }
+
+}

+ 126 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/JaimesAscentWizardPanel.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 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.templates.jaimesascent;
+
+import java.awt.Component;
+import java.util.HashSet;
+import java.util.Set;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+import org.openide.util.HelpCtx;
+import org.openide.util.NbBundle;
+
+/**
+ * Panel just asking for basic info.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class JaimesAscentWizardPanel implements WizardDescriptor.Panel,
+        WizardDescriptor.ValidatingPanel, WizardDescriptor.FinishablePanel {
+
+    private WizardDescriptor wizardDescriptor;
+    private JaimesAscentPanelVisual component;
+
+    public JaimesAscentWizardPanel() {
+    }
+
+    @Override
+    public Component getComponent() {
+        if (component == null) {
+            component = new JaimesAscentPanelVisual(this);
+            component.setName(NbBundle.getMessage(JaimesAscentWizardPanel.class, "LBL_CreateProjectStep"));
+        }
+        return component;
+    }
+
+    @Override
+    public HelpCtx getHelp() {
+        return new HelpCtx("sdk.project_creation");
+    }
+
+    @Override
+    public boolean isValid() {
+        getComponent();
+        return component.valid(wizardDescriptor);
+    }
+    
+    private final Set<ChangeListener> listeners = new HashSet<>(1); // or can use ChangeSupport in NB 6.0
+
+    @Override
+    public final void addChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.add(l);
+        }
+    }
+
+    @Override
+    public final void removeChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.remove(l);
+        }
+    }
+
+    protected final void fireChangeEvent() {
+        Set<ChangeListener> ls;
+        synchronized (listeners) {
+            ls = new HashSet<>(listeners);
+        }
+        ChangeEvent ev = new ChangeEvent(this);
+        for (ChangeListener l : ls) {
+            l.stateChanged(ev);
+        }
+    }
+
+    @Override
+    public void readSettings(Object settings) {
+        wizardDescriptor = (WizardDescriptor) settings;
+        component.read(wizardDescriptor);
+    }
+
+    @Override
+    public void storeSettings(Object settings) {
+        WizardDescriptor d = (WizardDescriptor) settings;
+        component.store(d);
+    }
+
+    @Override
+    public boolean isFinishPanel() {
+        return false;
+    }
+
+    @Override
+    public void validate() throws WizardValidationException {
+        getComponent();
+        component.validate(wizardDescriptor);
+    }
+}

+ 65 - 0
jme3-templates/src/com/jme3/gde/templates/jaimesascent/build.gradle.ftl

@@ -0,0 +1,65 @@
+plugins {
+    id 'java'
+    id 'application'
+}
+
+group 'com.JaimesAscent'
+version '1.0'
+
+mainClassName = "com.JaimesAscent.JaimesAscent"
+
+repositories {
+    mavenCentral()
+}
+
+project.ext {
+  jmeVer = '3.7.0-stable'
+}
+
+project(":assets") {
+    apply plugin: "java"
+
+    buildDir = rootProject.file("build/assets")
+
+    sourceSets {
+        main {
+            resources {
+                srcDir '.'
+            }
+        }
+    }
+
+    java {
+        toolchain {
+            languageVersion = JavaLanguageVersion.of(21)
+        }
+    }
+}
+
+java {
+    toolchain {
+        languageVersion = JavaLanguageVersion.of(21)
+    }
+}
+
+dependencies {
+
+  implementation "org.jmonkeyengine:jme3-core:$jmeVer"
+  implementation "org.jmonkeyengine:jme3-desktop:$jmeVer"
+  implementation "org.jmonkeyengine:jme3-lwjgl:$jmeVer"
+  implementation "org.jmonkeyengine:jme3-lwjgl:$jmeVer"
+  implementation "com.github.stephengold:Heart:9.0.0"
+  implementation "com.github.stephengold:Minie:8.0.0"
+  implementation project("assets")
+
+}
+
+jar {
+    manifest {
+        attributes 'Main-Class': "$mainClassName"
+    }
+}
+
+wrapper {
+    gradleVersion = '8.6'
+}

BIN
jme3-templates/src/com/jme3/gde/templates/jaimesascent/jaimesascent.png


+ 26 - 1
jme3-templates/src/com/jme3/gde/templates/layer.xml

@@ -22,14 +22,39 @@
                     <attr name="instantiatingWizardURL" urlvalue="nbresloc:/com/jme3/gde/templates/gradledesktop/GradleDesktopGameDescription.html"/>
                     <attr name="template" boolvalue="true"/>
                 </file>
+
+            </folder>
+        </folder>
+        
+        <folder name="Project">
+            <folder name="JME3 - Sample projects">
+                <attr name="position" intvalue="20"/>
+                
+                <file name="Monkey Zone" url="RollingTheMonkeyProject.zip">
+                    <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/com/jme3/gde/templates/jme-logo.png"/>
+                    <attr name="displayName" bundlevalue="com.jme3.gde.templates.Bundle#Templates/Project/JME3/Examples/MonkeyZone"/>
+                    <attr name="instantiatingIterator" methodvalue="com.jme3.gde.templates.monkeyzone.MonkeyZoneWizardIterator.createIterator"/>
+                    <attr name="instantiatingWizardURL" urlvalue="nbresloc:/com/jme3/gde/templates/monkeyzone/MonkeyZoneDescription.html"/>
+                    <attr name="template" boolvalue="true"/>
+                </file>
                 
+                <file name="Jaimes Ascent" url="JaimesAscent.zip">
+                    <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/com/jme3/gde/templates/jme-logo.png"/>
+                    <attr name="displayName" bundlevalue="com.jme3.gde.templates.Bundle#Templates/Project/JME3/Examples/JaimesAscent"/>
+                    <attr name="instantiatingIterator" methodvalue="com.jme3.gde.templates.jaimesascent.JaimesAscentWizardIterator.createIterator"/>
+                    <attr name="instantiatingWizardURL" urlvalue="nbresloc:/com/jme3/gde/templates/jaimesascent/JaimesAscentDescription.html"/>
+                    <attr name="template" boolvalue="true"/>
+                </file>
+
                 <file name="Rolling the Monkey" url="RollingTheMonkeyProject.zip">
                     <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/com/jme3/gde/templates/jme-logo.png"/>
-                    <attr name="displayName" bundlevalue="com.jme3.gde.templates.Bundle#Templates/Project/JME3/RollingTheMonkeyProject.zip"/>
+                    <attr name="displayName" bundlevalue="com.jme3.gde.templates.Bundle#Templates/Project/JME3/Examples/RollingTheMonkeyProject.zip"/>
                     <attr name="instantiatingIterator" methodvalue="com.jme3.gde.templates.rollingthemonkey.RollingTheMonkeyWizardIterator.createIterator"/>
                     <attr name="instantiatingWizardURL" urlvalue="nbresloc:/com/jme3/gde/templates/rollingthemonkey/RollingTheMonkeyDescription.html"/>
                     <attr name="template" boolvalue="true"/>
                 </file>
+                
+                
 
             </folder>
         </folder>

+ 16 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/Bundle.properties

@@ -0,0 +1,16 @@
+LBL_DownloadProjectStep=Download project
+LBL_CreateProjectStep=Name and Location
+MonkeyZonePanelVisual.browseButton.text=Br&owse...
+MonkeyZoneDownloadPanelVisual.downloading=Downloading... Please wait
+MonkeyZoneDownloadPanelVisual.downloadSuccess=Download complete. Press 'next' to proceed.
+MonkeyZoneDownloadPanelVisual.downloadFailed=Download failed.
+MonkeyZoneDownloadPanelVisual.downloadButton.actionCommand=DOWNLOAD
+MonkeyZoneDownloadPanelVisual.downloadButton.text=Download project
+MonkeyZoneDownloadPanelVisual.jLabel1.text=
+MonkeyZoneDownloadPanelVisual.statusField.text=
+MonkeyZoneDownloadPanelVisual.jTextArea1.text=Pressing the button below will download the project from Github, to a temporary location from which it will be installed in the next step\n
+MonkeyZonePanelVisual.projectLocationLabel.text=Project &Location:
+MonkeyZonePanelVisual.projectNameLabel.text=Project &Name:
+MonkeyZonePanelVisual.createdFolderLabel.text=Project &Folder:
+MonkeyZonePanelVisual.browseButton.actionCommand=BROWSE
+MonkeyZonePanelVisual.browseButton.text=Browse

+ 13 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDescription.html

@@ -0,0 +1,13 @@
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+        <title>Monkey Zone</title>
+    </head>
+    <body>
+        A networked multiplayer game by the jMonkeyEngine team.<br><br>
+        
+        Inspired by the classic game BattleZone, this is a networked multiplayer<br>
+        game with both on foot action and enterable vehicles.<br>
+        This is not a beginner project.
+    </body>
+</html>

+ 189 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDownloadPanel.java

@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 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.templates.monkeyzone;
+
+import java.awt.Component;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.Set;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+import org.openide.util.HelpCtx;
+import org.openide.util.NbBundle;
+
+/**
+ * Panel just asking for basic info.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class MonkeyZoneDownloadPanel implements WizardDescriptor.Panel,
+        WizardDescriptor.ValidatingPanel, WizardDescriptor.FinishablePanel {
+
+    private WizardDescriptor wizardDescriptor;
+    private MonkeyZoneDownloadPanelVisual component;
+    
+    static String ZIP_NAME = "MonkeyZone.zip";
+    static String DOWNLOAD_FOLDER = System.getProperty("java.io.tmpdir");
+
+    public MonkeyZoneDownloadPanel() {
+    }
+    
+    public int doDownloadZip() {
+        return downloadFile("https://github.com/jMonkeyEngine/monkeyzone/archive/refs/heads/master.zip", DOWNLOAD_FOLDER, ZIP_NAME);
+    }
+    
+    private int downloadFile(String fileURL, String saveDir, String fileName) {
+        HttpURLConnection httpConn = null;
+        BufferedInputStream inputStream = null;
+        FileOutputStream fileOutputStream = null;
+        
+        final File outputFile = new File(saveDir, fileName);
+        
+        if (outputFile.exists()) {
+            return 1;
+        }
+
+        try {
+            // Create URL object
+            URL url = new URL(fileURL);
+            httpConn = (HttpURLConnection) url.openConnection();
+
+            // Check HTTP response code
+            int responseCode = httpConn.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) {
+                // Open input stream from the HTTP connection
+                inputStream = new BufferedInputStream(httpConn.getInputStream());
+                
+                
+
+                // Create output stream to save the file
+                fileOutputStream = new FileOutputStream(outputFile);
+
+                byte[] buffer = new byte[4096];
+                int bytesRead;
+                while ((bytesRead = inputStream.read(buffer)) != -1) {
+                    fileOutputStream.write(buffer, 0, bytesRead);
+                }
+
+                return 1;
+            }
+        } catch (IOException e) {
+            return 0;
+        } finally {
+            // Close resources
+            try {
+                if (inputStream != null) inputStream.close();
+                if (fileOutputStream != null) fileOutputStream.close();
+                if (httpConn != null) httpConn.disconnect();
+            } catch (IOException ex) {
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public Component getComponent() {
+        if (component == null) {
+            component = new MonkeyZoneDownloadPanelVisual(this);
+            component.setName(NbBundle.getMessage(MonkeyZoneDownloadPanel.class, "LBL_DownloadProjectStep"));
+        }
+        return component;
+    }
+
+    @Override
+    public HelpCtx getHelp() {
+        return new HelpCtx("sdk.download_project");
+    }
+
+    @Override
+    public boolean isValid() {
+        getComponent();
+        return component.valid(wizardDescriptor);
+    }
+    
+    private final Set<ChangeListener> listeners = new HashSet<>(1); // or can use ChangeSupport in NB 6.0
+
+    @Override
+    public final void addChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.add(l);
+        }
+    }
+
+    @Override
+    public final void removeChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.remove(l);
+        }
+    }
+
+    protected final void fireChangeEvent() {
+        Set<ChangeListener> ls;
+        synchronized (listeners) {
+            ls = new HashSet<>(listeners);
+        }
+        ChangeEvent ev = new ChangeEvent(this);
+        for (ChangeListener l : ls) {
+            l.stateChanged(ev);
+        }
+    }
+
+    @Override
+    public void readSettings(Object settings) {
+        wizardDescriptor = (WizardDescriptor) settings;
+        component.read(wizardDescriptor);
+    }
+
+    @Override
+    public void storeSettings(Object settings) {
+        WizardDescriptor d = (WizardDescriptor) settings;
+        component.store(d);
+    }
+
+    @Override
+    public boolean isFinishPanel() {
+        return false;
+    }
+
+    @Override
+    public void validate() throws WizardValidationException {
+        getComponent();
+        component.validate(wizardDescriptor);
+    }
+}

+ 120 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDownloadPanelVisual.form

@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.5" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="0"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" alignment="0" attributes="0">
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Group type="102" attributes="0">
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Group type="103" groupAlignment="0" attributes="0">
+                          <Component id="jScrollPane1" alignment="0" min="-2" pref="307" max="-2" attributes="0"/>
+                          <Component id="statusField" alignment="1" min="-2" pref="313" max="-2" attributes="0"/>
+                      </Group>
+                  </Group>
+                  <Group type="102" alignment="0" attributes="0">
+                      <EmptySpace min="-2" pref="99" max="-2" attributes="0"/>
+                      <Component id="downloadButton" min="-2" max="-2" attributes="0"/>
+                  </Group>
+              </Group>
+              <EmptySpace pref="14" max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Group type="102" alignment="0" attributes="0">
+                      <EmptySpace min="-2" pref="71" max="-2" attributes="0"/>
+                      <Component id="jScrollPane1" min="-2" max="-2" attributes="0"/>
+                      <EmptySpace min="-2" pref="21" max="-2" attributes="0"/>
+                      <Component id="downloadButton" min="-2" max="-2" attributes="0"/>
+                      <EmptySpace type="separate" max="-2" attributes="0"/>
+                      <Component id="statusField" min="-2" pref="52" max="-2" attributes="0"/>
+                  </Group>
+                  <Group type="102" alignment="0" attributes="0">
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
+                  </Group>
+              </Group>
+              <EmptySpace max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="javax.swing.JButton" name="downloadButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZoneDownloadPanelVisual.downloadButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="actionCommand" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZoneDownloadPanelVisual.downloadButton.actionCommand" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="downloadButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Container class="javax.swing.JScrollPane" name="jScrollPane1">
+      <AuxValues>
+        <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
+      </AuxValues>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
+      <SubComponents>
+        <Component class="javax.swing.JTextArea" name="jTextArea1">
+          <Properties>
+            <Property name="editable" type="boolean" value="false"/>
+            <Property name="columns" type="int" value="20"/>
+            <Property name="lineWrap" type="boolean" value="true"/>
+            <Property name="rows" type="int" value="5"/>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZoneDownloadPanelVisual.jTextArea1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+            <Property name="wrapStyleWord" type="boolean" value="true"/>
+            <Property name="enabled" type="boolean" value="false"/>
+          </Properties>
+        </Component>
+      </SubComponents>
+    </Container>
+    <Component class="javax.swing.JTextField" name="statusField">
+      <Properties>
+        <Property name="editable" type="boolean" value="false"/>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZoneDownloadPanelVisual.statusField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="enabled" type="boolean" value="false"/>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="statusFieldActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="jLabel1">
+      <Properties>
+        <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
+          <Image iconType="3" name="/com/jme3/gde/templates/monkeyzone/monkeyzone.png"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZoneDownloadPanelVisual.jLabel1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+  </SubComponents>
+</Form>

+ 194 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneDownloadPanelVisual.java

@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 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.templates.monkeyzone;
+
+import javax.swing.JPanel;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+
+public class MonkeyZoneDownloadPanelVisual extends JPanel implements DocumentListener {
+
+    public static final String PROP_PROJECT_NAME = "projectName";
+    private final MonkeyZoneDownloadPanel panel;
+
+    public MonkeyZoneDownloadPanelVisual(MonkeyZoneDownloadPanel panel) {
+        initComponents();
+        this.panel = panel;
+    }
+
+    /** This method is called from within the constructor to
+     * initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is
+     * always regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        downloadButton = new javax.swing.JButton();
+        jScrollPane1 = new javax.swing.JScrollPane();
+        jTextArea1 = new javax.swing.JTextArea();
+        statusField = new javax.swing.JTextField();
+        jLabel1 = new javax.swing.JLabel();
+
+        org.openide.awt.Mnemonics.setLocalizedText(downloadButton, org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.downloadButton.text")); // NOI18N
+        downloadButton.setActionCommand(org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.downloadButton.actionCommand")); // NOI18N
+        downloadButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                downloadButtonActionPerformed(evt);
+            }
+        });
+
+        jTextArea1.setEditable(false);
+        jTextArea1.setColumns(20);
+        jTextArea1.setLineWrap(true);
+        jTextArea1.setRows(5);
+        jTextArea1.setText(org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.jTextArea1.text")); // NOI18N
+        jTextArea1.setWrapStyleWord(true);
+        jTextArea1.setEnabled(false);
+        jScrollPane1.setViewportView(jTextArea1);
+
+        statusField.setEditable(false);
+        statusField.setText(org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.statusField.text")); // NOI18N
+        statusField.setEnabled(false);
+        statusField.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                statusFieldActionPerformed(evt);
+            }
+        });
+
+        jLabel1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/com/jme3/gde/templates/monkeyzone/monkeyzone.png"))); // NOI18N
+        org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.jLabel1.text")); // NOI18N
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addComponent(jLabel1)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addGroup(layout.createSequentialGroup()
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                            .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 307, javax.swing.GroupLayout.PREFERRED_SIZE)
+                            .addComponent(statusField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 313, javax.swing.GroupLayout.PREFERRED_SIZE)))
+                    .addGroup(layout.createSequentialGroup()
+                        .addGap(99, 99, 99)
+                        .addComponent(downloadButton)))
+                .addContainerGap(14, Short.MAX_VALUE))
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addGroup(layout.createSequentialGroup()
+                        .addGap(71, 71, 71)
+                        .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                        .addGap(21, 21, 21)
+                        .addComponent(downloadButton)
+                        .addGap(18, 18, 18)
+                        .addComponent(statusField, javax.swing.GroupLayout.PREFERRED_SIZE, 52, javax.swing.GroupLayout.PREFERRED_SIZE))
+                    .addGroup(layout.createSequentialGroup()
+                        .addContainerGap()
+                        .addComponent(jLabel1)))
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void downloadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_downloadButtonActionPerformed
+        statusField.setText(org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.downloading"));
+        final int result = this.panel.doDownloadZip();
+        if (result == 1) {
+            statusField.setText(org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.downloadSuccess"));
+        } else {
+            statusField.setText(org.openide.util.NbBundle.getMessage(MonkeyZoneDownloadPanelVisual.class, "MonkeyZoneDownloadPanelVisual.downloadFailed"));
+        }
+    }//GEN-LAST:event_downloadButtonActionPerformed
+
+    private void statusFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_statusFieldActionPerformed
+        
+    }//GEN-LAST:event_statusFieldActionPerformed
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    javax.swing.JButton downloadButton;
+    javax.swing.JLabel jLabel1;
+    javax.swing.JScrollPane jScrollPane1;
+    javax.swing.JTextArea jTextArea1;
+    javax.swing.JTextField statusField;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public void addNotify() {
+        super.addNotify();
+    }
+
+    boolean valid(WizardDescriptor wizardDescriptor) {
+
+        return true;
+    }
+
+    void store(WizardDescriptor d) {
+        
+    }
+
+    void read(WizardDescriptor settings) {
+        
+    }
+
+    void validate(WizardDescriptor d) throws WizardValidationException {
+        // nothing to validate
+    }
+
+    // Implementation of DocumentListener --------------------------------------
+    @Override
+    public void changedUpdate(DocumentEvent e) {
+        
+    }
+
+    @Override
+    public void insertUpdate(DocumentEvent e) {
+        updateTexts(e);
+    }
+
+    @Override
+    public void removeUpdate(DocumentEvent e) {
+        updateTexts(e);
+    }
+
+    /** Handles changes in the Project name and project directory, */
+    private void updateTexts(DocumentEvent e) {
+
+        panel.fireChangeEvent(); // Notify that the panel changed
+    }
+}

+ 122 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZonePanelVisual.form

@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.5" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="0"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="projectNameLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="projectLocationLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="createdFolderLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="projectNameTextField" alignment="1" pref="191" max="32767" attributes="0"/>
+                  <Component id="projectLocationTextField" alignment="1" pref="191" max="32767" attributes="0"/>
+                  <Component id="createdFolderTextField" alignment="1" pref="191" max="32767" attributes="0"/>
+              </Group>
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+              <Component id="browseButton" min="-2" max="-2" attributes="0"/>
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="projectNameLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="projectNameTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="projectLocationLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="projectLocationTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="browseButton" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="createdFolderLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="createdFolderTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace pref="213" max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="javax.swing.JLabel" name="projectNameLabel">
+      <Properties>
+        <Property name="labelFor" type="java.awt.Component" editor="org.netbeans.modules.form.ComponentChooserEditor">
+          <ComponentRef name="projectNameTextField"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZonePanelVisual.projectNameLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+      </AuxValues>
+    </Component>
+    <Component class="javax.swing.JTextField" name="projectNameTextField">
+    </Component>
+    <Component class="javax.swing.JLabel" name="projectLocationLabel">
+      <Properties>
+        <Property name="labelFor" type="java.awt.Component" editor="org.netbeans.modules.form.ComponentChooserEditor">
+          <ComponentRef name="projectLocationTextField"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZonePanelVisual.projectLocationLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+      </AuxValues>
+    </Component>
+    <Component class="javax.swing.JTextField" name="projectLocationTextField">
+    </Component>
+    <Component class="javax.swing.JButton" name="browseButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZonePanelVisual.browseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="actionCommand" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZonePanelVisual.browseButton.actionCommand" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="browseButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="createdFolderLabel">
+      <Properties>
+        <Property name="labelFor" type="java.awt.Component" editor="org.netbeans.modules.form.ComponentChooserEditor">
+          <ComponentRef name="createdFolderTextField"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/jme3/gde/templates/monkeyzone/Bundle.properties" key="MonkeyZonePanelVisual.createdFolderLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JTextField" name="createdFolderTextField">
+      <Properties>
+        <Property name="editable" type="boolean" value="false"/>
+      </Properties>
+    </Component>
+  </SubComponents>
+</Form>

+ 291 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZonePanelVisual.java

@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 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.templates.monkeyzone;
+
+import java.io.File;
+import javax.swing.JFileChooser;
+import javax.swing.JPanel;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.Document;
+import org.netbeans.spi.project.ui.support.ProjectChooser;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+import org.openide.filesystems.FileUtil;
+
+public class MonkeyZonePanelVisual extends JPanel implements DocumentListener {
+
+    public static final String PROP_PROJECT_NAME = "projectName";
+    static final String PROJECT_NAME = "MonkeyZone";
+    private final MonkeyZoneWizardPanel panel;
+
+    public MonkeyZonePanelVisual(MonkeyZoneWizardPanel panel) {
+        initComponents();
+        this.panel = panel;
+        // Register listener on the textFields to make the automatic updates
+        projectNameTextField.getDocument().addDocumentListener(this);
+        projectLocationTextField.getDocument().addDocumentListener(this);
+    }
+
+    public String getProjectName() {
+        return this.projectNameTextField.getText();
+    }
+
+    /** This method is called from within the constructor to
+     * initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is
+     * always regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        projectNameLabel = new javax.swing.JLabel();
+        projectNameTextField = new javax.swing.JTextField();
+        projectLocationLabel = new javax.swing.JLabel();
+        projectLocationTextField = new javax.swing.JTextField();
+        browseButton = new javax.swing.JButton();
+        createdFolderLabel = new javax.swing.JLabel();
+        createdFolderTextField = new javax.swing.JTextField();
+
+        projectNameLabel.setLabelFor(projectNameTextField);
+        org.openide.awt.Mnemonics.setLocalizedText(projectNameLabel, org.openide.util.NbBundle.getMessage(MonkeyZonePanelVisual.class, "MonkeyZonePanelVisual.projectNameLabel.text")); // NOI18N
+
+        projectLocationLabel.setLabelFor(projectLocationTextField);
+        org.openide.awt.Mnemonics.setLocalizedText(projectLocationLabel, org.openide.util.NbBundle.getMessage(MonkeyZonePanelVisual.class, "MonkeyZonePanelVisual.projectLocationLabel.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(MonkeyZonePanelVisual.class, "MonkeyZonePanelVisual.browseButton.text")); // NOI18N
+        browseButton.setActionCommand(org.openide.util.NbBundle.getMessage(MonkeyZonePanelVisual.class, "MonkeyZonePanelVisual.browseButton.actionCommand")); // NOI18N
+        browseButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                browseButtonActionPerformed(evt);
+            }
+        });
+
+        createdFolderLabel.setLabelFor(createdFolderTextField);
+        org.openide.awt.Mnemonics.setLocalizedText(createdFolderLabel, org.openide.util.NbBundle.getMessage(MonkeyZonePanelVisual.class, "MonkeyZonePanelVisual.createdFolderLabel.text")); // NOI18N
+
+        createdFolderTextField.setEditable(false);
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(projectNameLabel)
+                    .addComponent(projectLocationLabel)
+                    .addComponent(createdFolderLabel))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(projectNameTextField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 191, Short.MAX_VALUE)
+                    .addComponent(projectLocationTextField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 191, Short.MAX_VALUE)
+                    .addComponent(createdFolderTextField, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 191, Short.MAX_VALUE))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(browseButton)
+                .addContainerGap())
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(projectNameLabel)
+                    .addComponent(projectNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(projectLocationLabel)
+                    .addComponent(projectLocationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(browseButton))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(createdFolderLabel)
+                    .addComponent(createdFolderTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+                .addContainerGap(213, Short.MAX_VALUE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed
+        String command = evt.getActionCommand();
+        if ("BROWSE".equals(command)) {
+            JFileChooser chooser = new JFileChooser();
+            chooser.setCurrentDirectory(null);
+            chooser.setDialogTitle("Select Project Location");
+            chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+            String path = this.projectLocationTextField.getText();
+            if (path.length() > 0) {
+                File f = new File(path);
+                if (f.exists()) {
+                    chooser.setSelectedFile(f);
+                }
+            }
+            if (JFileChooser.APPROVE_OPTION == chooser.showOpenDialog(this)) {
+                File projectDir = chooser.getSelectedFile();
+                projectLocationTextField.setText(FileUtil.normalizeFile(projectDir).getAbsolutePath());
+            }
+            panel.fireChangeEvent();
+        }
+
+    }//GEN-LAST:event_browseButtonActionPerformed
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    javax.swing.JButton browseButton;
+    javax.swing.JLabel createdFolderLabel;
+    javax.swing.JTextField createdFolderTextField;
+    javax.swing.JLabel projectLocationLabel;
+    javax.swing.JTextField projectLocationTextField;
+    javax.swing.JLabel projectNameLabel;
+    javax.swing.JTextField projectNameTextField;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public void addNotify() {
+        super.addNotify();
+        //same problem as in 31086, initial focus on Cancel button
+        projectNameTextField.requestFocus();
+    }
+
+    boolean valid(WizardDescriptor wizardDescriptor) {
+
+        if (projectNameTextField.getText().length() == 0) {
+            // TODO if using org.openide.dialogs >= 7.8, can use WizardDescriptor.PROP_ERROR_MESSAGE:
+            wizardDescriptor.putProperty("WizardPanel_errorMessage",
+                    "Project Name is not a valid folder name.");
+            return false; // Display name not specified
+        }
+        File f = FileUtil.normalizeFile(new File(projectLocationTextField.getText()).getAbsoluteFile());
+        if (!f.isDirectory()) {
+            String message = "Project Folder is not a valid path.";
+            wizardDescriptor.putProperty("WizardPanel_errorMessage", message);
+            return false;
+        }
+        final File destFolder = FileUtil.normalizeFile(new File(createdFolderTextField.getText()).getAbsoluteFile());
+
+        File projLoc = destFolder;
+        while (projLoc != null && !projLoc.exists()) {
+            projLoc = projLoc.getParentFile();
+        }
+        if (projLoc == null || !projLoc.canWrite()) {
+            wizardDescriptor.putProperty("WizardPanel_errorMessage",
+                    "Project Folder cannot be created.");
+            return false;
+        }
+
+        if (FileUtil.toFileObject(projLoc) == null) {
+            String message = "Project Folder is not a valid path.";
+            wizardDescriptor.putProperty("WizardPanel_errorMessage", message);
+            return false;
+        }
+
+        File[] kids = destFolder.listFiles();
+        if (destFolder.exists() && kids != null && kids.length > 0) {
+            // Folder exists and is not empty
+            wizardDescriptor.putProperty("WizardPanel_errorMessage",
+                    "Project Folder already exists and is not empty.");
+            return false;
+        }
+        wizardDescriptor.putProperty("WizardPanel_errorMessage", "");
+        return true;
+    }
+
+    void store(WizardDescriptor d) {
+        String name = projectNameTextField.getText().trim();
+        String folder = createdFolderTextField.getText().trim();
+
+        d.putProperty("projdir", new File(folder));
+        d.putProperty("name", name);
+    }
+
+    void read(WizardDescriptor settings) {
+        File projectLocation = (File) settings.getProperty("projdir");
+        if (projectLocation == null || projectLocation.getParentFile() == null || !projectLocation.getParentFile().isDirectory()) {
+            projectLocation = ProjectChooser.getProjectsFolder();
+        } else {
+            projectLocation = projectLocation.getParentFile();
+        }
+        this.projectLocationTextField.setText(projectLocation.getAbsolutePath());
+
+        String projectName = (String) settings.getProperty("name");
+        if (projectName == null) {
+            projectName = PROJECT_NAME;
+        }
+        this.projectNameTextField.setText(projectName);
+        this.projectNameTextField.selectAll();
+    }
+
+    void validate(WizardDescriptor d) throws WizardValidationException {
+        // nothing to validate
+    }
+
+    // Implementation of DocumentListener --------------------------------------
+    @Override
+    public void changedUpdate(DocumentEvent e) {
+        updateTexts(e);
+        if (this.projectNameTextField.getDocument() == e.getDocument()) {
+            firePropertyChange(PROP_PROJECT_NAME, null, this.projectNameTextField.getText());
+        }
+    }
+
+    @Override
+    public void insertUpdate(DocumentEvent e) {
+        updateTexts(e);
+        if (this.projectNameTextField.getDocument() == e.getDocument()) {
+            firePropertyChange(PROP_PROJECT_NAME, null, this.projectNameTextField.getText());
+        }
+    }
+
+    @Override
+    public void removeUpdate(DocumentEvent e) {
+        updateTexts(e);
+        if (this.projectNameTextField.getDocument() == e.getDocument()) {
+            firePropertyChange(PROP_PROJECT_NAME, null, this.projectNameTextField.getText());
+        }
+    }
+
+    /** Handles changes in the Project name and project directory, */
+    private void updateTexts(DocumentEvent e) {
+
+        Document doc = e.getDocument();
+
+        if (doc == projectNameTextField.getDocument() || doc == projectLocationTextField.getDocument()) {
+            // Change in the project name
+
+            String projectName = projectNameTextField.getText();
+            String projectFolder = projectLocationTextField.getText();
+
+            //if (projectFolder.trim().length() == 0 || projectFolder.equals(oldName)) {
+            createdFolderTextField.setText(projectFolder + File.separatorChar + projectName);
+            //}
+
+        }
+        panel.fireChangeEvent(); // Notify that the panel changed
+    }
+}

+ 254 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneWizardIterator.java

@@ -0,0 +1,254 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.templates.monkeyzone;
+
+import com.jme3.gde.templates.gradledesktop.options.CachedOptionsContainer;
+import java.awt.Component;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import javax.swing.JComponent;
+import javax.swing.event.ChangeListener;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.spi.project.ui.support.ProjectChooser;
+import org.openide.WizardDescriptor;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.util.NbBundle;
+
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class MonkeyZoneWizardIterator implements WizardDescriptor.InstantiatingIterator {
+
+    private int index;
+    private WizardDescriptor.Panel[] panels;
+    private WizardDescriptor wiz;
+    
+    
+    final static String master = "monkeyzone-master/";
+
+    
+    public MonkeyZoneWizardIterator() {
+
+        // Initiate the options getting...
+        CachedOptionsContainer.getInstance();
+    }
+
+    public static MonkeyZoneWizardIterator createIterator() {
+        return new MonkeyZoneWizardIterator();
+    }
+
+    private WizardDescriptor.Panel[] createPanels() {
+        return new WizardDescriptor.Panel[]{
+                    new MonkeyZoneDownloadPanel(),
+                    new MonkeyZoneWizardPanel()
+        };
+    }
+
+    private String[] createSteps() {
+        return new String[]{
+                    NbBundle.getMessage(MonkeyZoneWizardIterator.class, "LBL_DownloadProjectStep"),
+                    NbBundle.getMessage(MonkeyZoneWizardIterator.class, "LBL_CreateProjectStep"),
+                };
+    }
+
+    @Override
+    public Set/*<FileObject>*/ instantiate(/*ProgressHandle handle*/) throws IOException {
+        Set<FileObject> resultSet = new LinkedHashSet<>();
+        File dirF = FileUtil.normalizeFile((File) wiz.getProperty("projdir"));
+        dirF.mkdirs();
+
+        FileObject template = FileUtil.toFileObject(new File(
+                MonkeyZoneDownloadPanel.DOWNLOAD_FOLDER, 
+                MonkeyZoneDownloadPanel.ZIP_NAME));
+        
+        FileObject dir = FileUtil.toFileObject(dirF);
+        unZipFile(template.getInputStream(), dir);
+        
+        // Always open top dir as a project:
+        resultSet.add(dir);
+        // Look for nested projects to open as well:
+        Enumeration<? extends FileObject> e = dir.getFolders(true);
+        while (e.hasMoreElements()) {
+            FileObject subfolder = e.nextElement();
+            if (ProjectManager.getDefault().isProject(subfolder)) {
+                resultSet.add(subfolder);
+            }
+        }
+
+        File parent = dirF.getParentFile();
+        if (parent != null && parent.exists()) {
+            ProjectChooser.setProjectsFolder(parent);
+        }
+
+        return resultSet;
+    }
+
+    @Override
+    public void initialize(WizardDescriptor wiz) {
+        this.wiz = wiz;
+        index = 0;
+        panels = createPanels();
+        // Make sure list of steps is accurate.
+        String[] steps = createSteps();
+        for (int i = 0; i < panels.length; i++) {
+            Component c = panels[i].getComponent();
+            if (steps[i] == null) {
+                // Default step name to component name of panel.
+                // Mainly useful for getting the name of the target
+                // chooser to appear in the list of steps.
+                steps[i] = c.getName();
+            }
+            if (c instanceof JComponent jc) {                 // Step #.
+                // TODO if using org.openide.dialogs >= 7.8, can use WizardDescriptor.PROP_*:
+                jc.putClientProperty("WizardPanel_contentSelectedIndex", i);
+                // Step name (actually the whole list for reference).
+                jc.putClientProperty("WizardPanel_contentData", steps);
+            }
+        }
+    }
+
+    @Override
+    public void uninitialize(WizardDescriptor wiz) {
+        this.wiz.putProperty("projdir", null);
+        this.wiz.putProperty("name", null);
+        this.wiz = null;
+        panels = null;
+    }
+
+    @Override
+    public String name() {
+        return MessageFormat.format("{0} of {1}",
+                new Object[]{index + 1, panels.length});
+    }
+
+    @Override
+    public boolean hasNext() {
+        return index < panels.length - 1;
+    }
+
+    @Override
+    public boolean hasPrevious() {
+        return index > 0;
+    }
+
+    @Override
+    public void nextPanel() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+        index++;
+    }
+
+    @Override
+    public void previousPanel() {
+        if (!hasPrevious()) {
+            throw new NoSuchElementException();
+        }
+        index--;
+    }
+
+    @Override
+    public WizardDescriptor.Panel current() {
+        return panels[index];
+    }
+
+    // If nothing unusual changes in the middle of the wizard, simply:
+    @Override
+    public final void addChangeListener(ChangeListener l) {
+    }
+
+    @Override
+    public final void removeChangeListener(ChangeListener l) {
+    }
+
+    private void createFileFromTemplate(File target, String templateResourcePath, Map<String, Object> tokens) throws IOException {
+        
+        // Create FreeMarker script engine
+        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
+        ScriptEngine engine = scriptEngineManager.getEngineByName("freemarker");
+        Map<String, Object> bindings = engine.getContext().getBindings(ScriptContext.ENGINE_SCOPE);
+        bindings.putAll(tokens);
+        
+        // Process template           
+        try {
+            FileObject targetFO = FileUtil.toFileObject(target);
+            try (Writer os = new BufferedWriter(new OutputStreamWriter(targetFO.getOutputStream(), StandardCharsets.UTF_8)); Reader is = new BufferedReader(new InputStreamReader(MonkeyZoneWizardIterator.class.getResourceAsStream("/" + templateResourcePath)));) {
+                engine.getContext().setWriter(os);
+                engine.eval(is);
+            }
+        } catch (IOException | ScriptException ex) {
+                throw new IOException(ex.getMessage(), ex);
+        }
+    }
+    
+    private static void unZipFile(InputStream source, FileObject projectRoot) throws IOException {
+        try (source) {
+            ZipInputStream str = new ZipInputStream(source);
+            ZipEntry entry;
+            while ((entry = str.getNextEntry()) != null) {
+                if (entry.getName().endsWith(master)) {
+                    continue;
+                }
+                final String entryName = entry.getName().replace(master, "");
+                if (entry.isDirectory()) {
+                    FileUtil.createFolder(projectRoot, entryName);
+                } else {
+                    FileObject fo = FileUtil.createData(projectRoot, entryName);
+                    try (OutputStream out = fo.getOutputStream()) {
+                        FileUtil.copy(str, out);
+                    }
+                }
+            }
+        }
+    }
+
+}

+ 126 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/MonkeyZoneWizardPanel.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 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.templates.monkeyzone;
+
+import java.awt.Component;
+import java.util.HashSet;
+import java.util.Set;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
+import org.openide.util.HelpCtx;
+import org.openide.util.NbBundle;
+
+/**
+ * Panel just asking for basic info.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class MonkeyZoneWizardPanel implements WizardDescriptor.Panel,
+        WizardDescriptor.ValidatingPanel, WizardDescriptor.FinishablePanel {
+
+    private WizardDescriptor wizardDescriptor;
+    private MonkeyZonePanelVisual component;
+
+    public MonkeyZoneWizardPanel() {
+    }
+
+    @Override
+    public Component getComponent() {
+        if (component == null) {
+            component = new MonkeyZonePanelVisual(this);
+            component.setName(NbBundle.getMessage(MonkeyZoneWizardPanel.class, "LBL_CreateProjectStep"));
+        }
+        return component;
+    }
+
+    @Override
+    public HelpCtx getHelp() {
+        return new HelpCtx("sdk.project_creation");
+    }
+
+    @Override
+    public boolean isValid() {
+        getComponent();
+        return component.valid(wizardDescriptor);
+    }
+    
+    private final Set<ChangeListener> listeners = new HashSet<>(1); // or can use ChangeSupport in NB 6.0
+
+    @Override
+    public final void addChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.add(l);
+        }
+    }
+
+    @Override
+    public final void removeChangeListener(ChangeListener l) {
+        synchronized (listeners) {
+            listeners.remove(l);
+        }
+    }
+
+    protected final void fireChangeEvent() {
+        Set<ChangeListener> ls;
+        synchronized (listeners) {
+            ls = new HashSet<>(listeners);
+        }
+        ChangeEvent ev = new ChangeEvent(this);
+        for (ChangeListener l : ls) {
+            l.stateChanged(ev);
+        }
+    }
+
+    @Override
+    public void readSettings(Object settings) {
+        wizardDescriptor = (WizardDescriptor) settings;
+        component.read(wizardDescriptor);
+    }
+
+    @Override
+    public void storeSettings(Object settings) {
+        WizardDescriptor d = (WizardDescriptor) settings;
+        component.store(d);
+    }
+
+    @Override
+    public boolean isFinishPanel() {
+        return false;
+    }
+
+    @Override
+    public void validate() throws WizardValidationException {
+        getComponent();
+        component.validate(wizardDescriptor);
+    }
+}

+ 65 - 0
jme3-templates/src/com/jme3/gde/templates/monkeyzone/build.gradle.ftl

@@ -0,0 +1,65 @@
+plugins {
+    id 'java'
+    id 'application'
+}
+
+group 'com.monkeyzone'
+version '1.0'
+
+mainClassName = "com.monkeyzone.MonkeyZone"
+
+repositories {
+    mavenCentral()
+}
+
+project.ext {
+  jmeVer = '3.7.0-stable'
+}
+
+project(":assets") {
+    apply plugin: "java"
+
+    buildDir = rootProject.file("build/assets")
+
+    sourceSets {
+        main {
+            resources {
+                srcDir '.'
+            }
+        }
+    }
+
+    java {
+        toolchain {
+            languageVersion = JavaLanguageVersion.of(21)
+        }
+    }
+}
+
+java {
+    toolchain {
+        languageVersion = JavaLanguageVersion.of(21)
+    }
+}
+
+dependencies {
+
+  implementation "org.jmonkeyengine:jme3-core:$jmeVer"
+  implementation "org.jmonkeyengine:jme3-desktop:$jmeVer"
+  implementation "org.jmonkeyengine:jme3-lwjgl:$jmeVer"
+  implementation "org.jmonkeyengine:jme3-lwjgl:$jmeVer"
+  implementation "com.github.stephengold:Heart:9.0.0"
+  implementation "com.github.stephengold:Minie:8.0.0"
+  implementation project("assets")
+
+}
+
+jar {
+    manifest {
+        attributes 'Main-Class': "$mainClassName"
+    }
+}
+
+wrapper {
+    gradleVersion = '8.6'
+}

BIN
jme3-templates/src/com/jme3/gde/templates/monkeyzone/monkeyzone.png