Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6b3fb02
Add the Maven-first GUI Builder rewrite
shai-almog Aug 5, 2026
f0099da
Fix table cell shuffling and the stale canvas after a drop
shai-almog Aug 5, 2026
a745163
Hold the whole tree to account after every nested move
shai-almog Aug 5, 2026
83742fb
Add demo forms covering the nesting cases that keep breaking
shai-almog Aug 5, 2026
3e0b524
Stop an emptied container from pushing its neighbour off the canvas
shai-almog Aug 5, 2026
a48a012
Revert demo theme colours left over from a manual CSS test
shai-almog Aug 5, 2026
e9935e8
Say why a keystroke was refused instead of ignoring it
shai-almog Aug 5, 2026
6fbb1c2
Leave the caret alone when an edit is refused
shai-almog Aug 5, 2026
87972f5
Type real keystrokes at the editor in an integration test
shai-almog Aug 5, 2026
b5e94ca
Stop the drag guide overlay from swallowing every click on the editor
shai-almog Aug 6, 2026
bc033dd
Make the Java editor typable and stop a stale record eating the P key
shai-almog Aug 6, 2026
c464bd0
Stop the Edit menu stealing keys from the code editor
shai-almog Aug 6, 2026
6e516f8
Repaint the canvas when CSS restyles it, and focus the CSS editor onc…
shai-almog Aug 6, 2026
c9e7aeb
Keep an open editor alive when the canvas is rebuilt
shai-almog Aug 6, 2026
26fce3e
Deliver every keystroke, give the canvas its own theme, verify what s…
shai-almog Aug 6, 2026
fb7a361
Let a text component have the letter p back
shai-almog Aug 6, 2026
9d5bc37
Drive a CSS edit from the keyboard in a test, and repaint the form
shai-almog Aug 6, 2026
4fb1569
Verify the canvas is styled by the project's own stylesheet
shai-almog Aug 6, 2026
ed92f38
Report resolved styles over MCP, and see the canvas ignoring the styl…
shai-almog Aug 6, 2026
2d3fe7c
Correct the record on canvas styling
shai-almog Aug 6, 2026
4238759
Add the required copyright headers to every new file
shai-almog Aug 6, 2026
f4c07f9
Copy built artifacts into the local repository instead of trusting in…
shai-almog Aug 6, 2026
4a30134
Address the PR review: generated source, save safety and IDE bindings
shai-almog Aug 6, 2026
9287cf7
Give the GUI form scaffolder the project's copyright header
shai-almog Aug 6, 2026
ac77fcf
Document the GUI builder that ships, with screenshots of a new project
shai-almog Aug 6, 2026
8499f3a
Give a pasted table child a free cell
shai-almog Aug 6, 2026
9711156
Match the guide's American spelling in the GUI builder section
shai-almog Aug 7, 2026
4040496
Report a failed replacement instead of a successful save
shai-almog Aug 7, 2026
a65a4b3
Leave multi key mode alone when the application already owns it
shai-almog Aug 7, 2026
a242e6a
Build the GUI Builder for Java 8 so every project can open it
shai-almog Aug 7, 2026
ab354af
Restore the Java 8 build and fix a batch of review findings
shai-almog Aug 7, 2026
f3739b1
Close the gaps the last batch of fixes opened
shai-almog Aug 7, 2026
f66cba8
Generate the binding model on save and respect table spans
shai-almog Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .github/workflows/guibuilder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: GUI Builder

# The standalone GUI Builder lives outside the Maven reactor (scripts/guibuilder)
# and PR CI ignores scripts/**, so nothing else compiles it. It depends on core
# APIs (CodeEditor protected regions, LayeredLayout baseline units) that can be
# changed in the same PR, which is exactly how the editor silently stops
# compiling against a freshly built core.

on:
workflow_dispatch:
pull_request:
branches:
- master
paths:
- '.github/workflows/guibuilder.yml'
- 'scripts/guibuilder/**'
- '!scripts/guibuilder/**/*.md'
# The editor is an ordinary Codename One app: it renders the design canvas with
# Tabs, TableLayout, SplitPane and the rest of the core UI, compiles the project
# stylesheet with the CSS compiler, and runs on the JavaSE port. Listing only the
# few classes it obviously touches let a core change break it with this job skipped.
- 'CodenameOne/src/**'
- 'Ports/JavaSE/src/**'
- 'maven/css-compiler/**'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/**'
push:
branches:
- master
paths:
- '.github/workflows/guibuilder.yml'
- 'scripts/guibuilder/**'
- '!scripts/guibuilder/**/*.md'
# The editor is an ordinary Codename One app: it renders the design canvas with
# Tabs, TableLayout, SplitPane and the rest of the core UI, compiles the project
# stylesheet with the CSS compiler, and runs on the JavaSE port. Listing only the
# few classes it obviously touches let a core change break it with this job skipped.
- 'CodenameOne/src/**'
- 'Ports/JavaSE/src/**'
- 'maven/css-compiler/**'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/**'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
guibuilder:
runs-on: ubuntu-latest
timeout-minutes: 60
container: ghcr.io/codenameone/codenameone/pr-ci-container:latest
defaults:
run:
shell: bash

steps:
- uses: actions/checkout@v6

- name: Cache Maven dependencies
uses: actions/cache@v5
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2

- name: Prepare Codename One binaries
# The JavaSE port needs jfxrt.jar from cn1-binaries; without it the
# build fails with a large, misleading set of missing javafx.* errors.
run: |
set -euo pipefail
rm -rf maven/target/cn1-binaries
mkdir -p maven/target
cp -r /opt/cn1-binaries maven/target/cn1-binaries

- name: Install Codename One artifacts (JDK 8)
# JAVA_HOME_8 / JAVA_HOME_17 are container image environment variables,
# so they are expanded by the shell rather than the env context.
run: |
set -euo pipefail
export JAVA_HOME="${JAVA_HOME_8}"
export PATH="${JAVA_HOME}/bin:${PATH}"
cd maven
mvn -B install -Plocal-dev-javase -DskipTests \
-Darchetype.test.skip=true -Dmaven.javadoc.skip=true \
-Dcn1.binaries="${GITHUB_WORKSPACE}/maven/target/cn1-binaries"

# Built and tested on JDK 8 so that the editor stays loadable on every JDK a Codename One
# project may be built with. A newer JDK here would let a post-8 API through unnoticed.
- name: Run standalone GUI Builder tests (JDK 8)
run: |
set -euo pipefail
export JAVA_HOME="${JAVA_HOME_8}"
export PATH="${JAVA_HOME}/bin:${PATH}"
cd scripts/guibuilder
xvfb-run -a mvn -B -pl javase -am clean test -Dcodename1.platform=javase

- name: Package the executable GUI Builder (JDK 8)
run: |
set -euo pipefail
export JAVA_HOME="${JAVA_HOME_8}"
export PATH="${JAVA_HOME}/bin:${PATH}"
cd scripts/guibuilder
xvfb-run -a mvn -B -pl javase -am -Pexecutable-jar package \
-Dcodename1.platform=javase -Dmaven.test.skip=true
ls javase/target/codenameone-guibuilder-*.jar

- name: Upload GUI Builder test reports
if: always()
uses: actions/upload-artifact@v7
with:
name: guibuilder-test-reports
path: scripts/guibuilder/javase/target/surefire-reports
if-no-files-found: warn
retention-days: 14
100 changes: 92 additions & 8 deletions .github/workflows/release-on-maven-central.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
# The four Central confirmation polls each cover the documented 30-minute
# The five Central confirmation polls each cover the documented 30-minute
# propagation window, and each fires only when its own deploy reported failure,
# so the worst case is ~2h of polling on top of the build. Bounded explicitly
# so the worst case is ~2.5h of polling on top of the build. Bounded explicitly
# rather than inheriting the 6h default, so a wedged release fails in a shift
# rather than overnight.
timeout-minutes: 240
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
export GPG_TTY=$(tty)
# Once dual publish is off, R2 is the repository and Central must not receive
# new immutable releases. skipPublishing still stages, so the R2 upload that
# follows is unaffected -- same treatment as the three editor deploys.
# follows is unaffected -- same treatment as the four editor deploys.
skip_publishing=true
if [ "${{ vars.CN1_DUAL_PUBLISH }}" = "true" ]; then
skip_publishing=false
Expand Down Expand Up @@ -246,7 +246,7 @@ jobs:
continue-on-error: true
# always(): central-publishing stages before it uploads, so the tree exists
# even when the Central deploy above failed -- which is precisely the case
# R2 is here to survive. Metadata is regenerated once, after all three.
# R2 is here to survive. Metadata is regenerated once, after all four.
if: always() && steps.deploy_gamebuilder.conclusion != 'skipped'
run: |
# Each editor is its own reactor, so it stages into its own target dir.
Expand Down Expand Up @@ -335,7 +335,7 @@ jobs:
continue-on-error: true
# always(): central-publishing stages before it uploads, so the tree exists
# even when the Central deploy above failed -- which is precisely the case
# R2 is here to survive. Metadata is regenerated once, after all three.
# R2 is here to survive. Metadata is regenerated once, after all four.
if: always() && steps.deploy_certificatewizard.conclusion != 'skipped'
run: |
# Each editor is its own reactor, so it stages into its own target dir.
Expand Down Expand Up @@ -413,7 +413,7 @@ jobs:
continue-on-error: true
# always(): central-publishing stages before it uploads, so the tree exists
# even when the Central deploy above failed -- which is precisely the case
# R2 is here to survive. Metadata is regenerated once, after all three.
# R2 is here to survive. Metadata is regenerated once, after all four.
if: always() && steps.deploy_settings.conclusion != 'skipped'
run: |
# Each editor is its own reactor, so it stages into its own target dir.
Expand Down Expand Up @@ -447,10 +447,88 @@ jobs:
echo "codenameone-settings ${GITHUB_REF_NAME} never appeared on Maven Central within 30 minutes." >&2
exit 1

# --- GUI Builder editor -------------------------------------------------
# The cn1:guibuilder goal resolves the standalone Java-17 GUI Builder and
# its runtime dependencies from Maven Central, exactly like cn1:settings.
- name: Deploy GUI Builder editor to Maven Central
id: deploy_guibuilder
continue-on-error: true
if: >-
always() && (steps.deploy.outcome == 'success' || steps.confirm.outcome == 'success' ||
steps.r2_core.outcome == 'success')
run: |
export GPG_TTY=$(tty)
# Do not publish this editor to Central unless the core release actually
# reached Central. The editor declares core/plugin at this same version, so
# publishing it against a core that is not there leaves an immutable Central
# release whose dependencies cannot resolve. skipPublishing still stages the
# artifacts, so the R2 upload that follows is unaffected.
skip_publishing=true
if [ "${{ vars.CN1_DUAL_PUBLISH }}" = "true" ] \
&& { [ "${{ steps.deploy.outcome }}" = "success" ] \
|| [ "${{ steps.confirm.outcome }}" = "success" ]; }; then
skip_publishing=false
else
echo "Core release did not reach Central (or dual publish is off):"
echo "staging this editor for R2 only."
fi
cd scripts/guibuilder
xvfb-run -a mvn -Pexecutable-jar -Pguibuilder-central deploy \
-DskipPublishing=$skip_publishing \
-Dcodename1.platform=javase \
-Dgpg.passphrase=$MAVEN_GPG_PASSPHRASE \
-Dcn1.version=$GITHUB_REF_NAME -Dcn1.plugin.version=$GITHUB_REF_NAME \
-Dmaven.test.skip=true
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}

- name: Publish GUI Builder editor to R2
id: r2_guibuilder
# Non-fatal for the same reason as the core upload: one editor's R2 failure
# must not skip the remaining editors' Central deploys.
continue-on-error: true
# always(): central-publishing stages before it uploads, so the tree exists
# even when the Central deploy above failed -- which is precisely the case
# R2 is here to survive. Metadata is regenerated once, after all four.
if: always() && steps.deploy_guibuilder.conclusion != 'skipped'
run: |
# Each editor is its own reactor, so it stages into its own target dir.
bash maven/scripts/r2/publish-staging-to-r2.sh scripts/guibuilder/target/central-staging
url="${R2_BASE_URL}/com/codenameone/codenameone-guibuilder/${GITHUB_REF_NAME}/codenameone-guibuilder-${GITHUB_REF_NAME}.pom?cb=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
[ "$code" = "200" ] || { echo "MISSING on R2: codenameone-guibuilder (HTTP $code)" >&2; exit 1; }
echo "ok: codenameone-guibuilder ${GITHUB_REF_NAME} on R2"

- name: Confirm GUI Builder editor on Maven Central
id: confirm_guibuilder
# Runs only to second-guess a failed deploy: central-publishing
# reports "Deployment failed while publishing" for bundles it actually accepted,
# and Central releases are immutable so a rerun cannot repair that.
if: >-
always() && vars.CN1_DUAL_PUBLISH == 'true' &&
steps.deploy_guibuilder.outcome == 'failure'
continue-on-error: true
run: |
set +e
url="https://repo1.maven.org/maven2/com/codenameone/codenameone-guibuilder/${GITHUB_REF_NAME}/codenameone-guibuilder-${GITHUB_REF_NAME}.jar"
for i in $(seq 1 90); do
code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
if [ "$code" = "200" ]; then
echo "Confirmed codenameone-guibuilder ${GITHUB_REF_NAME} on Maven Central"
exit 0
fi
echo "[$i/90] Waiting on Maven Central for codenameone-guibuilder (code=$code)"
sleep 20
done
echo "codenameone-guibuilder ${GITHUB_REF_NAME} never appeared on Maven Central within 30 minutes." >&2
exit 1

- name: Mark the R2 release complete
id: r2_mark_complete
continue-on-error: true
# Only now, once the core reactor and all three editors are up AND the core
# Only now, once the core reactor and all four editors are up AND the core
# artifacts have been observed on R2. This is what makes the tag discoverable:
# regen refuses to advertise a version without it, on this run and every future
# one. r2_core_confirm is what verifies the expected artifacts are actually
Expand All @@ -465,7 +543,8 @@ jobs:
steps.r2_core_confirm.outcome == 'success' &&
steps.r2_gamebuilder.outcome == 'success' &&
steps.r2_certificatewizard.outcome == 'success' &&
steps.r2_settings.outcome == 'success'
steps.r2_settings.outcome == 'success' &&
steps.r2_guibuilder.outcome == 'success'
run: bash maven/scripts/r2/mark-release-complete.sh "${GITHUB_REF_NAME}"

- name: Regenerate R2 metadata and archetype catalog
Expand Down Expand Up @@ -517,6 +596,7 @@ jobs:
bad "${{ steps.r2_gamebuilder.outcome }}" && fail "Game Builder -> R2"
bad "${{ steps.r2_certificatewizard.outcome }}" && fail "Signing Wizard -> R2"
bad "${{ steps.r2_settings.outcome }}" && fail "Settings -> R2"
bad "${{ steps.r2_guibuilder.outcome }}" && fail "GUI Builder -> R2"
bad "${{ steps.r2_mark_complete.outcome }}" && fail "marking the R2 release complete"
bad "${{ steps.r2_metadata.outcome }}" && fail "R2 metadata regeneration"
if [ "${{ vars.CN1_DUAL_PUBLISH }}" = "true" ]; then
Expand All @@ -538,6 +618,10 @@ jobs:
&& [ "${{ steps.confirm_settings.outcome }}" != "success" ]; then
fail "Settings -> Maven Central"
fi
if bad "${{ steps.deploy_guibuilder.outcome }}" \
&& [ "${{ steps.confirm_guibuilder.outcome }}" != "success" ]; then
fail "GUI Builder -> Maven Central"
fi
fi
[ "$status" = "0" ] && echo " all publications succeeded"
exit $status
6 changes: 5 additions & 1 deletion CodenameOne/src/com/codename1/components/SplitPane.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.codename1.ui.Container;
import com.codename1.ui.Display;
import com.codename1.ui.FontImage;
import com.codename1.ui.Form;
import com.codename1.ui.Graphics;
import com.codename1.ui.Image;
import com.codename1.ui.Label;
Expand Down Expand Up @@ -1094,7 +1095,10 @@ protected boolean isStickyDrag() {
@Override
protected void initComponent() {
super.initComponent();
getComponentForm().setEnableCursors(true);
Form form = getComponentForm();
if (form != null) {
form.setEnableCursors(true);
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5428,8 +5428,9 @@ public String editorPeerQuery(PeerComponent peer, String name, String arg) {

/// Returns true when this platform can bind a `com.codename1.ui.TextInputClient` to a low level text
/// input source (soft keyboard / IME / hardware keyboard) so a component can capture raw text input
/// while rendering the document itself. When false the pure Codename One editors fall back to their
/// `BrowserComponent` backend. The default returns false.
/// while rendering the document itself. When false the pure Codename One editors read the physical
/// keyboard directly through `Component#keyReleased(int)`; there is no browser or HTML backend.
/// The default returns false.
public boolean isTextInputSupported() {
return false;
}
Expand Down
41 changes: 41 additions & 0 deletions CodenameOne/src/com/codename1/ui/CodeEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
import com.codename1.ui.editor.CodePureEditor;
import com.codename1.ui.editor.PureEditor;
import com.codename1.ui.editor.SyntaxHighlighter;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.util.EventDispatcher;
import com.codename1.util.SuccessCallback;

import java.util.Hashtable;
Expand Down Expand Up @@ -66,6 +69,7 @@ public class CodeEditor extends AbstractEditorComponent {
private boolean showLineNumbers = true;
private int tabSize = 4;
private CodeCompletionProvider completionProvider;
private final EventDispatcher protectedEditListeners = new EventDispatcher();

/// Creates an empty code editor.
public CodeEditor() {
Expand Down Expand Up @@ -219,6 +223,39 @@ public void insertAtCursor(String text) {
command("insertText", text);
}

/// Protects all text between matching marker lines from editing. This is intended for generated
/// source previews that contain explicit user-editable regions. Passing null clears protection.
public void setProtectedRegionMarkers(String startMarker, String endMarker) {
command("setProtectedMarkers", startMarker == null || endMarker == null
? "" : startMarker + "\n" + endMarker);
}

/// Registers a listener notified when an edit is refused because it falls inside a protected
/// region. Without it a protected region is silent, and one covering most of the document is
/// indistinguishable from an editor that ignores the keyboard. The event source is this editor
/// and the event is fired on the EDT.
///
/// #### Parameters
///
/// - `listener`: invoked once per refused edit
public void addProtectedEditListener(ActionListener listener) {
protectedEditListeners.addListener(listener);
}

/// Removes a listener added by `#addProtectedEditListener(ActionListener)`.
///
/// #### Parameters
///
/// - `listener`: the listener to remove
public void removeProtectedEditListener(ActionListener listener) {
protectedEditListeners.removeListener(listener);
}

/// Moves the caret to a character offset, clamped by the editor backend to the document bounds.
public void setCursorPosition(int offset) {
command("setCursor", String.valueOf(Math.max(0, offset)));
}

/// Retrieves the current caret character offset. The callback is invoked on the EDT.
///
/// #### Parameters
Expand Down Expand Up @@ -303,6 +340,10 @@ void onEditorEvent(String type, String value) {
handleCompletionRequest(value);
return;
}
if ("protectedEdit".equals(type)) {
protectedEditListeners.fireActionEvent(new ActionEvent(this));
return;
}
super.onEditorEvent(type, value);
}

Expand Down
Loading
Loading