diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index 9d90635a80..29c1e4ea22 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -458,10 +458,17 @@ private String getGradleJavaHome() throws BuildException { ); } } + if (home == null && currentJvmIsJava17OrLater()) { + // The Maven plugin itself is already running on a JDK that Gradle 8 accepts + // (Java 17+). Use that JVM's home instead of forcing the user to duplicate + // the JDK location in JAVA17_HOME. + return System.getProperty("java.home"); + } if (home == null) { throw new BuildException( "When using gradle 8, " + - "you must set the JAVA17_HOME environment variable to the location of a Java 17 JDK" + "you must set the JAVA17_HOME environment variable to the location of a Java 17 JDK " + + "(or run Maven on Java 17+ and let the build reuse the current JVM)" ); } @@ -473,6 +480,19 @@ private String getGradleJavaHome() throws BuildException { } return System.getProperty("java.home"); } + + private static boolean currentJvmIsJava17OrLater() { + String spec = System.getProperty("java.specification.version", ""); + if (spec.startsWith("1.")) { + // 1.5 .. 1.8 era — definitely older than 17. + return false; + } + try { + return Integer.parseInt(spec) >= 17; + } catch (NumberFormatException ex) { + return false; + } + } private int parseVersionStringAsInt(String versionString) { if (versionString.indexOf(".") > 0) { try { diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java index 4aa572cb1c..5b19e1101b 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java @@ -220,11 +220,24 @@ public void generateCode(File destination, boolean overwrite) throws IOException log.debug(javaseFile+" already exists. Skipping"); } - if(overwrite || !csFile.exists()) { - log.info("Writing "+csFile); - generateCSFile(csFile, "FrameworkElement"); + // Only emit the UWP / Windows C# stub if the project actually has a win/ + // module. Java 17 generated projects no longer ship that module (the + // Windows native port is legacy), so omitting the stub avoids leaving + // dangling .cs files outside any compiled source set. + File winModuleRoot = csFile.getParentFile(); + while (winModuleRoot != null && !"win".equals(winModuleRoot.getName())) { + winModuleRoot = winModuleRoot.getParentFile(); + } + boolean winModulePresent = winModuleRoot != null && winModuleRoot.isDirectory(); + if (winModulePresent) { + if(overwrite || !csFile.exists()) { + log.info("Writing "+csFile); + generateCSFile(csFile, "FrameworkElement"); + } else { + log.debug(csFile+" already exists. Skipping"); + } } else { - log.debug(csFile+" already exists. Skipping"); + log.debug("No win/ module under destination — skipping C# stub for "+nativeInterface.getName()); } if(overwrite || !(iosHFile.exists() || iosMFile.exists())) { log.info("Writing "+iosHFile); diff --git a/scripts/initializr/common/pom.xml b/scripts/initializr/common/pom.xml index 34167c9ded..829b8b4834 100644 --- a/scripts/initializr/common/pom.xml +++ b/scripts/initializr/common/pom.xml @@ -306,6 +306,22 @@ + + + + src/main/resources + + skill/** + + + org.apache.maven.plugins @@ -402,6 +418,28 @@ + + + package-claude-skill + process-resources + + run + + + + + + + + + diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java index e017e450ca..42bd266fe0 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java @@ -53,7 +53,7 @@ public void runApp() { final boolean[] roundedButtons = new boolean[]{true}; final boolean[] includeLocalizationBundles = new boolean[]{false}; final ProjectOptions.PreviewLanguage[] previewLanguage = new ProjectOptions.PreviewLanguage[]{ProjectOptions.PreviewLanguage.ENGLISH}; - final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_8}; + final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_17}; final String[] customThemeCss = new String[]{""}; final String[] lastValidCustomThemeCss = new String[]{""}; final TextArea[] customCssEditorRef = new TextArea[1]; diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java index 1e927881a1..8d8a0a7916 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -42,6 +42,53 @@ public class GeneratorModel { ".DS_Store\n" + "Thumbs.db\n"; + private static final String AGENT_SKILL_TARGET_PREFIX = ".agent-skills/codename-one/"; + private static final String CLAUDE_SKILL_STUB_PATH = ".claude/skills/codename-one/SKILL.md"; + private static final String CLAUDE_SKILL_STUB_BODY = + "---\n" + + "name: codename-one\n" + + "description: Build and modify Codename One cross-platform mobile apps (Java 17, Maven, ParparVM/Android/iOS/JavaScript). Use when the project contains a `common/codenameone_settings.properties`, depends on `com.codenameone:codenameone-core`, edits CSS files under `common/src/main/css/`, calls `cn1:run`, `cn1:test`, `cn1:build`, references `com.codename1.ui.*` / `com.codename1.testing.*`, or when the user asks to build a UI, write screen tests, generate screenshots, or compare to Swing/HTML/Android.\n" + + "metadata:\n" + + " type: skill\n" + + "---\n" + + "\n" + + "# Codename One — App and UI Authoring Skill (Claude Code stub)\n" + + "\n" + + "This file exists so Claude Code can index the Codename One authoring skill.\n" + + "The actual skill content is **vendor-neutral** and lives in this repository at:\n" + + "\n" + + "- `.agent-skills/codename-one/SKILL.md` — top-level cheat sheet\n" + + "- `.agent-skills/codename-one/references/*.md` — deep-dive references\n" + + "- `.agent-skills/codename-one/tools/` — runnable Java 17 utilities (`isApiSupported`, `isCssValid`, ...)\n" + + "\n" + + "**Read `.agent-skills/codename-one/SKILL.md` next.** All the guidance you need to\n" + + "build, style, test, debug, and port to Codename One is in that directory.\n"; + private static final String AGENTS_MD_BODY = + "# AGENTS.md\n" + + "\n" + + "This project is a Codename One cross-platform mobile app (Java 17 / Maven /\n" + + "ParparVM-iOS / Android / JavaScript / desktop). A vendor-neutral authoring skill\n" + + "is bundled in this repository for any AI agent:\n" + + "\n" + + "- **Start here:** `.agent-skills/codename-one/SKILL.md`\n" + + "- **Topical references:** `.agent-skills/codename-one/references/`\n" + + "- **Runnable utilities (Java 17 single-file source mode):** `.agent-skills/codename-one/tools/`\n" + + "\n" + + "Tool integrations (Claude Code, Cursor, etc.) may also pick this skill up via\n" + + "their own conventions; the canonical source of truth is `.agent-skills/`.\n" + + "\n" + + "## Quick orientation for an agent\n" + + "\n" + + "- App source lives in `common/src/main/java/`.\n" + + "- Theme/styling lives in `common/src/main/css/theme.css` (Codename One CSS — a\n" + + " deliberate subset, see `.agent-skills/codename-one/references/css.md`).\n" + + "- Run the simulator with `mvn -pl common cn1:run`.\n" + + "- Run tests with `mvn -pl common cn1:test` (on Linux CI use `xvfb-run -a`).\n" + + "- Native cloud builds use `mvn -pl package -Dcodename1.platform=... -Dcodename1.buildTarget=...`.\n" + + "\n" + + "When in doubt, open `.agent-skills/codename-one/SKILL.md` and follow the\n" + + "reference table at the bottom.\n"; + private final IDE ide; private final Template template; private final String appName; @@ -83,6 +130,9 @@ void writeProjectZip(OutputStream outputStream) throws IOException { copyZipEntriesToMap("/common.zip", mergedEntries, ZipEntryType.COMMON); copySingleTextEntryToMap(".gitignore", GENERATED_GITIGNORE, mergedEntries, ZipEntryType.COMMON); copySingleTextEntryToMap("README.md", buildReadmeMarkdown(), mergedEntries, ZipEntryType.COMMON); + if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17) { + addAgentSkillEntries(mergedEntries); + } copySingleTextEntryToMap("common/pom.xml", readResourceToString(template.POM_XML), mergedEntries, ZipEntryType.TEMPLATE_POM); if (template.CN1LIB_ZIP != null) { copyZipEntriesToMap(template.CN1LIB_ZIP, mergedEntries, ZipEntryType.TEMPLATE_CN1LIB); @@ -102,6 +152,38 @@ void writeProjectZip(OutputStream outputStream) throws IOException { } + private void addAgentSkillEntries(Map mergedEntries) throws IOException { + // Ship the Codename One authoring skill inside every generated project under a + // vendor-neutral path so any AI agent (Claude Code, Cursor, others) can pick it up. + // The skill markdown lives in source form under src/main/resources/skill/** and is + // repackaged into skill.zip at build time (CN1 classloader rejects nested + // directories under resources). ASCII-only Markdown so no encoding surprises end + // up in the project tree. + try (ZipInputStream zis = new ZipInputStream(getResourceAsStream("/skill.zip"))) { + ZipEntry entry = zis.getNextEntry(); + while (entry != null) { + if (!entry.isDirectory()) { + byte[] sourceData = readToBytesNoClose(zis); + String relative = entry.getName(); + // Defensive normalization: ant may produce backslashes on Windows. + relative = StringUtil.replaceAll(relative, "\\", "/"); + String targetPath = AGENT_SKILL_TARGET_PREFIX + relative; + byte[] targetData = applyDataReplacements(targetPath, sourceData); + mergedEntries.put(targetPath, targetData); + } + zis.closeEntry(); + entry = zis.getNextEntry(); + } + } + // Top-level AGENTS.md so agents that follow the (emerging) AGENTS.md convention + // discover the skill without having to know our directory layout. + copySingleTextEntryToMap("AGENTS.md", AGENTS_MD_BODY, mergedEntries, ZipEntryType.COMMON); + // Claude Code stub. Frontmatter so the skill shows up in /skills, body redirects + // to the canonical vendor-neutral content. + copySingleTextEntryToMap(CLAUDE_SKILL_STUB_PATH, CLAUDE_SKILL_STUB_BODY, + mergedEntries, ZipEntryType.COMMON); + } + private void addLocalizationEntries(Map mergedEntries) throws IOException { if (!isBareTemplate() || !options.includeLocalizationBundles) { return; @@ -130,11 +212,21 @@ private void addLocalizationEntries(Map mergedEntries) throws IO } private void copyZipEntriesToMap(String zipResource, Map mergedEntries, ZipEntryType zipType) throws IOException { + boolean dropWindowsModule = options.javaVersion == ProjectOptions.JavaVersion.JAVA_17; try(ZipInputStream zis = new ZipInputStream(getResourceAsStream(zipResource))) { ZipEntry entry = zis.getNextEntry(); while (entry != null) { if (!entry.isDirectory()) { - copyEntryToMap(entry.getName(), readToBytesNoClose(zis), mergedEntries, zipType); + String name = entry.getName(); + if (dropWindowsModule && (name.startsWith("win/") || name.startsWith("win\\"))) { + // Java 17 projects don't ship the UWP/Windows native module — strip + // its source tree from the extracted skeleton. The matching root-pom + // block is removed in applyDataReplacements below. + zis.closeEntry(); + entry = zis.getNextEntry(); + continue; + } + copyEntryToMap(name, readToBytesNoClose(zis), mergedEntries, zipType); } zis.closeEntry(); entry = zis.getNextEntry(); @@ -220,6 +312,9 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw if ("pom.xml".equals(targetPath)) { content = replaceTagValue(content, "cn1.plugin.version", CN1_PLUGIN_VERSION); content = replaceTagValue(content, "cn1.version", CN1_PLUGIN_VERSION); + if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17) { + content = stripWindowsModuleProfile(content); + } } if ("android/pom.xml".equals(targetPath) || "ios/pom.xml".equals(targetPath) || "javascript/pom.xml".equals(targetPath)) { content = hardenPlatformModulePomAgainstDoubleJarAttach(content); @@ -233,14 +328,14 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw private String applyJavaVersionSettings(String content) { - if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL) { + if (options.javaVersion == ProjectOptions.JavaVersion.JAVA_17) { content = replaceProperty(content, "codename1.arg.java.version", "17"); } return content; } private String applyJavaVersionToPom(String content) { - if (options.javaVersion != ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL) { + if (options.javaVersion != ProjectOptions.JavaVersion.JAVA_17) { return content; } content = StringUtil.replaceAll(content, "1.8", "17"); @@ -249,7 +344,7 @@ private String applyJavaVersionToPom(String content) { } private String normalizeIntellijMiscXml(String content) { - String languageLevel = options.javaVersion == ProjectOptions.JavaVersion.JAVA_17_EXPERIMENTAL ? "JDK_17" : "JDK_1_8"; + String languageLevel = options.javaVersion == ProjectOptions.JavaVersion.JAVA_17 ? "JDK_17" : "JDK_1_8"; content = removeXmlAttribute(content, "project-jdk-name"); content = removeXmlAttribute(content, "project-jdk-type"); content = setXmlAttribute(content, "languageLevel", languageLevel); @@ -415,6 +510,34 @@ private static String replaceTagValue(String xml, String tagName, String value) return xml.substring(0, valueStart) + value + xml.substring(end); } + private static String stripWindowsModuleProfile(String pom) { + // Remove the win... block from the root pom. + // Tolerant of leading whitespace and any inner content. The win/ source + // tree is also dropped from the zip extraction (see copyZipEntriesToMap). + int idIdx = pom.indexOf("win"); + if (idIdx < 0) { + return pom; + } + int profileStart = pom.lastIndexOf("", idIdx); + if (profileStart < 0) { + return pom; + } + int profileEnd = pom.indexOf("", idIdx); + if (profileEnd < 0) { + return pom; + } + profileEnd += "".length(); + // Swallow trailing newline if present so we don't leave an empty line. + while (profileEnd < pom.length() && (pom.charAt(profileEnd) == '\n' || pom.charAt(profileEnd) == '\r')) { + profileEnd++; + } + // Also swallow leading whitespace on the line containing . + while (profileStart > 0 && (pom.charAt(profileStart - 1) == ' ' || pom.charAt(profileStart - 1) == '\t')) { + profileStart--; + } + return pom.substring(0, profileStart) + pom.substring(profileEnd); + } + private static String normalizeJavasePom(String pom) { pom = removeDependencyBlock(pom, "com.codenameone", "codenameone-core", "provided"); pom = removeDependencyBlock(pom, "com.codenameone", "codenameone-javase", "provided"); diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java index fa5546003f..2ae1e3bb2c 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java @@ -44,8 +44,8 @@ public enum Accent { } public enum JavaVersion { - JAVA_8("Java 8"), - JAVA_17_EXPERIMENTAL("Java 17 (Experimental)"); + JAVA_17("Java 17"), + JAVA_8("Java 8"); public final String label; @@ -81,11 +81,11 @@ public ProjectOptions(ThemeMode themeMode, Accent accent, boolean roundedButtons this.roundedButtons = roundedButtons; this.includeLocalizationBundles = includeLocalizationBundles; this.previewLanguage = previewLanguage == null ? PreviewLanguage.ENGLISH : previewLanguage; - this.javaVersion = javaVersion == null ? JavaVersion.JAVA_8 : javaVersion; + this.javaVersion = javaVersion == null ? JavaVersion.JAVA_17 : javaVersion; this.customThemeCss = customThemeCss; } public static ProjectOptions defaults() { - return new ProjectOptions(ThemeMode.LIGHT, Accent.DEFAULT, true, false, PreviewLanguage.ENGLISH, JavaVersion.JAVA_8); + return new ProjectOptions(ThemeMode.LIGHT, Accent.DEFAULT, true, false, PreviewLanguage.ENGLISH, JavaVersion.JAVA_17); } } diff --git a/scripts/initializr/common/src/main/resources/skill/SKILL.md b/scripts/initializr/common/src/main/resources/skill/SKILL.md new file mode 100644 index 0000000000..753a13ccad --- /dev/null +++ b/scripts/initializr/common/src/main/resources/skill/SKILL.md @@ -0,0 +1,280 @@ +--- +name: codename-one +description: Build and modify Codename One cross-platform mobile apps (Java 17, Maven, ParparVM/Android/iOS/JavaScript). Use when the project contains a `common/codenameone_settings.properties`, depends on `com.codenameone:codenameone-core`, edits CSS files under `common/src/main/css/`, calls `cn1:run`, `cn1:test`, `cn1:build`, references `com.codename1.ui.*` / `com.codename1.testing.*`, or when the user asks to build a UI, write screen tests, generate screenshots, or compare to Swing/HTML. +metadata: + type: skill +--- + +# Codename One — App and UI Authoring Skill + +This skill teaches you how to write code for a Codename One (CN1) cross-platform mobile project. Codename One compiles Java/Kotlin bytecode to native iOS, Android, desktop and web. It looks like Java AWT/Swing, behaves like a mobile UI toolkit, and styles with a subset of CSS. + +**Use this skill when**: + +- A file you are editing imports `com.codename1.ui.*`, `com.codename1.io.*`, `com.codename1.testing.*`, or extends `com.codename1.system.Lifecycle`. +- You are editing a file in `common/src/main/css/` (CN1 CSS). +- You are running `cn1:run`, `cn1:debug`, `cn1:test`, or `cn1:build` Maven goals. +- The user asks for a UI screen, a screenshot test, a responsive layout, or wants to convert a Swing/HTML snippet to CN1. + +## How this skill is organized + +`SKILL.md` (this file) is the top-level cheat sheet. Deeper reference material lives under `references/` — pull the relevant file in **only when you need it**: + +- `references/build-and-run.md` — Local vs cloud builds, JDK matrix, Maven goals, `codenameone_settings.properties`, running the simulator, building for iOS/Android/Web, automated (Enterprise) cloud builds in CI. +- `references/build-hints.md` — Curated index of `codename1.arg.*` build hints (iOS, Android, push, web). +- `references/java-api-subset.md` — How to inspect the supported Java API subset, IO (`Storage`, `FileSystemStorage`), networking (`ConnectionRequest`, `Rest`), concurrency, dates, SQLite. **Read this whenever the compliance check fails or when you reach for a `java.*` API.** +- `references/ui-components.md` — Form, Toolbar, Container layouts (Border/Box/Flow/Grid/Layered), common components, navigation, dialogs. +- `references/css.md` — CSS capabilities and (important) **limitations**. Selectors, supported properties, 9-patch borders, theme constants. +- `references/swing-comparison.md` — Mapping Swing concepts and code to Codename One. Read this when porting Swing code. +- `references/html-css-cheatsheet.md` — Converting common HTML/CSS snippets to CN1 components + CSS. +- `references/android-to-cn1.md` — Porting Android (XML + Kotlin/Java) screens to Codename One. +- `references/testing-and-screenshots.md` — `AbstractTest`, `TestUtils`, `screenshotTest`, the `cn1:test` Maven goal, the screenshot tolerance algorithm. +- `references/mobile-adaptability.md` — Density-independent units (mm), `convertToPixels`, `LayeredLayout` for responsive design, `Display.isTablet()`, font scaling. +- `references/native-interfaces.md` — Authoring native interfaces for iOS/Android/JavaScript/Desktop with `cn1:generate-native-interfaces` and platform callbacks. +- `references/cn1libs.md` — Creating, packaging, and consuming Codename One libraries (Maven and legacy `.cn1lib`). +- `references/snapshot-builds.md` — Edge case: compiling against a Codename One SNAPSHOT from git. +- `references/debugging.md` — `jdb`-attach workflow for an agent: start the simulator paused, set breakpoints, dump locals, drive the session non-interactively from a script. +- `tools/` — runnable Java 17 single-file utilities. `tools/IsApiSupported.java` answers "is this `java.*` class in the CN1 subset?"; `tools/IsCssValid.java` answers "does this `theme.css` compile?". Run with `java tools/.java `. + +When the user's task hits any one of those topics, **read the matching reference before generating code**. Do not paste large snippets without checking. + +## Project layout (multi-module Maven) + +A CN1 project generated by the initializr has these modules: + +``` +my-app/ +├── pom.xml # Aggregator. cn1.plugin.version + cn1.version pinned here. +├── common/ # Cross-platform Java/Kotlin source. THIS IS WHERE THE APP LIVES. +│ ├── pom.xml # 17 17 by default +│ ├── codenameone_settings.properties +│ └── src/main/ +│ ├── java//.java +│ ├── css/theme.css # CN1 CSS (NOT regular web CSS - see references/css.md) +│ ├── l10n/ # i18n bundles (NOT src/main/resources!) +│ └── guibuilder/ # Optional GUI builder XML +├── javase/ # Desktop simulator port +├── android/ # Android wrapper (built via build server or local Gradle) +├── ios/ # iOS wrapper (ParparVM) +└── javascript/ # TeaVM-based web port +``` + +**Only edit `common/`**. The platform modules are thin wrappers — touching them is almost always wrong unless you are intentionally writing a native interface. + +## Java version and language features + +This project targets **Java 17** (`17` / `17` in `common/pom.xml`, plus `codename1.arg.java.version=17` in `codenameone_settings.properties`). Use: + +- `var` for local variable type inference +- Text blocks (`"""..."""`) +- Records +- Pattern matching for `instanceof` +- `switch` expressions +- Lambdas, method references, `Stream`s + +**Caveat — the build server cross-compiles to bytecode that ParparVM/TeaVM can consume.** Codename One ships a curated subset of the JDK, **not** the full `java.*` namespace. The `cn1:compliance-check` Maven goal runs on every compile and fails the build if you call an unsupported API. The most common gotchas: + +- No `java.nio.file.*` — use `com.codename1.io.FileSystemStorage` and `Storage`. +- No `java.net.http.*` / `java.net.URLConnection` — use `com.codename1.io.rest.Rest` (preferred) or `ConnectionRequest`. +- No `java.util.concurrent.locks.*` beyond simple `synchronized` — use `Display.getInstance().callSerially(...)` or `Display.startThread(...)`. +- No `java.awt.*` / `javax.swing.*` — CN1 has its own UI stack. See `references/swing-comparison.md`. +- No `java.lang.reflect.*` on production builds — works in the simulator only. +- No threads spawned with `new Thread(...).start()` for UI work — always go through `Display.callSerially` or `Display.startThread(...)`. + +For the authoritative subset list and IO/networking patterns, read `references/java-api-subset.md` (which also shows how to grep the `java-runtime` jar to verify any specific class/method). + +## The Event Dispatch Thread (EDT) + +CN1 has a single EDT, exactly like Swing. All UI mutation **must** happen on it. + +- Inside event listeners and lifecycle callbacks (`start`, `stop`, `init`) you are already on the EDT. +- From a background thread, hop back with `Display.getInstance().callSerially(() -> { ... })` (or `callSeriallyAndWait` if you need to block). +- Use `Display.getInstance().startThread(runnable, "name").start()` instead of `new Thread(...)` so cleanup happens correctly across platforms. + +`references/swing-comparison.md` contains a Swing→CN1 EDT idiom table. + +## The Lifecycle main class + +Every CN1 app extends `com.codename1.system.Lifecycle` (or `com.codename1.ui.util.Lifecycle` in older code). The four methods you may override: + +```java +public class MyAppName extends Lifecycle { + @Override + public void init(Object context) { + // Called once on the EDT. The Lifecycle base class already installs + // the theme; reach for the cached global resources instance from + // here on (Resources.getGlobalResources() returns the in-RAM copy, + // no disk re-read). + } + + @Override + public void runApp() { + // Build and show the first form. + Form f = new Form("Hello", new BorderLayout()); + f.add(BorderLayout.CENTER, new Label("Welcome")); + f.show(); + } + + @Override + public void stop() { /* App backgrounded */ } + + @Override + public void destroy() { /* App killed */ } +} +``` + +## Minimal "first screen" pattern + +```java +import static com.codename1.ui.CN.*; // Convenience statics: callSerially, etc. +import com.codename1.ui.*; +import com.codename1.ui.layouts.*; + +Form f = new Form("Profile", BoxLayout.y()); +f.getToolbar().addCommandToRightBar("Save", null, e -> save()); +f.add(new Label("Name")) + .add(new TextField()) + .add(new Button("Submit")); +f.show(); +``` + +`BoxLayout.y()` (vertical) and `BoxLayout.x()` (horizontal) are the most common layouts. Wrap a `Form` content pane in `BorderLayout` when you want a header/footer/center split. See `references/ui-components.md` for the full layout matrix. + +## CSS in Codename One + +CN1 ships with a CSS compiler that bakes `common/src/main/css/theme.css` into the binary theme resource (`theme.res`). It supports a deliberate **subset** of web CSS: + +```css +Form { + background-color: #0f172a; /* hex, rgb(), or named colors */ + padding: 2mm; /* mm is the recommended unit */ +} + +Button { + background-color: #1d4ed8; + color: #ffffff; + border: 1px solid #1d4ed8; + border-radius: 3mm; + padding: 2mm 4mm; +} + +Button.pressed { /* state pseudo-class baked as UIID */ + background-color: #1e3a8a; +} + +#Constants { + useLargerTextScaleBool: true; /* theme constants, not standard CSS */ +} +``` + +**Key differences from web CSS** (read `references/css.md` before authoring more): + +- Selectors target **UIIDs** (Codename One component style names), not arbitrary HTML elements. `Button`, `Form`, `Label`, `Toolbar`, `Title` are the most common. +- No descendant combinator, no `:hover`, no media queries. State variants are baked: `.pressed`, `.disabled`, `.selected`. +- Units: prefer `mm` (millimeters) over `px`. CN1 converts `mm` to device pixels via `Display.convertToPixels`. `1mm` ≈ 6-9 px depending on density. +- `border-radius` works but is rasterized at compile time — animating it at runtime requires programmatic styling. +- No `transform`, no `flex`, no `grid`. Use CN1 Java layouts for arrangement; CSS is only for *styling*. +- Bundled named colors are limited: `pink`, `orange`, `purple`, `yellow`, `gray`/`grey` are translated to hex by the initializr, anything else you must specify as hex. + +`references/html-css-cheatsheet.md` shows how to map "I want a flexbox row" / "I want a hero section" / "I want a card" to CN1 idioms. + +## Adaptability and responsive design + +Mobile screens vary wildly. CN1 gives you: + +- **Density-independent units**: `1mm` always renders ~1mm tall regardless of pixel density. Always size in `mm`, not `px`. +- `Display.getInstance().convertToPixels(2.5f)` — convert millimeters to current device pixels programmatically. +- `Display.getInstance().isTablet()`, `Display.getInstance().isPortrait()`, `Display.getInstance().getDisplayWidth/Height()` — branch on form factor. +- `LayeredLayout` with `LayeredLayoutConstraint` for precise responsive positioning (percent-based insets). +- `Toolbar` automatically reshapes to platform conventions (Android side menu / iOS tab bar). + +See `references/mobile-adaptability.md` for patterns: phone-vs-tablet master-detail, orientation listeners, dynamic font scaling. + +## Testing + +CN1 has its own test runner (`cn1:test`), not surefire. Tests extend `com.codename1.testing.AbstractTest`: + +```java +public class LoginFormTest extends AbstractTest { + @Override + public boolean shouldExecuteOnEDT() { return true; } + + @Override + public boolean runTest() throws Exception { + new MyAppName().runApp(); + TestUtils.waitForFormTitle("Login"); + TestUtils.setText("usernameField", "alice"); + TestUtils.clickButtonByLabel("Sign In"); + TestUtils.waitForFormTitle("Home"); + return screenshotTest("home-screen-baseline"); + } +} +``` + +Run with `mvn -pl common cn1:test` or `mvn test`. + +`screenshotTest(name)` captures the current form, compares against a stored baseline under `Storage`, and returns `true` if within tolerance. First run records the baseline. See `references/testing-and-screenshots.md` for the tolerance algorithm and how to validate UI you just wrote. + +> Important: a "screenshot matches baseline" only proves consistency, **not** correctness. If you just generated the baseline yourself, you have not validated the screen — visually inspect at least once before treating that baseline as ground truth. + +## Build and run commands + +From the project root: + +```bash +# Run in the desktop simulator (requires JDK 11–25 at runtime; build still uses JDK 17 source level) +mvn -pl common cn1:run + +# Run with breakpoints +mvn -pl common cn1:debug + +# Execute the CN1 test runner +mvn -pl common cn1:test + +# Cloud build for Android/iOS/JS (requires CN1 build server creds) +mvn -pl android package -Dcodename1.platform=android -Dcodename1.buildTarget=android-device +mvn -pl ios package -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-device +mvn -pl javascript package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript +``` + +See `references/build-and-run.md` for the local-vs-cloud matrix, automated-build mode (Enterprise), iOS local-build prerequisites, and the complete goal list. The full `codename1.arg.*` index lives in `references/build-hints.md`. + +## What NOT to do + +- Don't use `java.awt.Color` / `java.awt.Font` / `javax.swing.*` — CN1 has its own `Color` constants (just `int` ARGB), `Font.createTrueTypeFont`, and `Component` hierarchy. +- Don't add CSS that references web-only properties (`display`, `flex`, `position`, `transform`, `@media`) — the CN1 CSS compiler will silently ignore them or fail. +- Don't put localization bundles under `common/src/main/resources/`. The CN1 plugin scans `common/src/main/l10n/` (or `common/src/main/i18n/`); bundles placed anywhere else are NOT baked into `theme.res` and `Resources.getL10N("messages", lang)` returns `null` at runtime. +- Don't spin up `new Thread(...)` for UI work — use `Display.getInstance().callSerially(...)` or `Display.startThread(...)`. +- Don't mutate UI off the EDT. Symptoms: random repaint glitches, native crashes on iOS. +- Don't write screenshot tests where the baseline was just generated by the same code you are validating — that proves nothing. + +## Sanity-check loop before reporting "done" + +For any UI-altering change: + +1. Run `mvn -pl common cn1:run` in the simulator and click through the changed flow. +2. Inspect at least one screenshot (capture via the simulator menu → Save Screenshot, or generate via a test). +3. Resize the simulator window or toggle a different skin to confirm the layout doesn't break on a different form factor. +4. If you wrote a `screenshotTest`, delete the auto-generated baseline once if the screen has changed, then re-run twice — the second run should pass with `true`. + +If you cannot run the simulator (e.g. headless environment), **say so explicitly in the response** rather than claiming the UI works. + +## Reference quick-look index + +| If the user asks for... | Open this reference | +| --- | --- | +| "Add a screen with a list / form / dialog" | `references/ui-components.md` | +| "Make this look like X" / CSS tweaks | `references/css.md` | +| "Port this from Swing" / Swing idioms | `references/swing-comparison.md` | +| "I have HTML/CSS, convert it" | `references/html-css-cheatsheet.md` | +| "I have Android XML/Kotlin/Java, convert it" | `references/android-to-cn1.md` | +| "Write a test for this screen" / "Compare to a baseline" | `references/testing-and-screenshots.md` | +| "Make it look right on tablet/landscape" | `references/mobile-adaptability.md` | +| "How do I run/build/deploy" | `references/build-and-run.md` | +| "What's the right `codename1.arg.*` for X" / native config | `references/build-hints.md` | +| "Why does the compliance check fail" / Java/IO/networking | `references/java-api-subset.md` | +| "I need to call a native iOS/Android/JS/desktop API" | `references/native-interfaces.md` | +| "How do I create / consume a cn1lib" | `references/cn1libs.md` | +| "Build against a Codename One SNAPSHOT from git" | `references/snapshot-builds.md` | +| "Debug a faulty screen — attach `jdb` to the simulator" | `references/debugging.md` | +| Quick yes/no check: "is this `java.*` class supported", "does my `theme.css` compile" | `tools/` directory — `java tools/IsApiSupported.java ` / `java tools/IsCssValid.java ` | diff --git a/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md b/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md new file mode 100644 index 0000000000..96654d3b9e --- /dev/null +++ b/scripts/initializr/common/src/main/resources/skill/references/android-to-cn1.md @@ -0,0 +1,140 @@ +# Porting Android (XML + Kotlin/Java) to Codename One + +Codename One's component model is similar enough to Android's that you can usually translate screens one-to-one. As with the HTML conversion guide (`references/html-css-cheatsheet.md`), the layout system is the part that differs most — Android XML doesn't translate; build the same screen in Java + CN1 CSS. + +## Android view → CN1 component + +| Android `View` | CN1 component | Notes | +| --- | --- | --- | +| `LinearLayout` (vertical) | `BoxLayout.y()` | | +| `LinearLayout` (horizontal) | `BoxLayout.x()` | | +| `RelativeLayout` / `ConstraintLayout` | `LayeredLayout` with `LayeredLayoutConstraint` | Percent or mm insets and `setReferenceComponent*` for "below this view". | +| `FrameLayout` | `LayeredLayout` | Same stacking semantics. | +| `GridLayout` (Android's, not CSS) | `GridLayout` | | +| `RecyclerView` | `InfiniteContainer` | The pagination/recycling story maps directly. | +| `ScrollView` (vertical) | `Container.setScrollableY(true)` | Wrap your content. | +| `HorizontalScrollView` | `Container.setScrollableX(true)` | | +| `NestedScrollView` | **Don't nest scrollables in CN1** — see *Scrolling* in `references/ui-components.md`. | +| `Toolbar` / `ActionBar` | `Form.getToolbar()` | Already on every Form. Use `addMaterialCommandToRightBar/SideMenu` for actions. | +| `BottomNavigationView` | `Tabs` (placed in `BorderLayout.SOUTH`) or `Toolbar` bottom commands. | | +| `NavigationView` (drawer) | `Toolbar.addCommandToSideMenu(...)` / `addMaterialCommandToSideMenu(...)`. | | +| `TextView` | `Label` (single line) / `SpanLabel` (wrapped) | | +| `EditText` | `TextField` (single line) / `TextArea` (multi-line) | Set `setConstraint(...)` for the keyboard type. | +| `Button` / `MaterialButton` | `Button` | Use `Button(String, char)` to set a Material icon directly. | +| `ImageView` | `Label` with an `Image`, or `URLImage` for remote, or `Label.setMaterialIcon(char)` for an icon glyph. | | +| `Switch` / `CheckBox` / `RadioButton` | `Switch` / `CheckBox` / `RadioButton` | `RadioButton` needs a `ButtonGroup`. | +| `Spinner` | `Picker` (string list) | Do **not** use `ComboBox`. | +| `ProgressBar` (determinate) | `Slider` (set max, `setEditable(false)`) | | +| `ProgressBar` (indeterminate) | `InfiniteProgress` | | +| `FloatingActionButton` | `com.codename1.components.FloatingActionButton` | `createFAB(char icon).bindFabToContainer(...)`. | +| `Dialog` / `AlertDialog` | `Dialog.show(...)` | | +| `Toast` | `ToastBar.showMessage(...)` | | +| `Fragment` | A factory method returning a configured `Container`, attached to a Form. CN1 has no Fragment lifecycle — keep state in regular Java objects. | +| `Activity` | A separate `Form` class (or factory). Navigation = `nextForm.show()`. | +| `Intent` (in-app) | Direct method call to the next Form's factory. | +| `Intent` (external — `tel:` / `mailto:` / URL) | `Display.getInstance().execute("tel:...")`. | +| `RecyclerView.Adapter` | Implement `InfiniteContainer.fetchComponents(int, int)` or pass items to `MultiList`. | +| `WebView` | `BrowserComponent`. | +| `BottomSheetDialogFragment` | `InteractionDialog` (slides up from the bottom of the layered pane). | + +## Android XML → CN1 Java + +CN1 has no XML layout for screens (the GUI builder uses its own format). Translate Android XML directly to Java: + +```xml + + + + +