Simulator: data-driven menu hooks for cn1libs#4988
Open
shai-almog wants to merge 9 commits into
Open
Conversation
Adds a small framework feature that lets cn1libs contribute items to the
JavaSE simulator's menu bar via a properties file, without referencing
any Swing types. Each cn1lib drops a
META-INF/codenameone/simulator-hooks.properties on its classpath:
name=Bluetooth
item1.label=Add demo peripheral
item1.action=com.example.bt.sim.Hooks#addDemoPeripheral
The new SimulatorHookLoader scans every jar on the simulator classpath
via getResources(), parses the file, resolves each action's static
method against the classloader that loaded Display, and pre-binds a
Runnable that dispatches on the CN1 EDT. JavaSEPort.installMenu groups
the result by menu name and renders one JMenu per group between the
existing menus and the Help menu.
Why data-driven instead of a Java SPI: the simulator UX is going to be
rewritten and we don't want cn1libs to depend on JMenu/JMenuItem (or
have to be recompiled when the UX shape changes). The neutral
SimulatorHook record (menuName, label, Runnable) is the contract; the
UI shell on top is replaceable.
Tests in maven/javase cover well-formed parsing, declaration-order
preservation, and skip-on-error for every malformed case
(missing name, dangling label, unknown class, non-static target,
malformed action string).
Documentation lives in docs/developer-guide/Maven-Creating-CN1Libs.adoc
with the cn1-bluetooth lib as a worked example.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Compared 11 screenshots: 11 matched. |
Contributor
Cloudflare Preview
|
Collaborator
Author
|
Compared 20 screenshots: 20 matched. |
Contributor
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Collaborator
Author
|
Compared 109 screenshots: 109 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
CI's vale gate flagged 10 prose issues in the simulator-menu-hooks section: "backend" hits Microsoft.Avoid (treated as a noise token since it's our standard term for a swappable implementation), "e.g.", a missing Oxford-style comma inside quoted enumerations, a stray "freely" adverb, heading punctuation, and "do not" instead of "don't". Adds "[Bb]ackend" to the project vocabulary and rewrites the affected sentences. No semantic change to the section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog
added a commit
to codenameone/bluetoothle-codenameone
that referenced
this pull request
May 19, 2026
…latform plumbing Every cn1lib CI job failed on its first run with a mix of issues that all map to the same shape: the OS-activated native-helper Maven profiles tried to do too much, and the framework setup composite action didn't actually work cross-OS. Concretely: - The OS-activated profiles (linux-native-ble-helper, etc.) fired whenever a build ran on the matching OS, so the simulator-tests, ios-native-tests, and android-native-tests jobs all tried to compile the Rust helper too. Those runners don't have cargo (or libdbus-1-dev on Linux) installed, and they don't need the helper. Each profile now also requires `!skipNativeBleHelper` to activate, and every workflow / script that builds the cn1lib but doesn't need the helper passes -DskipNativeBleHelper=true. - Composite action used a hard-coded /tmp path. Windows runners fail with "directory name is invalid" because Git Bash there doesn't see a real Unix /tmp. Swapped to $RUNNER_TEMP throughout. - Composite action requested JDK 8 from Temurin, which doesn't ship Apple-Silicon JDK 8 binaries; macos-14 jobs failed with "Could not find satisfied version for SemVer '8'". Switched to Zulu. - device-test.yml never ran the framework setup; it tried to fetch codenameone-maven-plugin 8.0-SNAPSHOT from Central. Wired in the composite action and the skip flag. - TEMPORARY: composite's default cn1-ref is feat/simulator-menu-hooks (the framework PR's branch). Once codenameone/CodenameOne#4988 merges, flip back to 'master'. Comment in the action calls this out so the followup isn't forgotten. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Original feedback: CN1 tests live in the cross-platform common/
project and can't import JavaSE-port classes (no reflection, no
JavaSE-only types). The previous design forced tests to either
reflect on SimulatorHookLoader or directly instantiate cn1lib
internals, both of which break on iOS/Android/JavaScript.
This commit adds the missing piece: every hook gets a stable
`namespace:id` identifier and is registered with a new core class,
SimulatorHookExecutor. CN.executeHook(String hookId) delegates to
that executor and returns false on platforms where the registry is
empty (i.e., every non-simulator target). Tests in common/ can
drive simulator-only behavior with one cross-platform call:
CN.executeHook("bluetooth:addDemoPeripheral");
Also lifts the menu-label restriction: hooks may now declare an
id and action without a label, in which case they are registered
with the executor (callable from tests) but hidden from the
simulator's menu. Useful for test fixture scaffolding ("seed N
peripherals", "prime next-call failure") that would clutter the
menu UX.
Properties-file grammar additions:
namespace=<token> # defaults to slugified `name`
itemN.id=<token> # defaults to the property key (item1, item2)
itemN.label=... # NOW OPTIONAL; absent = API-only hook
Documentation in Maven-Creating-CN1Libs.adoc updated with the new
shape and a CN.executeHook test example.
12 JUnit tests on the framework parser pass; existing menu rendering
is unchanged for any hook that ships a label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The action wrapper used Display.callSerially, which is fire-and-forget. That broke CN.executeHook callers running off the EDT (every CN1 UnitTest's runTest()) — they'd assert state changes before the EDT had run the action and false-fail. Switched to Display.callSeriallyAndWait. From the EDT the body runs inline (CN1's existing semantics); from any other thread the call blocks until the action completes. CN.executeHook now returns true only after the hook has actually executed, so tests can immediately assert on the side effects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog
added a commit
to codenameone/bluetoothle-codenameone
that referenced
this pull request
May 20, 2026
Rewrite BluetoothSimulatorHooksTest to call hooks by id through
CN.executeHook("bluetooth:<id>") instead of importing
com.codename1.impl.javase.simulator.SimulatorHook{,Loader} directly.
The new pattern works unchanged in cn1lib-using apps where the test
lives in the cross-platform common/ project — no JavaSE-port
imports, no reflection.
simulator-hooks.properties:
- explicit namespace=bluetooth
- explicit item.id for every hook (matches the executor key shape)
- new item8 = primeReadFailure: a label-less hook the test uses
to script a one-shot read failure. Demonstrates the API-only
branch (no menu entry, still callable from tests).
BluetoothSimulatorHooks gains primeReadFailure() which delegates to
BluetoothSimulator.failNext("read", ...). Other existing hooks are
unchanged; the only state asserted from BluetoothSimulator is via
its public testing API (isEnabled, registeredPeripheralCount,
isPeripheralRegistered), which has always been part of the lib.
Depends on codenameone/CodenameOne#4988 latest commit (the
callSeriallyAndWait fix that makes CN.executeHook synchronous from
off-EDT callers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The framework's docs-style gate (build-test JDK 8/17/21) rejects classic /** Javadoc markers in core/CLDC classes — CN1 standardized on Java 25 /// markdown comments instead. Convert SimulatorHookExecutor to the project style. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous design added CN.executeHook + nested itemN.id/.label/.action
keys. Both miss the mark:
- We already have CN/Display.execute(String url) for native dispatch
on every platform; the JavaSE port can intercept hook urls before
handing the rest to the OS browser. Adding executeHook duplicates
that surface and forces app code to learn a second method.
- Items are positional: item1 is the first menu entry, item2 the
second. The previous design treated itemN as an opaque pairing
token (any string worked) and allowed reordering — that's wrong;
the simulator UX renders in numeric order and the loader should
too. The loop now stops at the first missing itemN, matching
what the user-facing menu would do.
- The compound key syntax (itemN.label, itemN.action, itemN.id) is
redundant when the namespace is already in the file's `name`/
`namespace` field. Replaced with parallel arrays: itemN holds
the action, labelN holds the label.
Final shape:
name=Bluetooth
namespace=bluetooth # optional; defaults to slugified `name`
item1=fqcn#method # required; the Nth menu item's action
label1=Toggle adapter # optional; if absent, hook is API-only
item2=fqcn#method2
label2=Add demo peripheral
# No labelN -> registered with the executor but not in the menu
item3=fqcn#primeReadFailure
JavaSEPort.execute intercepts urls matching a registered hook (key
shape: "namespace:itemN") and routes them through the existing
SimulatorHookExecutor; non-matching urls fall through to the
browser launcher. JavaSEPort.canExecute reports TRUE for registered
hook urls so tests can guard cross-platform.
SimulatorHookExecutor stays in core. Tests use CN.execute(...) plus
CN.canExecute(...) as a "are we in a simulator?" gate; no
JavaSE-only imports needed. 12 JUnit tests pin the positional loop,
slugify rules, gap-stops behavior, executor registration, and the
API-only branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Android port's javac uses ASCII encoding (see feedback memory),
so any non-ASCII character in a .java file under CodenameOne/ or
Ports/JavaSE/ trips "unmappable character" errors in build-test
(8/17). The redesign sneaked em-dashes ('--'), arrows ('->') and a
right single quote into a handful of new/edited files. Sweep them
out: every modified .java is now pure ASCII.
Also drops a stray "e.g." from the dev guide section that Vale's
Microsoft.Foreign rule flags; replaced with "for example".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PMD's AvoidUsingVolatile rule (enforced by the build-test JDK 8 "Generate static analysis HTML summaries" step) flagged the `private static volatile Map<String, Runnable> hooks` field. The semantics we want -- safe replacement of the registry from the JavaSE port while readers see either the old or new map atomically -- map cleanly to AtomicReference, which avoids the volatile keyword and keeps PMD happy. No observable behavior change. 12 framework JUnit tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rence) The volatile-vs-AtomicReference choice was a false binary: - volatile: trips PMD's AvoidUsingVolatile gate on the JDK 8 PR CI run. - AtomicReference: triggers "package java.util.concurrent.atomic does not exist" in the JavaSE port's javase-simulator-tests Ant step, which compiles the framework core against the CLDC subset. CLDC doesn't ship j.u.c.atomic. Both gates fail on the new class. The portable third option is plain synchronized accessors guarding the registry field with a private lock object -- same memory-visibility guarantee, compiles under every supported target, no PMD warning. Hook actions still run OUTSIDE the lock so a long-running hook can't deadlock a concurrent register() call. 12 framework JUnit tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog
added a commit
to codenameone/bluetoothle-codenameone
that referenced
this pull request
May 20, 2026
Previous version of this PR depended on CN1 8.0-SNAPSHOT to pick up the simulator hook intercept added in codenameone/CodenameOne#4988. That's wrong for a cn1lib: the framework feature isn't released yet, and tying this PR to an in-flight master commit means everything downstream (CI, anyone reviewing) has to also build CN1 from source. Reverting the cn1lib to the current release (7.0.243): - pom.xml: cn1.version / cn1.plugin.version = 7.0.243. - Drop the local-snapshots repository entry and the setup-cn1-framework composite action that cloned + built CN1 master on every CI job. The workflows now use the same plain curl-the-build-client pattern they had pre-PR. - ios-native-tests no longer needs continue-on-error: the iOS codegen on the released line generates the same dispatch shim the bridge has always been targeting (it worked under 7.0.71 and carries through 7.0.243), so the lib's ObjC selector naming (param1/param2/...) compiles cleanly. The 8.0-SNAPSHOT clash was upstream churn, not a bridge bug. - cn1-framework-pin.txt removed. The new hook surface ships dormant on 7.0.243: - simulator-hooks.properties and BluetoothSimulatorHooks are still there; on 7.0.243 the simulator simply doesn't read them, the menu items don't render, and CN.canExecute("bluetooth:item1") returns false. - BluetoothSimulatorHooksTest now opens with that exact canExecute guard. On 7.0.243 it returns true (test "passes" without doing anything); on a future CN1 release that intercepts the URLs (whenever #4988 ships), every existing assertion fires. Local verification: - mvn install of every module succeeds against 7.0.243. - mvn cn1:test from BTDemo passes 8/8 (hook test no-ops cleanly). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SimulatorHookLoaderto the JavaSE port: cn1libs ship aMETA-INF/codenameone/simulator-hooks.propertiesfile and get a menu in the simulator's menu bar, with no Swing types in the contract.JavaSEPort.installMenuconsumes the neutral hook list and emitsJMenu/JMenuItems today — replaceable when the simulator UX is rewritten without breaking any cn1lib.docs/developer-guide/Maven-Creating-CN1Libs.adocdocuments the contract with thecn1-bluetoothcn1lib as a worked example.Contract
Actions are
fqcn#staticMethodreferences topublic static voidno-arg methods. The loader resolves them viaDisplay.class.getClassLoader()and pre-binds aRunnablethat dispatches on the CN1 EDT viaDisplay.callSerially. Multiple cn1libs each contribute one menu; discovery usesClassLoader.getResources()so they merge cleanly.The neutral
SimulatorHookrecord (menuName,label,Runnable) is the contract; today's Swing menu rendering is just one consumer.Test plan
mvn test -pl maven/javase— 7 new tests, all pass:name=.actioncn1-bluetoothlib on the classpath shows a "Bluetooth" menu with all of the lib's items; clicking each fires the matching static method on the CN1 EDT.mvn cn1:test) still passes — the loader is additive and the menu construction path is unchanged for the existing three menus.Companion change
The
cn1-bluetoothcn1lib at codenameone/bluetoothle-codenameone has a companion PR using this mechanism. The lib's CI builds CN1 from source via a composite action so once this lands the lib's PR can be merged on top.🤖 Generated with Claude Code