diff --git a/.github/workflows/developer-guide-docs.yml b/.github/workflows/developer-guide-docs.yml
index bd8c1b34a8..490c5611c0 100644
--- a/.github/workflows/developer-guide-docs.yml
+++ b/.github/workflows/developer-guide-docs.yml
@@ -5,6 +5,7 @@ on:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'docs/developer-guide/**'
+ - 'docs/demos/**'
- '.github/workflows/developer-guide-docs.yml'
release:
types: [published]
@@ -22,6 +23,32 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4
+ - name: Determine changed components
+ id: changes
+ if: github.event_name == 'pull_request'
+ uses: dorny/paths-filter@v3
+ with:
+ filters: |
+ demos:
+ - 'docs/demos/**'
+ docs:
+ - 'docs/developer-guide/**'
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+
+ - name: Build Codename One demos
+ if: github.event_name != 'pull_request' || steps.changes.outputs.demos == 'true'
+ run: |
+ set -euo pipefail
+ mkdir -p "$HOME/.codenameone"
+ touch "$HOME/.codenameone/guibuilder.jar"
+ cp maven/CodeNameOneBuildClient.jar "$HOME/.codenameone/CodeNameOneBuildClient.jar"
+ xvfb-run -a mvn -B -ntp -Dgenerate-gui-sources-done=true -pl common -am -f docs/demos/pom.xml test
+
- name: Determine publication metadata
run: |
set -euo pipefail
@@ -87,6 +114,7 @@ jobs:
gem install --no-document asciidoctor asciidoctor-pdf
- name: Build Developer Guide HTML and PDF
+ if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true'
run: |
set -euo pipefail
OUTPUT_ROOT="build/developer-guide"
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/CounterDemo.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/CounterDemo.java
new file mode 100644
index 0000000000..32313eb564
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/CounterDemo.java
@@ -0,0 +1,61 @@
+package com.codenameone.developerguide;
+
+import com.codename1.components.SpanLabel;
+import com.codename1.ui.Button;
+import com.codename1.ui.Form;
+import com.codename1.ui.Slider;
+import com.codename1.ui.layouts.BoxLayout;
+
+/**
+ * Simple interactive demo that lets the user update a counter using buttons and a slider.
+ */
+public class CounterDemo implements Demo {
+ @Override
+ public String getTitle() {
+ return "Counter";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Interactive counter with increment/decrement controls and a slider.";
+ }
+
+ @Override
+ public void show(Form parent) {
+ Form form = new Form("Counter", BoxLayout.y());
+ form.setName("counterForm");
+ form.getToolbar().setBackCommand("Back", e -> parent.showBack());
+
+ SpanLabel valueLabel = new SpanLabel("Current value: 0");
+ valueLabel.setName("counterValueLabel");
+
+ Slider slider = new Slider();
+ slider.setName("counterSlider");
+ slider.setEditable(true);
+ slider.setMinValue(0);
+ slider.setMaxValue(20);
+ slider.setProgress(0);
+
+ Button increment = new Button("Increment");
+ increment.setName("incrementButton");
+ Button decrement = new Button("Decrement");
+ decrement.setName("decrementButton");
+
+ increment.addActionListener(e -> adjustValue(slider, valueLabel, Math.min(slider.getProgress() + 1, slider.getMaxValue())));
+ decrement.addActionListener(e -> adjustValue(slider, valueLabel, Math.max(slider.getProgress() - 1, slider.getMinValue())));
+ slider.addDataChangedListener((type, index) -> updateLabel(valueLabel, slider.getProgress()));
+
+ form.addAll(valueLabel, slider, increment, decrement);
+ form.show();
+ }
+
+ private void adjustValue(Slider slider, SpanLabel valueLabel, int newValue) {
+ slider.setProgress(newValue);
+ updateLabel(valueLabel, newValue);
+ }
+
+ private void updateLabel(SpanLabel valueLabel, int value) {
+ valueLabel.setText("Current value: " + value);
+ valueLabel.repaint();
+ }
+}
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/Demo.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/Demo.java
new file mode 100644
index 0000000000..0d019ea5ce
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/Demo.java
@@ -0,0 +1,26 @@
+package com.codenameone.developerguide;
+
+import com.codename1.ui.Form;
+
+/**
+ * Represents a standalone demo that can be launched from the demo browser.
+ */
+public interface Demo {
+ /**
+ * @return The title used to identify this demo to the user.
+ */
+ String getTitle();
+
+ /**
+ * @return A short description that is displayed in the demo browser.
+ */
+ String getDescription();
+
+ /**
+ * Launches the demo, optionally using the supplied parent form to return
+ * to when the demo is closed.
+ *
+ * @param parent The form that launched this demo.
+ */
+ void show(Form parent);
+}
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoBrowserForm.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoBrowserForm.java
new file mode 100644
index 0000000000..e3c149792a
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoBrowserForm.java
@@ -0,0 +1,32 @@
+package com.codenameone.developerguide;
+
+import com.codename1.components.MultiButton;
+import com.codename1.ui.Container;
+import com.codename1.ui.Form;
+import com.codename1.ui.layouts.BorderLayout;
+import com.codename1.ui.layouts.BoxLayout;
+
+/**
+ * Parent form that lists all demos and allows launching them.
+ */
+public class DemoBrowserForm extends Form {
+
+ public DemoBrowserForm() {
+ super("Developer Guide Demos", new BorderLayout());
+ getToolbar().setTitleCentered(false);
+ Container listContainer = new Container(BoxLayout.y());
+ listContainer.setName("demoList");
+ listContainer.setScrollableY(true);
+
+ int index = 0;
+ for (Demo demo : DemoRegistry.getDemos()) {
+ MultiButton demoButton = new MultiButton(demo.getTitle());
+ demoButton.setTextLine2(demo.getDescription());
+ demoButton.setName("demoButton-" + index++);
+ demoButton.addActionListener(e -> demo.show(DemoBrowserForm.this));
+ listContainer.add(demoButton);
+ }
+
+ add(BorderLayout.CENTER, listContainer);
+ }
+}
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoCode.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoCode.java
index 579f3c5b6d..759bde784a 100644
--- a/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoCode.java
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoCode.java
@@ -1,31 +1,13 @@
package com.codenameone.developerguide;
-import static com.codename1.ui.CN.*;
import com.codename1.system.Lifecycle;
-import com.codename1.ui.*;
-import com.codename1.ui.layouts.*;
-import com.codename1.io.*;
-import com.codename1.ui.plaf.*;
-import com.codename1.ui.util.Resources;
/**
- * This file was generated by Codename One for the purpose
- * of building native mobile applications using Java.
+ * Application entry point that launches the demo browser.
*/
public class DemoCode extends Lifecycle {
@Override
public void runApp() {
- Form hi = new Form("Hi World", BoxLayout.y());
- Button helloButton = new Button("Hello World");
- hi.add(helloButton);
- helloButton.addActionListener(e -> hello());
- hi.getToolbar().addMaterialCommandToSideMenu("Hello Command",
- FontImage.MATERIAL_CHECK, 4, e -> hello());
- hi.show();
+ new DemoBrowserForm().show();
}
-
- private void hello() {
- Dialog.show("Hello Codename One", "Welcome to Codename One", "OK", null);
- }
-
}
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoRegistry.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoRegistry.java
new file mode 100644
index 0000000000..eb803bed44
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/DemoRegistry.java
@@ -0,0 +1,25 @@
+package com.codenameone.developerguide;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Registry of available demos so that the browser can enumerate them.
+ */
+public final class DemoRegistry {
+ private static final List DEMOS = Collections.unmodifiableList(
+ Arrays.asList(
+ new HelloWorldDemo(),
+ new CounterDemo()
+ )
+ );
+
+ private DemoRegistry() {
+ // utility class
+ }
+
+ public static List getDemos() {
+ return DEMOS;
+ }
+}
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/HelloWorldDemo.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/HelloWorldDemo.java
new file mode 100644
index 0000000000..ed6d64208c
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/HelloWorldDemo.java
@@ -0,0 +1,34 @@
+package com.codenameone.developerguide;
+
+import com.codename1.ui.Button;
+import com.codename1.ui.Dialog;
+import com.codename1.ui.Form;
+import com.codename1.ui.layouts.BoxLayout;
+
+/**
+ * Simple hello world demo showing a dialog.
+ */
+public class HelloWorldDemo implements Demo {
+
+ @Override
+ public String getTitle() {
+ return "Hello World";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Shows a button that pops up a welcome dialog.";
+ }
+
+ @Override
+ public void show(Form parent) {
+ Form form = new Form("Hello World", BoxLayout.y());
+ form.setName("helloWorldForm");
+ form.getToolbar().setBackCommand("Back", e -> parent.showBack());
+ Button helloButton = new Button("Say Hello");
+ helloButton.setName("helloButton");
+ helloButton.addActionListener(e -> Dialog.show("Hello Codename One", "Welcome to Codename One", "OK", null));
+ form.add(helloButton);
+ form.show();
+ }
+}
diff --git a/docs/demos/common/src/test/java/com/codenameone/developerguide/CounterDemoTest.java b/docs/demos/common/src/test/java/com/codenameone/developerguide/CounterDemoTest.java
new file mode 100644
index 0000000000..50386eda4b
--- /dev/null
+++ b/docs/demos/common/src/test/java/com/codenameone/developerguide/CounterDemoTest.java
@@ -0,0 +1,88 @@
+package com.codenameone.developerguide;
+
+import com.codename1.components.SpanLabel;
+import com.codename1.testing.AbstractTest;
+import com.codename1.testing.TestUtils;
+import com.codename1.ui.Button;
+import com.codename1.ui.Command;
+import com.codename1.ui.Display;
+import com.codename1.ui.Form;
+import com.codename1.ui.Slider;
+import com.codename1.ui.events.ActionEvent;
+
+/**
+ * Validates the Counter demo's interactions update the UI consistently.
+ */
+public class CounterDemoTest extends AbstractTest {
+
+ @Override
+ public boolean runTest() throws Exception {
+ Form parent = new Form("Parent");
+ parent.show();
+ TestUtils.waitForFormTitle("Parent", 5000);
+
+ Demo demo = new CounterDemo();
+ demo.show(parent);
+ TestUtils.waitForFormTitle("Counter", 5000);
+
+ Form current = Display.getInstance().getCurrent();
+ assertEqual("Counter", current.getTitle());
+ assertEqual("counterForm", current.getName());
+
+ SpanLabel valueLabel = (SpanLabel) TestUtils.findByName("counterValueLabel");
+ assertNotNull(valueLabel, "Counter value label should exist.");
+ assertEqual("Current value: 0", valueLabel.getText());
+
+ Slider slider = (Slider) TestUtils.findByName("counterSlider");
+ assertNotNull(slider, "Counter slider should exist.");
+ assertEqual(0, slider.getProgress());
+
+ Button increment = (Button) TestUtils.findByName("incrementButton");
+ Button decrement = (Button) TestUtils.findByName("decrementButton");
+ assertNotNull(increment);
+ assertNotNull(decrement);
+
+ increment.pressed();
+ increment.released();
+ TestUtils.waitFor(200);
+ assertEqual(1, slider.getProgress());
+ assertEqual("Current value: 1", valueLabel.getText());
+
+ slider.setProgress(slider.getMaxValue());
+ TestUtils.waitFor(200);
+ assertEqual("Current value: " + slider.getMaxValue(), valueLabel.getText());
+
+ increment.pressed();
+ increment.released();
+ TestUtils.waitFor(200);
+ assertEqual(slider.getMaxValue(), slider.getProgress());
+ assertEqual("Current value: " + slider.getMaxValue(), valueLabel.getText());
+
+ decrement.pressed();
+ decrement.released();
+ TestUtils.waitFor(200);
+ assertEqual(slider.getMaxValue() - 1, slider.getProgress());
+ assertEqual("Current value: " + (slider.getMaxValue() - 1), valueLabel.getText());
+
+ slider.setProgress(slider.getMinValue());
+ TestUtils.waitFor(200);
+ decrement.pressed();
+ decrement.released();
+ TestUtils.waitFor(200);
+ assertEqual(slider.getMinValue(), slider.getProgress());
+ assertEqual("Current value: " + slider.getMinValue(), valueLabel.getText());
+
+ Command back = current.getBackCommand();
+ assertNotNull(back, "Back command should be available.");
+ back.actionPerformed(new ActionEvent(back));
+ TestUtils.waitForFormTitle("Parent", 5000);
+ assertEqual(parent, Display.getInstance().getCurrent());
+
+ return true;
+ }
+
+ @Override
+ public boolean shouldExecuteOnEDT() {
+ return true;
+ }
+}
diff --git a/docs/demos/common/src/test/java/com/codenameone/developerguide/DemoBrowserFormTest.java b/docs/demos/common/src/test/java/com/codenameone/developerguide/DemoBrowserFormTest.java
new file mode 100644
index 0000000000..05cec86ba4
--- /dev/null
+++ b/docs/demos/common/src/test/java/com/codenameone/developerguide/DemoBrowserFormTest.java
@@ -0,0 +1,48 @@
+package com.codenameone.developerguide;
+
+import com.codename1.components.MultiButton;
+import com.codename1.testing.AbstractTest;
+import com.codename1.testing.TestUtils;
+import com.codename1.ui.Component;
+import com.codename1.ui.Container;
+import com.codename1.ui.Display;
+
+/**
+ * Ensures the demo browser lists and describes the registered demos.
+ */
+public class DemoBrowserFormTest extends AbstractTest {
+
+ @Override
+ public boolean runTest() throws Exception {
+ DemoBrowserForm form = new DemoBrowserForm();
+ form.show();
+ TestUtils.waitForFormTitle("Developer Guide Demos", 5000);
+
+ assertEqual("Developer Guide Demos", Display.getInstance().getCurrent().getTitle());
+
+ Component content = form.getContentPane().getComponentAt(0);
+ assertTrue(content instanceof Container, "Demo list should be wrapped in a container.");
+
+ Container list = (Container) content;
+ assertEqual("demoList", list.getName());
+ assertEqual(DemoRegistry.getDemos().size(), list.getComponentCount());
+
+ for (int i = 0; i < list.getComponentCount(); i++) {
+ Component component = list.getComponentAt(i);
+ assertTrue(component instanceof MultiButton, "Expected demo option to be a MultiButton.");
+
+ MultiButton button = (MultiButton) component;
+ Demo demo = DemoRegistry.getDemos().get(i);
+ assertEqual(demo.getTitle(), button.getTextLine1());
+ assertEqual(demo.getDescription(), button.getTextLine2());
+ assertEqual("demoButton-" + i, button.getName());
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean shouldExecuteOnEDT() {
+ return true;
+ }
+}
diff --git a/docs/demos/common/src/test/java/com/codenameone/developerguide/DemoRegistryTest.java b/docs/demos/common/src/test/java/com/codenameone/developerguide/DemoRegistryTest.java
new file mode 100644
index 0000000000..21bbc8fc64
--- /dev/null
+++ b/docs/demos/common/src/test/java/com/codenameone/developerguide/DemoRegistryTest.java
@@ -0,0 +1,52 @@
+package com.codenameone.developerguide;
+
+import com.codename1.testing.AbstractTest;
+
+import java.util.List;
+
+/**
+ * Verifies the demo registry exposes the expected demos and is immutable.
+ */
+public class DemoRegistryTest extends AbstractTest {
+
+ @Override
+ public boolean runTest() throws Exception {
+ List demos = DemoRegistry.getDemos();
+
+ assertNotNull(demos, "Demo registry should never return null.");
+ assertFalse(demos.isEmpty(), "Demo registry should contain at least one demo.");
+ assertEqual(2, demos.size(), "Demo registry should contain the sample demos.");
+
+ Demo hello = demos.get(0);
+ assertEqual("Hello World", hello.getTitle());
+ assertEqual("Shows a button that pops up a welcome dialog.", hello.getDescription());
+
+ Demo counter = demos.get(1);
+ assertEqual("Counter", counter.getTitle());
+ assertEqual("Interactive counter with increment/decrement controls and a slider.", counter.getDescription());
+
+ boolean immutable = false;
+ try {
+ demos.add(new Demo() {
+ @Override
+ public String getTitle() {
+ return "Should Not Add";
+ }
+
+ @Override
+ public String getDescription() {
+ return "";
+ }
+
+ @Override
+ public void show(com.codename1.ui.Form parent) {
+ }
+ });
+ } catch (UnsupportedOperationException expected) {
+ immutable = true;
+ }
+ assertTrue(immutable, "Demo registry should be immutable.");
+
+ return true;
+ }
+}
diff --git a/docs/demos/common/src/test/java/com/codenameone/developerguide/HelloWorldDemoTest.java b/docs/demos/common/src/test/java/com/codenameone/developerguide/HelloWorldDemoTest.java
new file mode 100644
index 0000000000..714c163cd2
--- /dev/null
+++ b/docs/demos/common/src/test/java/com/codenameone/developerguide/HelloWorldDemoTest.java
@@ -0,0 +1,64 @@
+package com.codenameone.developerguide;
+
+import com.codename1.testing.AbstractTest;
+import com.codename1.testing.TestUtils;
+import com.codename1.ui.Button;
+import com.codename1.ui.Command;
+import com.codename1.ui.Dialog;
+import com.codename1.ui.Display;
+import com.codename1.ui.Form;
+import com.codename1.ui.events.ActionEvent;
+
+/**
+ * Exercises the Hello World demo lifecycle and interactions.
+ */
+public class HelloWorldDemoTest extends AbstractTest {
+
+ @Override
+ public boolean runTest() throws Exception {
+ Form parent = new Form("Parent");
+ parent.show();
+ TestUtils.waitForFormTitle("Parent", 5000);
+
+ Demo demo = new HelloWorldDemo();
+ demo.show(parent);
+ TestUtils.waitForFormTitle("Hello World", 5000);
+
+ Form current = Display.getInstance().getCurrent();
+ assertEqual("Hello World", current.getTitle());
+ assertEqual("helloWorldForm", current.getName());
+
+ Button button = (Button) TestUtils.findByName("helloButton");
+ assertNotNull(button, "Hello button should be present.");
+ assertEqual("Say Hello", button.getText());
+
+ Thread dialogCloser = new Thread(() -> {
+ TestUtils.waitFor(500);
+ Display.getInstance().callSerially(() -> {
+ if (Display.getInstance().getCurrent() instanceof Dialog) {
+ ((Dialog) Display.getInstance().getCurrent()).dispose();
+ }
+ });
+ });
+ dialogCloser.start();
+ button.pressed();
+ button.released();
+ dialogCloser.join(2000);
+
+ TestUtils.waitForFormTitle("Hello World", 5000);
+ assertEqual("Hello World", Display.getInstance().getCurrent().getTitle());
+
+ Command back = current.getBackCommand();
+ assertNotNull(back, "Back command should be available.");
+ back.actionPerformed(new ActionEvent(back));
+ TestUtils.waitForFormTitle("Parent", 5000);
+ assertEqual(parent, Display.getInstance().getCurrent());
+
+ return true;
+ }
+
+ @Override
+ public boolean shouldExecuteOnEDT() {
+ return true;
+ }
+}
diff --git a/docs/demos/common/src/test/java/com/codenameone/developerguide/MyFirstTest.java b/docs/demos/common/src/test/java/com/codenameone/developerguide/MyFirstTest.java
deleted file mode 100644
index 8ce80a9ce0..0000000000
--- a/docs/demos/common/src/test/java/com/codenameone/developerguide/MyFirstTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-
-/*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
- */
-package com.codenameone.developerguide;
-
-import com.codename1.testing.AbstractTest;
-
-/**
- *
- * @author shannah
- */
-public class MyFirstTest extends AbstractTest {
-
- @Override
- public boolean runTest() throws Exception {
- return true;
- }
-
-
-}