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; - } - - -}