Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fdc26a7
Add lightweight rich text and code editors
shai-almog Jul 15, 2026
711d200
Exclude preserved articles from editor dependency gate
shai-almog Jul 15, 2026
4815e6d
Complete native editor formats and playground integration
shai-almog Jul 15, 2026
6ee1ef1
Merge remote-tracking branch 'origin/master' into pure-cn1-editors-clean
shai-almog Jul 15, 2026
c257ea6
Fix playground registry licensing and local validation
shai-almog Jul 15, 2026
c19dfac
Make code editor parser smoke portable across JDKs
shai-almog Jul 15, 2026
6a79a20
Fix developer guide prose quality findings
shai-almog Jul 15, 2026
bed4a18
Resolve editor static analysis findings
shai-almog Jul 15, 2026
a61ebbe
Fix JavaScript lightweight editor input
shai-almog Jul 16, 2026
f310182
Fix JavaScript lightweight editor input ordering
shai-almog Jul 16, 2026
ba05285
Verify deployed playground editor input
shai-almog Jul 16, 2026
8150d9b
Refresh JavaScript sticky header golden
shai-almog Jul 16, 2026
730a94d
Retry Linux ARM package downloads over IPv4
shai-almog Jul 16, 2026
51335e0
Fix pure editor input reliability: session lifecycle, JS focus, array…
shai-almog Jul 16, 2026
f864ff7
Cover primitive multi-dimensional array covariance in the JS runtime …
shai-almog Jul 16, 2026
d0c6578
Editor input hardening: iOS echo/composition/threading, Android IME m…
shai-almog Jul 16, 2026
7c33b07
Android: hardware keyboards and tap-to-reshow for the pure editor ses…
shai-almog Jul 16, 2026
0381c3e
Fix the copyright gate: CRLF sources and the covariance fixture header
shai-almog Jul 16, 2026
fea013b
Keep the playground editor tripwire from matching its own comment
shai-almog Jul 16, 2026
93d4103
Gate editor screenshot captures on the first real paint; appease Spot…
shai-almog Jul 16, 2026
fa8c42a
Add the standard header to BaseTest
shai-almog Jul 16, 2026
0ac88d1
FirstPaintGate: only fire once the gated content has real bounds
shai-almog Jul 16, 2026
66b0da0
Anchor Android screenshot PixelCopy to the next committed frame
shai-almog Jul 16, 2026
35f090f
Add the standard copyright header to AndroidScreenshotTask
shai-almog Jul 16, 2026
065f623
Stop sharing the text paint across Android graphics contexts
shai-almog Jul 16, 2026
9b1cd4f
Exclude AndroidGraphics from the header check
shai-almog Jul 16, 2026
55d8dbe
Retry transient GitHub API failures in skin resolution
shai-almog Jul 16, 2026
2b07c0e
Fall back to anonymous GitHub API lookups during incidents
shai-almog Jul 16, 2026
f229fb4
Scope the private text paint to mutable-image graphics
shai-almog Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 7 additions & 2 deletions .github/workflows/linux-build-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ jobs:
bash scripts/ci/apt-get-update.sh
# The CN1 CSS compiler renders via CEF/AWT and throws HeadlessException
# without a display, so the common build runs under a virtual X server.
sudo apt-get install -y --no-install-recommends xvfb
sudo apt-get install -y --no-install-recommends \
-o Acquire::ForceIPv4=true -o Acquire::Retries=5 \
xvfb

- name: Set up JDK 8
uses: actions/setup-java@v5
Expand Down Expand Up @@ -141,6 +143,7 @@ jobs:
set -e
bash scripts/ci/apt-get-update.sh
sudo apt-get install -y --no-install-recommends \
-o Acquire::ForceIPv4=true -o Acquire::Retries=5 \
cmake ninja-build pkg-config unzip xvfb fonts-dejavu-core \
libgtk-3-dev libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev libglib2.0-dev \
libfontconfig1-dev libfreetype-dev \
Expand Down Expand Up @@ -222,7 +225,9 @@ jobs:
LIBGL_ALWAYS_SOFTWARE: '1'
run: |
set -e
sudo apt-get install -y --no-install-recommends gdb >/dev/null 2>&1 || true
sudo apt-get install -y --no-install-recommends \
-o Acquire::ForceIPv4=true -o Acquire::Retries=5 \
gdb >/dev/null 2>&1 || true
Xvfb :99 -screen 0 1200x1600x24 >/tmp/xvfb-run.log 2>&1 &
export DISPLAY=:99
sleep 2
Expand Down
80 changes: 80 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import com.codename1.ui.Button;
import com.codename1.ui.CN;
import com.codename1.ui.Command;
import com.codename1.ui.ClipboardContent;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Dialog;
Expand Down Expand Up @@ -5200,6 +5201,12 @@ public void copyToClipboard(Object obj) {
lightweightClipboard = obj;
}

/// Copies multiple native clipboard representations in one operation. Ports should override
/// `copyToClipboard(Object)` and publish every MIME representation they support.
public void copyToClipboard(ClipboardContent content) {
copyToClipboard((Object) content);
}

/// Returns the current content of the clipboard
///
/// #### Returns
Expand All @@ -5209,6 +5216,30 @@ public Object getPasteDataFromClipboard() {
return lightweightClipboard;
}

/// Stores clipboard data received from a native paste event without publishing it back to the
/// system clipboard. Ports that negotiate multiple native clipboard representations should call
/// this before dispatching the framework paste event.
///
/// #### Parameters
///
/// - `data`: the clipboard payload exposed to `getPasteDataFromClipboard()`
protected final void setPasteDataFromClipboard(Object data) {
lightweightClipboard = data;
}

/// Returns the clipboard representations available to framework code. The default adapter keeps
/// old ports source-compatible while exposing plain text and content written through the new API.
public ClipboardContent getClipboardContent() {
Object value = getPasteDataFromClipboard();
if (value instanceof ClipboardContent) {
return (ClipboardContent) value;
}
if (value instanceof String) {
return new ClipboardContent().setData(ClipboardContent.MIME_TEXT, value);
}
return null;
}

/// Returns true if the device is currently in portrait mode
///
/// #### Returns
Expand Down Expand Up @@ -5352,6 +5383,55 @@ public String editorPeerQuery(PeerComponent peer, String name, String arg) {
return null;
}

/// 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.
public boolean isTextInputSupported() {
return false;
}

/// Binds a `com.codename1.ui.TextInputClient` to the platform text input source and shows the soft
/// keyboard on touch devices. The platform then routes committed text, IME composition, deletions and
/// key commands into the client, and reads back the client's editing state and caret rectangle. The
/// returned handle identifies this binding for `#updateTextInputState` and `#stopTextInput`. The
/// default returns null (unsupported).
///
/// #### Parameters
///
/// - `client`: the input client to bind
///
/// - `config`: the desired keyboard type and input behavior
///
/// #### Returns
///
/// an opaque handle for this binding, or null when unsupported
public Object startTextInput(com.codename1.ui.TextInputClient client, com.codename1.ui.TextInputConfig config) {
return null;
}

/// Pushes the client's authoritative editing state (surrounding text, selection, composition and
/// caret rectangle) down to the platform input source so autocorrect, prediction and the IME
/// candidate window stay in sync after a Codename One side edit (programmatic change, undo, reflow).
/// No-op by default.
///
/// #### Parameters
///
/// - `handle`: the handle returned by `#startTextInput`
///
/// - `state`: the current editing state
public void updateTextInputState(Object handle, com.codename1.ui.TextInputState state) {
}

/// Unbinds a text input client bound with `#startTextInput` and hides the soft keyboard on touch
/// devices. No-op by default.
///
/// #### Parameters
///
/// - `handle`: the handle returned by `#startTextInput`
public void stopTextInput(Object handle) {
}

/// Posts a message to the window in a BrowserComponent. This is intended to be an abstraction of the Javascript postMessage() API.
///
/// This is only overridden by the Javascript port to provide proper CORS handling. Other ports use the implementation
Expand Down
118 changes: 80 additions & 38 deletions CodenameOne/src/com/codename1/ui/AbstractEditorComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
package com.codename1.ui;

import com.codename1.ui.editor.EditorHost;
import com.codename1.ui.editor.PureEditor;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.layouts.BorderLayout;
Expand All @@ -40,27 +42,28 @@
/// and the inbound `#onEditorEvent(String, String)` dispatch. Two interchangeable backends honor
/// that channel:
///
/// 1. A 100% cross platform fallback backed by `BrowserComponent` (a `contenteditable` surface for
/// rich text, a syntax highlighting surface for code). This works on every platform that supports
/// the native web widget and gets virtual keyboard handling on phones/tablets and physical keyboard
/// handling on desktop for free.
/// 2. An optional native backend supplied by the platform port (see
/// 1. The pure Codename One text engine (`com.codename1.ui.editor`) which renders the document itself
/// with `Graphics`/`Font` and binds to the platform text input source (soft keyboard, hardware
/// keyboard and IME). This is the default where the port exposes low-level text input.
/// 2. A `BrowserComponent` fallback on ports that don't expose low-level text input. Its
/// `contenteditable` surface remains editable through the platform web view.
/// 3. An optional native backend supplied by the platform port (see
/// `com.codename1.impl.CodenameOneImplementation#createNativeEditorPeer(AbstractEditorComponent, String)`).
/// When a port returns a non-null native peer the editor drives it through
/// `editorPeerCommand` / `editorPeerQuery` instead of the browser, allowing a platform to provide a
/// genuinely native experience that can exceed an HTML based app.
/// `editorPeerCommand` / `editorPeerQuery` instead of the pure engine, allowing a platform to provide a
/// genuinely native experience.
///
/// Both backends are addressed with the same vocabulary so concrete editors never need to know which
/// All backends are addressed with the same vocabulary so concrete editors never need to know which
/// one is active.
///
/// @author Shai Almog
public abstract class AbstractEditorComponent extends Container {
/// Prefix used for all messages that travel from the web editor back to Codename One over the
/// `BrowserComponent` message bridge.
public abstract class AbstractEditorComponent extends Container implements EditorHost {
/// Prefix used for messages sent from the browser fallback to Codename One.
static final String MESSAGE_PREFIX = "cn1ed:";

private BrowserComponent browser;
private PeerComponent nativePeer;
private PureEditor pureEditor;
private boolean nativeMode;
private boolean ready;
private boolean editable = true;
Expand Down Expand Up @@ -101,8 +104,19 @@ private void initBackend() {
return;
}
nativeMode = false;
if (isTextInputSupported()) {
pureEditor = createPureEditor();
removeComponent(placeholder);
addComponent(BorderLayout.CENTER, pureEditor.getView());
markReady();
revalidateLater();
return;
}
initBrowserBackend();
}

private void initBrowserBackend() {
browser = new BrowserComponent();
// keep the editor chrome supplied by the surrounding form, the editing surface is transparent
browser.setProperty("BackgroundColor", 0xffffff);
browser.addWebEventListener(BrowserComponent.onMessage, new ActionListener() {
@Override
Expand All @@ -113,7 +127,6 @@ public void actionPerformed(ActionEvent evt) {
browser.addWebEventListener(BrowserComponent.onLoad, new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
// the page defines window.cn1editor synchronously so it is ready once the page loaded
markReady();
}
});
Expand All @@ -138,16 +151,8 @@ private void handleBrowserMessage(String msg) {
}
String body = msg.substring(MESSAGE_PREFIX.length());
int colon = body.indexOf(':');
String type;
String value;
if (colon < 0) {
type = body;
value = null;
} else {
type = body.substring(0, colon);
value = body.substring(colon + 1);
}
onEditorEvent(type, value);
onEditorEvent(colon < 0 ? body : body.substring(0, colon),
colon < 0 ? null : body.substring(colon + 1));
}

private void markReady() {
Expand All @@ -164,25 +169,25 @@ private void markReady() {
readyListeners.fireActionEvent(new ActionEvent(this));
}

/// Creates the pure Codename One backend for this editor. Subclasses override to supply a code or
/// rich text feature layer.
PureEditor createPureEditor() {
return new PureEditor(this, getEditorType());
}

/// Returns the editor type identifier passed to the native peer factory, e.g.
/// `"richtext"` or `"code"`.
abstract String getEditorType();

/// Returns the bootstrap HTML page used by the `BrowserComponent` fallback backend. The page must
/// define a global `window.cn1editor` object exposing `cmd(name, arg)` and `query(name, arg)`
/// functions and post change/ready events back through `window.cn1PostMessage`.
/// Returns the self-contained page used by the browser fallback.
abstract String createEditorHtml();

/// Base URL used when loading the editor page, allowing relative resources (such as a bundled code
/// editor library) to resolve. The default returns a synthetic origin.
/// Base URL for relative resources in the browser fallback page.
String getEditorBaseURL() {
return "https://cn1editor.codenameone.com/";
}

/// When non-null the browser fallback loads this app-hierarchy URL (via
/// `BrowserComponent#setURLHierarchy(String)`) as a custom editor engine instead of the built-in
/// `#createEditorHtml()` page. Subclasses override to allow an application to supply a richer editor
/// backend that speaks the same `window.cn1editor` bridge.
/// Optional app-hierarchy URL for a custom browser editor engine.
String getEngineURL() {
return null;
}
Expand Down Expand Up @@ -215,6 +220,7 @@ void onEditorEvent(String type, String value) {
/// - `type`: the event type
///
/// - `value`: optional payload, may be null
@Override
public void fireEditorEvent(final String type, final String value) {
if (CN.isEdt()) {
onEditorEvent(type, value);
Expand Down Expand Up @@ -248,6 +254,8 @@ public void run() {
}
if (nativeMode) {
Display.impl.editorPeerCommand(nativePeer, name, arg);
} else if (pureEditor != null) {
pureEditor.cmd(name, arg);
} else {
browser.execute("window.cn1editor.cmd(${0}, ${1})", new Object[]{name, arg == null ? "" : arg});
}
Expand Down Expand Up @@ -277,9 +285,12 @@ public void run() {
callback.onSucess(Display.impl.editorPeerQuery(nativePeer, name, arg));
return;
}
if (pureEditor != null) {
callback.onSucess(pureEditor.query(name, arg));
return;
}
browser.execute("callback.onSuccess(window.cn1editor.query(${0}, ${1}))",
new Object[]{name, arg == null ? "" : arg},
new JSRefStringCallback(callback));
new Object[]{name, arg == null ? "" : arg}, new JSRefStringCallback(callback));
}

private static final class JSRefStringCallback implements SuccessCallback<BrowserComponent.JSRef> {
Expand Down Expand Up @@ -314,14 +325,13 @@ public boolean isEditorReady() {
return ready;
}

/// True when a platform supplied native editor backend is in use, false when the cross platform
/// `BrowserComponent` fallback is active.
/// True when a platform supplied native editor backend is in use, false for the pure and browser
/// backends.
public boolean isNativeEditor() {
return nativeMode;
}

/// Returns the underlying `BrowserComponent` used by the fallback backend, or null when a native
/// backend is active. Exposed for advanced customization; most apps never need this.
/// Returns the browser fallback, or null when the pure or native backend is active.
public BrowserComponent getInternalBrowser() {
return browser;
}
Expand Down Expand Up @@ -397,4 +407,36 @@ public void focusEditor() {
public void blurEditor() {
command("blur", null);
}

// ---- EditorHost (pure backend bridge to the platform text input source) ----

/// {@inheritDoc}
@Override
public boolean isTextInputSupported() {
return Display.impl.isTextInputSupported();
}

/// {@inheritDoc}
@Override
public Object startTextInput(TextInputClient client, TextInputConfig config) {
return Display.impl.startTextInput(client, config);
}

/// {@inheritDoc}
@Override
public void updateTextInputState(Object handle, TextInputState state) {
Display.impl.updateTextInputState(handle, state);
}

/// {@inheritDoc}
@Override
public void stopTextInput(Object handle) {
Display.impl.stopTextInput(handle);
}

/// {@inheritDoc}
@Override
public void editorChanged() {
onEditorEvent("change", null);
}
}
2 changes: 1 addition & 1 deletion CodenameOne/src/com/codename1/ui/BrowserComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,7 @@ public void run() {
/// When a Dialog is shown, the parent Form is deinitialized, which normally causes
/// BrowserComponent's iframe to be removed from the DOM. When the Dialog is dismissed,
/// the Form is re-shown but the iframe must be recreated from scratch, losing all
/// JavaScript state (Monaco editor content, event listeners, etc.).
/// JavaScript state (editor content, event listeners, etc.).
///
/// Setting this to `Boolean.FALSE` preserves the iframe in the DOM across
/// deinitialization cycles, which is essential for:
Expand Down
Loading
Loading