diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 692b720cc4..e485c4441a 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -5114,6 +5114,59 @@ public PeerComponent createBrowserComponent(Object browserComponent) { return null; } + /// Creates a native peer for one of the visual editor components + /// (`com.codename1.ui.RichTextArea` / `com.codename1.ui.CodeEditor`). + /// + /// The default implementation returns null which makes the editor fall back to its 100% cross + /// platform `BrowserComponent` based backend. A platform port may override this to return a genuinely + /// native editing widget (e.g. a native rich text view or a native code editor) which is then driven + /// through `#editorPeerCommand(PeerComponent, String, String)` and + /// `#editorPeerQuery(PeerComponent, String, String)`. A native peer should deliver events back to the + /// owning editor by calling `editorComponent.fireEditorEvent(type, value)`. + /// + /// #### Parameters + /// + /// - `editorComponent`: the owning `AbstractEditorComponent` so native peers can fire events back + /// + /// - `editorType`: the editor flavor, currently `"richtext"` or `"code"` + /// + /// #### Returns + /// + /// a native editor peer, or null to use the cross platform fallback + public PeerComponent createNativeEditorPeer(Object editorComponent, String editorType) { + return null; + } + + /// Sends a one way semantic command to a native editor peer created by + /// `#createNativeEditorPeer(Object, String)`. No-op by default. + /// + /// #### Parameters + /// + /// - `peer`: the native editor peer + /// + /// - `name`: the semantic command name (e.g. `"setHtml"`, `"bold"`, `"setText"`) + /// + /// - `arg`: an optional string argument, may be null + public void editorPeerCommand(PeerComponent peer, String name, String arg) { + } + + /// Queries a native editor peer for a string value. Returns null by default. + /// + /// #### Parameters + /// + /// - `peer`: the native editor peer + /// + /// - `name`: the semantic query name (e.g. `"getHtml"`, `"getText"`) + /// + /// - `arg`: an optional string argument, may be null + /// + /// #### Returns + /// + /// the queried value or null + public String editorPeerQuery(PeerComponent peer, String name, String arg) { + return null; + } + /// 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 diff --git a/CodenameOne/src/com/codename1/ui/AbstractEditorComponent.java b/CodenameOne/src/com/codename1/ui/AbstractEditorComponent.java new file mode 100644 index 0000000000..fd99e3de83 --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/AbstractEditorComponent.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores + * CA 94065 USA or visit www.oracle.com if you need additional information or + * have any questions. + */ +package com.codename1.ui; + +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.util.EventDispatcher; +import com.codename1.util.SuccessCallback; + +import java.util.ArrayList; +import java.util.List; + +/// Base class for the native visual editors (rich text and code) introduced by Codename One. +/// +/// The editor components are designed around a *semantic command channel* rather than a single +/// hard-coded implementation. Every concrete editor (`RichTextArea`, `CodeEditor`) speaks to its +/// backend exclusively through `#command(String, String)`, `#query(String, String, SuccessCallback)` +/// 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 +/// `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. +/// +/// Both 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. + static final String MESSAGE_PREFIX = "cn1ed:"; + + private BrowserComponent browser; + private PeerComponent nativePeer; + private boolean nativeMode; + private boolean ready; + private boolean editable = true; + private final List readyQueue = new ArrayList(); + private final EventDispatcher changeListeners = new EventDispatcher(); + private final EventDispatcher readyListeners = new EventDispatcher(); + private final Label placeholder = new Label(""); + + /// Creates the editor and begins asynchronous backend initialization. + /// + /// #### Parameters + /// + /// - `uiid`: the UIID applied to the editor container + protected AbstractEditorComponent(String uiid) { + setUIID(uiid); + setLayout(new BorderLayout()); + placeholder.setShowEvenIfBlank(true); + addComponent(BorderLayout.CENTER, placeholder); + CN.callSerially(new Runnable() { + @Override + public void run() { + initBackend(); + } + }); + } + + private void initBackend() { + nativePeer = Display.impl.createNativeEditorPeer(this, getEditorType()); + if (nativePeer != null) { + nativeMode = true; + removeComponent(placeholder); + addComponent(BorderLayout.CENTER, nativePeer); + // the native peer signals readiness through onEditorEvent("ready", null); if a platform + // creates the peer fully initialized it may call that immediately, otherwise we mark ready + // here to flush queued commands as soon as the peer is attached. + markReady(); + revalidateLater(); + return; + } + nativeMode = false; + 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 + public void actionPerformed(ActionEvent evt) { + handleBrowserMessage((String) evt.getSource()); + } + }); + 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(); + } + }); + removeComponent(placeholder); + addComponent(BorderLayout.CENTER, browser); + String engineUrl = getEngineURL(); + if (engineUrl != null) { + try { + browser.setURLHierarchy(engineUrl); + } catch (java.io.IOException err) { + browser.setURL(engineUrl); + } + } else { + browser.setPage(createEditorHtml(), getEditorBaseURL()); + } + revalidateLater(); + } + + private void handleBrowserMessage(String msg) { + if (msg == null || !msg.startsWith(MESSAGE_PREFIX)) { + return; + } + 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); + } + + private void markReady() { + if (ready) { + return; + } + ready = true; + applyEditableState(); + List copy = new ArrayList(readyQueue); + readyQueue.clear(); + for (Runnable r : copy) { + r.run(); + } + readyListeners.fireActionEvent(new ActionEvent(this)); + } + + /// 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`. + 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. + 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. + String getEngineURL() { + return null; + } + + /// Inbound event dispatch from either backend. Subclasses override to react to editor side events + /// (content changes, selection changes, completion requests, ...). Always call + /// `super.onEditorEvent` for the shared `"change"` and `"ready"` handling. + /// + /// #### Parameters + /// + /// - `type`: the event type, e.g. `"change"`, `"ready"`, `"selection"` + /// + /// - `value`: an optional string payload, may be null + void onEditorEvent(String type, String value) { + if ("ready".equals(type)) { + markReady(); + return; + } + if ("change".equals(type)) { + changeListeners.fireActionEvent(new ActionEvent(this)); + } + } + + /// Entry point invoked by native editor peers to deliver events back to Codename One. This routes + /// to the same dispatch path used by the browser message bridge so subclasses handle events + /// uniformly regardless of backend. + /// + /// #### Parameters + /// + /// - `type`: the event type + /// + /// - `value`: optional payload, may be null + public void fireEditorEvent(final String type, final String value) { + if (CN.isEdt()) { + onEditorEvent(type, value); + } else { + CN.callSerially(new Runnable() { + @Override + public void run() { + onEditorEvent(type, value); + } + }); + } + } + + /// Sends a one way command to the active backend. If the backend is not ready yet the command is + /// queued and replayed once initialization completes. + /// + /// #### Parameters + /// + /// - `name`: the semantic command name understood by both backends + /// + /// - `arg`: an optional string argument, may be null + protected void command(final String name, final String arg) { + if (!ready) { + readyQueue.add(new Runnable() { + @Override + public void run() { + command(name, arg); + } + }); + return; + } + if (nativeMode) { + Display.impl.editorPeerCommand(nativePeer, name, arg); + } else { + browser.execute("window.cn1editor.cmd(${0}, ${1})", new Object[]{name, arg == null ? "" : arg}); + } + } + + /// Queries the active backend for a string value asynchronously. The callback is always invoked on + /// the EDT. If the backend is not ready the query is deferred until it is. + /// + /// #### Parameters + /// + /// - `name`: the semantic query name understood by both backends + /// + /// - `arg`: an optional string argument, may be null + /// + /// - `callback`: receives the query result + protected void query(final String name, final String arg, final SuccessCallback callback) { + if (!ready) { + readyQueue.add(new Runnable() { + @Override + public void run() { + query(name, arg, callback); + } + }); + return; + } + if (nativeMode) { + callback.onSucess(Display.impl.editorPeerQuery(nativePeer, name, arg)); + return; + } + browser.execute("callback.onSuccess(window.cn1editor.query(${0}, ${1}))", + new Object[]{name, arg == null ? "" : arg}, + new JSRefStringCallback(callback)); + } + + private static final class JSRefStringCallback implements SuccessCallback { + private final SuccessCallback delegate; + + JSRefStringCallback(SuccessCallback delegate) { + this.delegate = delegate; + } + + @Override + public void onSucess(BrowserComponent.JSRef value) { + delegate.onSucess(value == null ? null : value.getValue()); + } + } + + /// Runs the supplied task once the editor backend is ready, or immediately if it already is. + /// + /// #### Parameters + /// + /// - `r`: the task to run on the EDT when the editor is ready + public void onReady(final Runnable r) { + if (ready) { + r.run(); + } else { + readyQueue.add(r); + } + } + + /// Returns true once the underlying editor backend has finished initializing and is ready to accept + /// commands. + 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. + 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. + public BrowserComponent getInternalBrowser() { + return browser; + } + + /// Adds a listener notified whenever the editor content changes. + /// + /// #### Parameters + /// + /// - `l`: the change listener + public void addChangeListener(ActionListener l) { + changeListeners.addListener(l); + } + + /// Removes a previously registered change listener. + /// + /// #### Parameters + /// + /// - `l`: the change listener + public void removeChangeListener(ActionListener l) { + changeListeners.removeListener(l); + } + + /// Adds a listener notified once when the editor backend becomes ready. + /// + /// #### Parameters + /// + /// - `l`: the ready listener + public void addReadyListener(ActionListener l) { + if (ready) { + l.actionPerformed(new ActionEvent(this)); + } else { + readyListeners.addListener(l); + } + } + + /// Removes a previously registered ready listener. + /// + /// #### Parameters + /// + /// - `l`: the ready listener + public void removeReadyListener(ActionListener l) { + readyListeners.removeListener(l); + } + + /// Enables or disables editing. A disabled editor still displays content but rejects input. + /// + /// #### Parameters + /// + /// - `editable`: true to allow editing + public void setEditable(boolean editable) { + this.editable = editable; + if (ready) { + applyEditableState(); + } + } + + private void applyEditableState() { + command("setEditable", editable ? "1" : "0"); + } + + /// Returns true if the editor currently allows editing. + @Override + public boolean isEditable() { + return editable; + } + + /// Requests keyboard focus for the editing surface, showing the virtual keyboard on touch devices. + public void focusEditor() { + command("focus", null); + } + + /// Removes keyboard focus from the editing surface, hiding the virtual keyboard on touch devices. + public void blurEditor() { + command("blur", null); + } +} diff --git a/CodenameOne/src/com/codename1/ui/CodeCompletion.java b/CodenameOne/src/com/codename1/ui/CodeCompletion.java new file mode 100644 index 0000000000..823c45f1fa --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/CodeCompletion.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores + * CA 94065 USA or visit www.oracle.com if you need additional information or + * have any questions. + */ +package com.codename1.ui; + +/// A single code completion proposal returned by a `CodeCompletionProvider` and shown in the +/// `CodeEditor` completion popup. +/// +/// @author Shai Almog +public class CodeCompletion { + private final String displayText; + private final String insertText; + private String type; + private String detail; + + /// Creates a completion whose displayed and inserted text are identical. + /// + /// #### Parameters + /// + /// - `text`: the text shown in the popup and inserted when chosen + public CodeCompletion(String text) { + this(text, text); + } + + /// Creates a completion with distinct display and insertion text. + /// + /// #### Parameters + /// + /// - `displayText`: the text shown in the completion popup + /// + /// - `insertText`: the text inserted into the editor when this completion is chosen + public CodeCompletion(String displayText, String insertText) { + this.displayText = displayText; + this.insertText = insertText; + } + + /// The text shown to the user in the completion popup. + public String getDisplayText() { + return displayText; + } + + /// The text inserted into the editor when this completion is accepted. + public String getInsertText() { + return insertText; + } + + /// An optional category used to badge / icon the entry, e.g. `"method"`, `"keyword"`, + /// `"variable"`, `"class"`, `"snippet"`. + public String getType() { + return type; + } + + /// Sets the completion category. Returns this for chaining. + /// + /// #### Parameters + /// + /// - `type`: the completion category + public CodeCompletion setType(String type) { + this.type = type; + return this; + } + + /// Optional secondary detail (e.g. a method signature or return type) shown alongside the entry. + public String getDetail() { + return detail; + } + + /// Sets the optional secondary detail. Returns this for chaining. + /// + /// #### Parameters + /// + /// - `detail`: the detail string + public CodeCompletion setDetail(String detail) { + this.detail = detail; + return this; + } +} diff --git a/CodenameOne/src/com/codename1/ui/CodeCompletionProvider.java b/CodenameOne/src/com/codename1/ui/CodeCompletionProvider.java new file mode 100644 index 0000000000..036804c71f --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/CodeCompletionProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores + * CA 94065 USA or visit www.oracle.com if you need additional information or + * have any questions. + */ +package com.codename1.ui; + +import com.codename1.util.SuccessCallback; + +import java.util.List; + +/// Supplies IDE style code completion proposals to a `CodeEditor`. +/// +/// The provider is invoked by the editor as the user types (or when completion is explicitly triggered) +/// and returns its proposals asynchronously, which makes it suitable both for fast in-memory completion +/// and for completion driven by a remote language server. +/// +/// #### Example +/// +/// ```java +/// editor.setCompletionProvider((ed, code, cursor, results) -> { +/// String prefix = currentWord(code, cursor); +/// List out = new ArrayList<>(); +/// for (String kw : KEYWORDS) { +/// if (kw.startsWith(prefix)) { +/// out.add(new CodeCompletion(kw).setType("keyword")); +/// } +/// } +/// results.onSucess(out); +/// }); +/// ``` +/// +/// @author Shai Almog +public interface CodeCompletionProvider { + /// Requests completion proposals for the given editor state. Implementations must eventually invoke + /// `results.onSucess(list)` (possibly asynchronously); passing an empty list or null hides the + /// completion popup. + /// + /// #### Parameters + /// + /// - `editor`: the editor requesting completions + /// + /// - `code`: the full editor text + /// + /// - `cursorPosition`: the character offset of the caret within `code` + /// + /// - `results`: callback to deliver the proposals + void getCompletions(CodeEditor editor, String code, int cursorPosition, SuccessCallback> results); +} diff --git a/CodenameOne/src/com/codename1/ui/CodeDiagnostic.java b/CodenameOne/src/com/codename1/ui/CodeDiagnostic.java new file mode 100644 index 0000000000..db6cc634e9 --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/CodeDiagnostic.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores + * CA 94065 USA or visit www.oracle.com if you need additional information or + * have any questions. + */ +package com.codename1.ui; + +/// A diagnostic (error / warning / information) shown in a `CodeEditor` as a squiggly underline over a +/// source range, a marker in the line-number gutter and a tooltip carrying the message. +/// +/// Positions are 1-based: line `1` is the first line and column `1` is the first character of a line, +/// matching the convention used by most compilers and language servers. +/// +/// @author Shai Almog +public class CodeDiagnostic { + /// Severity for a problem that should block / is an error. + public static final String ERROR = "error"; + /// Severity for a non-blocking warning. + public static final String WARNING = "warning"; + /// Severity for purely informational hints. + public static final String INFO = "info"; + + private final int line; + private final int column; + private int endLine; + private int endColumn; + private String severity = ERROR; + private final String message; + + /// Creates a single-position diagnostic that underlines from the given column to the end of the + /// token / line. + /// + /// #### Parameters + /// + /// - `line`: the 1-based line + /// + /// - `column`: the 1-based start column + /// + /// - `message`: the human readable message + public CodeDiagnostic(int line, int column, String message) { + this(line, column, line, column, message); + } + + /// Creates a diagnostic spanning an explicit range. + /// + /// #### Parameters + /// + /// - `line`: the 1-based start line + /// + /// - `column`: the 1-based start column + /// + /// - `endLine`: the 1-based end line + /// + /// - `endColumn`: the 1-based end column (exclusive) + /// + /// - `message`: the human readable message + public CodeDiagnostic(int line, int column, int endLine, int endColumn, String message) { + this.line = line; + this.column = column; + this.endLine = endLine; + this.endColumn = endColumn; + this.message = message; + } + + /// The 1-based start line. + public int getLine() { + return line; + } + + /// The 1-based start column. + public int getColumn() { + return column; + } + + /// The 1-based end line. + public int getEndLine() { + return endLine; + } + + /// The 1-based end column (exclusive). + public int getEndColumn() { + return endColumn; + } + + /// The severity, one of `#ERROR`, `#WARNING` or `#INFO`. + public String getSeverity() { + return severity; + } + + /// Sets the severity. Returns this for chaining. + /// + /// #### Parameters + /// + /// - `severity`: one of `#ERROR`, `#WARNING`, `#INFO` + public CodeDiagnostic setSeverity(String severity) { + this.severity = severity == null ? ERROR : severity; + return this; + } + + /// The message shown in the tooltip. + public String getMessage() { + return message; + } +} diff --git a/CodenameOne/src/com/codename1/ui/CodeEditor.java b/CodenameOne/src/com/codename1/ui/CodeEditor.java new file mode 100644 index 0000000000..ebdc3a7797 --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/CodeEditor.java @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores + * CA 94065 USA or visit www.oracle.com if you need additional information or + * have any questions. + */ +package com.codename1.ui; + +import com.codename1.util.SuccessCallback; + +import java.util.List; + +/// An IDE style source code editor with syntax highlighting, a line-number gutter and asynchronous +/// code completion. +/// +/// `CodeEditor` is built for editing source code on both touch devices (with the virtual keyboard) and +/// desktops (with a physical keyboard). Code completion is driven by a `CodeCompletionProvider`, which +/// can resolve proposals locally or from a remote language server. +/// +/// Like `RichTextArea`, the editor uses a 100% cross platform `BrowserComponent` backend by default and +/// can be transparently upgraded to a native code editor by a platform port. The built-in editing +/// surface is fully self contained and offline. When the optional bundled CodeMirror library is present +/// (the Codename One build server adds it automatically when, and only when, an app actually uses +/// `CodeEditor`) the editor upgrades to CodeMirror for richer language support; otherwise it uses a +/// lightweight built-in highlighter. +/// +/// #### Example +/// +/// ```java +/// CodeEditor editor = new CodeEditor(); +/// editor.setLanguage("java"); +/// editor.setText("public class Main {\n\n}"); +/// editor.setCompletionProvider((ed, code, cursor, results) -> { +/// List out = new ArrayList<>(); +/// out.add(new CodeCompletion("System.out.println(", "System.out.println()").setType("method")); +/// results.onSucess(out); +/// }); +/// form.add(BorderLayout.CENTER, editor); +/// ``` +/// +/// @author Shai Almog +public class CodeEditor extends AbstractEditorComponent { + private String language = "text"; + private String theme = "light"; + private boolean showLineNumbers = true; + private int tabSize = 4; + private String engineUrl; + private CodeCompletionProvider completionProvider; + + /// Creates an empty code editor. + public CodeEditor() { + super("CodeEditor"); + } + + /// Creates a code editor initialized with the supplied source and language. + /// + /// #### Parameters + /// + /// - `language`: the language id used for syntax highlighting (e.g. `"java"`, `"javascript"`, + /// `"kotlin"`, `"css"`, `"xml"`, `"json"`, `"python"`) + /// + /// - `text`: the initial source code + public CodeEditor(String language, String text) { + this(); + setLanguage(language); + setText(text); + } + + @Override + String getEditorType() { + return "code"; + } + + /// Replaces the entire editor content. + /// + /// #### Parameters + /// + /// - `text`: the source code + public void setText(String text) { + command("setText", text == null ? "" : text); + } + + /// Retrieves the current source code. The callback is invoked on the EDT. + /// + /// #### Parameters + /// + /// - `callback`: receives the source code + public void getText(SuccessCallback callback) { + query("getText", null, callback); + } + + /// Sets the language used for syntax highlighting. + /// + /// #### Parameters + /// + /// - `language`: the language id (e.g. `"java"`, `"javascript"`, `"kotlin"`, `"css"`, `"xml"`, + /// `"json"`, `"python"`) + public void setLanguage(String language) { + this.language = language == null ? "text" : language; + command("setLanguage", this.language); + } + + /// Returns the current highlighting language id. + public String getLanguage() { + return language; + } + + /// Sets the color theme. Currently `"light"` and `"dark"` are supported. + /// + /// #### Parameters + /// + /// - `theme`: the theme id + public void setTheme(String theme) { + this.theme = theme == null ? "light" : theme; + command("setTheme", this.theme); + } + + /// Returns the current theme id. + public String getTheme() { + return theme; + } + + /// Shows or hides the line-number gutter. + /// + /// #### Parameters + /// + /// - `show`: true to show line numbers + public void setShowLineNumbers(boolean show) { + this.showLineNumbers = show; + command("setLineNumbers", show ? "1" : "0"); + } + + /// Returns true if the line-number gutter is shown. + public boolean isShowLineNumbers() { + return showLineNumbers; + } + + /// Sets the number of spaces inserted for a tab / used for indentation. + /// + /// #### Parameters + /// + /// - `tabSize`: the indentation width in spaces + public void setTabSize(int tabSize) { + this.tabSize = tabSize; + command("setTabSize", String.valueOf(tabSize)); + } + + /// Returns the indentation width in spaces. + public int getTabSize() { + return tabSize; + } + + /// Makes the editor read-only or editable. This is a convenience around + /// `AbstractEditorComponent#setEditable(boolean)`. + /// + /// #### Parameters + /// + /// - `readOnly`: true to prevent editing + public void setReadOnly(boolean readOnly) { + setEditable(!readOnly); + } + + /// Returns true when the editor is read-only. + public boolean isReadOnly() { + return !isEditable(); + } + + /// Inserts text at the current caret position, replacing any active selection. + /// + /// #### Parameters + /// + /// - `text`: the text to insert + public void insertAtCursor(String text) { + command("insertText", text); + } + + /// Retrieves the current caret character offset. The callback is invoked on the EDT. + /// + /// #### Parameters + /// + /// - `callback`: receives the caret offset as an Integer + public void getCursorPosition(final SuccessCallback callback) { + query("getCursor", null, new StringToIntCallback(callback)); + } + + private static final class StringToIntCallback implements SuccessCallback { + private final SuccessCallback delegate; + + StringToIntCallback(SuccessCallback delegate) { + this.delegate = delegate; + } + + @Override + public void onSucess(String value) { + int v = 0; + try { + if (value != null && value.length() > 0) { + v = Integer.parseInt(value.trim()); + } + } catch (NumberFormatException err) { + v = 0; + } + delegate.onSucess(Integer.valueOf(v)); + } + } + + /// Sets the diagnostics (errors / warnings / hints) displayed in the editor as squiggly underlines, + /// gutter markers and tooltips. Pass an empty list (or null) to clear all diagnostics. + /// + /// #### Parameters + /// + /// - `diagnostics`: the diagnostics to display + public void setDiagnostics(List diagnostics) { + command("setDiagnostics", diagnosticsJson(diagnostics)); + } + + private static String diagnosticsJson(List diagnostics) { + StringBuilder sb = new StringBuilder("["); + if (diagnostics != null) { + for (CodeDiagnostic d : diagnostics) { + if (d == null) { + continue; + } + if (sb.length() > 1) { + sb.append(","); + } + sb.append("{\"l\":").append(d.getLine()) + .append(",\"c\":").append(d.getColumn()) + .append(",\"el\":").append(d.getEndLine()) + .append(",\"ec\":").append(d.getEndColumn()) + .append(",\"s\":\"").append(jsonEscape(d.getSeverity())) + .append("\",\"m\":\"").append(jsonEscape(d.getMessage())) + .append("\"}"); + } + } + sb.append("]"); + return sb.toString(); + } + + /// Points the editor at a custom engine page bundled in the app's HTML hierarchy (loaded with + /// `BrowserComponent#setURLHierarchy(String)`) instead of the self contained built-in engine. The + /// custom page must implement the same `window.cn1editor` command/query bridge and post the same + /// `cn1ed:` events, which lets an application back `CodeEditor` with a richer editor (e.g. CodeMirror + /// or Monaco) when maximum polish is required while keeping the exact same Java API. Must be set + /// before the editor is shown. + /// + /// #### Parameters + /// + /// - `url`: an app-hierarchy URL such as `"/my-editor/index.html"`, or null to use the built-in engine + public void setEngineURL(String url) { + this.engineUrl = url; + } + + /// Returns the custom engine URL set with `#setEngineURL(String)`, or null when the built-in engine + /// is used. + @Override + public String getEngineURL() { + return engineUrl; + } + + String getEngineURLInternal() { + return engineUrl; + } + + /// Sets the provider that supplies code completion proposals. Passing null disables completion. + /// + /// #### Parameters + /// + /// - `provider`: the completion provider, or null to disable completion + public void setCompletionProvider(CodeCompletionProvider provider) { + this.completionProvider = provider; + command("setCompletionEnabled", provider != null ? "1" : "0"); + } + + /// Returns the current completion provider, or null if none is set. + public CodeCompletionProvider getCompletionProvider() { + return completionProvider; + } + + @Override + void onEditorEvent(String type, String value) { + if ("complete".equals(type)) { + handleCompletionRequest(value); + return; + } + super.onEditorEvent(type, value); + } + + private void handleCompletionRequest(String value) { + final CodeCompletionProvider provider = completionProvider; + if (provider == null || value == null) { + return; + } + int colon = value.indexOf(':'); + if (colon < 0) { + return; + } + final String reqId = value.substring(0, colon); + final int cursor; + int c; + try { + c = Integer.parseInt(value.substring(colon + 1).trim()); + } catch (NumberFormatException err) { + c = 0; + } + cursor = c; + getText(new SuccessCallback() { + @Override + public void onSucess(String code) { + final String safeCode = code == null ? "" : code; + provider.getCompletions(CodeEditor.this, safeCode, cursor, new SuccessCallback>() { + @Override + public void onSucess(List results) { + command("showCompletions", reqId + ":" + toJson(results)); + } + }); + } + }); + } + + private static String toJson(List items) { + StringBuilder sb = new StringBuilder("["); + if (items != null) { + for (CodeCompletion cc : items) { + if (cc == null) { + continue; + } + if (sb.length() > 1) { + sb.append(","); + } + sb.append("{\"d\":\"").append(jsonEscape(cc.getDisplayText())) + .append("\",\"i\":\"").append(jsonEscape(cc.getInsertText())) + .append("\",\"t\":\"").append(jsonEscape(cc.getType())) + .append("\",\"x\":\"").append(jsonEscape(cc.getDetail())) + .append("\"}"); + } + } + sb.append("]"); + return sb.toString(); + } + + private static String jsonEscape(String s) { + if (s == null) { + return ""; + } + StringBuilder sb = new StringBuilder(s.length() + 8); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + switch (ch) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + if (ch < 0x20) { + String hex = Integer.toHexString(ch); + sb.append("\\u"); + for (int p = hex.length(); p < 4; p++) { + sb.append('0'); + } + sb.append(hex); + } else { + sb.append(ch); + } + break; + } + } + return sb.toString(); + } + + @Override + String createEditorHtml() { + return CodeEditorHtml.PAGE; + } +} diff --git a/CodenameOne/src/com/codename1/ui/CodeEditorHtml.java b/CodenameOne/src/com/codename1/ui/CodeEditorHtml.java new file mode 100644 index 0000000000..8d433651c4 --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/CodeEditorHtml.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores + * CA 94065 USA or visit www.oracle.com if you need additional information or + * have any questions. + */ +package com.codename1.ui; + +/// Holds the self contained HTML/JS implementation of the `CodeEditor` built-in editing surface. Kept +/// in its own class so the large literal does not clutter the editor component. The page exposes the +/// `window.cn1editor` command/query bridge used by `AbstractEditorComponent` and supports syntax +/// highlighting, a line-number gutter, code completion, diagnostics (squiggly underlines + gutter +/// markers + tooltips), bracket auto-close and an active-line highlight. +final class CodeEditorHtml { + private CodeEditorHtml() { + } + + // Assigned in a static initializer (not at the declaration) so this large string is a runtime + // constant rather than a compile-time constant - that keeps the compiler from inlining the whole + // ~13KB literal into every class that references CodeEditorHtml.PAGE. + static final String PAGE; + + static { + PAGE = + "" + + "" + + "" + + "" + + "
1
" + + "
" + + ""; + } +} diff --git a/CodenameOne/src/com/codename1/ui/RichTextArea.java b/CodenameOne/src/com/codename1/ui/RichTextArea.java new file mode 100644 index 0000000000..c08c6be561 --- /dev/null +++ b/CodenameOne/src/com/codename1/ui/RichTextArea.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores + * CA 94065 USA or visit www.oracle.com if you need additional information or + * have any questions. + */ +package com.codename1.ui; + +import com.codename1.util.SuccessCallback; + +/// A native visual editor for rich text / HTML content (a WYSIWYG editor). +/// +/// `RichTextArea` lets the user visually edit formatted text - bold, italic, lists, links, colors, +/// headings and more - and exchange the result as HTML with your application. It works on phones and +/// tablets (with the on screen virtual keyboard) as well as on desktops (with a physical keyboard). +/// +/// By default the editor is implemented as a `contenteditable` surface hosted inside the platform's +/// native web widget (`BrowserComponent`), which makes it 100% cross platform and gives correct +/// keyboard, selection and IME behavior on every device for free. A platform port may transparently +/// replace this with a fully native editing widget; see +/// `com.codename1.impl.CodenameOneImplementation#createNativeEditorPeer(Object, String)`. +/// +/// #### Basic usage +/// +/// ```java +/// Form hi = new Form("Rich Text", new BorderLayout()); +/// RichTextArea editor = new RichTextArea(); +/// editor.setHtml("

Hello world

"); +/// +/// Toolbar tb = hi.getToolbar(); +/// tb.addCommandToRightBar("B", null, e -> editor.bold()); +/// tb.addCommandToRightBar("I", null, e -> editor.italic()); +/// +/// hi.add(BorderLayout.CENTER, editor); +/// hi.show(); +/// +/// // later, read the edited content back: +/// editor.getHtml(html -> Log.p("User wrote: " + html)); +/// ``` +/// +/// @author Shai Almog +public class RichTextArea extends AbstractEditorComponent { + private String placeholderText = ""; + + /// Creates an empty rich text editor. + public RichTextArea() { + super("RichTextArea"); + } + + /// Creates a rich text editor initialized with the supplied HTML. + /// + /// #### Parameters + /// + /// - `html`: the initial HTML content + public RichTextArea(String html) { + this(); + setHtml(html); + } + + @Override + String getEditorType() { + return "richtext"; + } + + /// Replaces the entire editor content with the supplied HTML. + /// + /// #### Parameters + /// + /// - `html`: the HTML to display and edit + public void setHtml(String html) { + command("setHtml", html == null ? "" : html); + } + + /// Retrieves the current editor content as an HTML string. The callback is invoked on the EDT. + /// + /// #### Parameters + /// + /// - `callback`: receives the HTML content + public void getHtml(SuccessCallback callback) { + query("getHtml", null, callback); + } + + /// Retrieves the current editor content as plain text (markup stripped). The callback is invoked on + /// the EDT. + /// + /// #### Parameters + /// + /// - `callback`: receives the plain text content + public void getText(SuccessCallback callback) { + query("getText", null, callback); + } + + /// Inserts the supplied HTML fragment at the current cursor position. + /// + /// #### Parameters + /// + /// - `html`: the HTML fragment to insert + public void insertHtml(String html) { + command("insertHtml", html); + } + + /// Inserts an image at the current cursor position. + /// + /// #### Parameters + /// + /// - `url`: the image URL (may be an http(s) URL or a data: URI) + public void insertImage(String url) { + command("insertImage", url); + } + + /// Sets the placeholder text shown when the editor is empty. + /// + /// #### Parameters + /// + /// - `text`: the placeholder hint + public void setPlaceholder(String text) { + placeholderText = text == null ? "" : text; + command("setPlaceholder", placeholderText); + } + + /// Returns the current placeholder text. + public String getPlaceholder() { + return placeholderText; + } + + /// Toggles bold styling on the current selection. + public void bold() { + command("bold", null); + } + + /// Toggles italic styling on the current selection. + public void italic() { + command("italic", null); + } + + /// Toggles underline styling on the current selection. + public void underline() { + command("underline", null); + } + + /// Toggles strike-through styling on the current selection. + public void strikeThrough() { + command("strikeThrough", null); + } + + /// Converts the current block(s) into an ordered (numbered) list. + public void insertOrderedList() { + command("insertOrderedList", null); + } + + /// Converts the current block(s) into an unordered (bulleted) list. + public void insertUnorderedList() { + command("insertUnorderedList", null); + } + + /// Increases the indentation of the current block. + public void indent() { + command("indent", null); + } + + /// Decreases the indentation of the current block. + public void outdent() { + command("outdent", null); + } + + /// Left aligns the current block. + public void justifyLeft() { + command("justifyLeft", null); + } + + /// Center aligns the current block. + public void justifyCenter() { + command("justifyCenter", null); + } + + /// Right aligns the current block. + public void justifyRight() { + command("justifyRight", null); + } + + /// Wraps the current selection in a hyperlink. + /// + /// #### Parameters + /// + /// - `url`: the link target + public void createLink(String url) { + command("createLink", url); + } + + /// Removes the hyperlink covering the current selection. + public void removeLink() { + command("unlink", null); + } + + /// Sets the foreground (text) color of the current selection. + /// + /// #### Parameters + /// + /// - `rgb`: the color as a 0xRRGGBB integer + public void setForegroundColor(int rgb) { + command("foreColor", toCss(rgb)); + } + + /// Sets the highlight (background) color of the current selection. + /// + /// #### Parameters + /// + /// - `rgb`: the color as a 0xRRGGBB integer + public void setHighlightColor(int rgb) { + command("hiliteColor", toCss(rgb)); + } + + /// Applies a block format / heading to the current block. Common values are `"p"`, `"h1"` .. + /// `"h6"`, `"pre"` and `"blockquote"`. + /// + /// #### Parameters + /// + /// - `tag`: the block tag name + public void setBlockFormat(String tag) { + command("formatBlock", tag); + } + + /// Applies a relative font size (1 through 7, matching the legacy HTML font size scale) to the + /// current selection. + /// + /// #### Parameters + /// + /// - `size`: a value between 1 and 7 + public void setFontSize(int size) { + command("fontSize", String.valueOf(size)); + } + + /// Removes all inline formatting from the current selection. + public void removeFormat() { + command("removeFormat", null); + } + + /// Undoes the last editing operation. + public void undo() { + command("undo", null); + } + + /// Redoes the last undone editing operation. + public void redo() { + command("redo", null); + } + + /// Queries whether a given inline command (e.g. `"bold"`, `"italic"`, `"underline"`) is currently + /// active for the selection, which is useful to keep a formatting toolbar in sync. The callback is + /// invoked on the EDT. + /// + /// #### Parameters + /// + /// - `command`: the command name to test + /// + /// - `callback`: receives true if the command is active for the current selection + public void queryCommandState(String command, final SuccessCallback callback) { + query("state", command, new StringToBoolCallback(callback)); + } + + private static final class StringToBoolCallback implements SuccessCallback { + private final SuccessCallback delegate; + + StringToBoolCallback(SuccessCallback delegate) { + this.delegate = delegate; + } + + @Override + public void onSucess(String value) { + delegate.onSucess("1".equals(value)); + } + } + + private static String toCss(int rgb) { + String s = Integer.toHexString(rgb & 0xffffff); + while (s.length() < 6) { + s = "0" + s; + } + return "#" + s; + } + + @Override + String createEditorHtml() { + // A self-contained contenteditable editor. No external resources are required which keeps the + // editor fully functional offline and adds zero footprint to apps that do not use it. + return "" + + "" + + "" + + "" + + "
" + + ""; + } +} diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h index 2384a41844..3e4cbde915 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h @@ -43,6 +43,7 @@ #import //#define CN1_USE_STOREKIT //#define CN1_USE_APPREVIEW +//#define CN1_USE_CODEMIRROR #if defined(CN1_USE_STOREKIT) || defined(CN1_USE_APPREVIEW) #import "StoreKit/StoreKit.h" #endif diff --git a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc index 11fb728f0c..da58d481f3 100644 --- a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc +++ b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc @@ -3476,6 +3476,97 @@ The effort to integrate Cordova/PhoneGap support into Codename One is handled wi This is discussed further in the https://www.codenameone.com/blog/phonegap-cordova-compatibility-for-codename-one.html[original announcement]. +=== RichTextArea + +The https://www.codenameone.com/javadoc/com/codename1/ui/RichTextArea.html[RichTextArea] is a visual editor for formatted text (a WYSIWYG / HTML editor). It lets the user select text and apply styling - bold, italic, underline, lists, links, colors, headings - and exchanges the result with your application as HTML. It works on phones and tablets with the on-screen virtual keyboard, and on desktops with a physical keyboard. + +`RichTextArea` derives from `AbstractEditorComponent`. By default the editing surface is a `contenteditable` document hosted inside the platform's native web widget (the same one used by `BrowserComponent`), which makes it fully cross-platform and gives correct keyboard, selection and IME behavior on every device. A platform port may transparently replace this with a fully native editing widget without any change to your code. + +NOTE: Because the editor is created asynchronously you should treat it like `BrowserComponent`: setters such as `setHtml(String)` are safe to call immediately (they're queued until the editor is ready) while reads such as `getHtml(SuccessCallback)` are asynchronous and deliver their result through a callback. + +The following sample places a `RichTextArea` in a form and wires a couple of formatting commands into the `Toolbar`: + +[source,java] +---- +Form hi = new Form("Compose", new BorderLayout()); +RichTextArea editor = new RichTextArea(); +editor.setPlaceholder("Write something..."); +editor.setHtml("

Trip itinerary

Meet at the main lobby.

"); + +Toolbar tb = hi.getToolbar(); +tb.addCommandToRightBar("B", null, e -> editor.bold()); +tb.addCommandToRightBar("I", null, e -> editor.italic()); +tb.addCommandToRightBar("List", null, e -> editor.insertUnorderedList()); +tb.addCommandToRightBar("Save", null, e -> + editor.getHtml(html -> Log.p("User wrote: " + html))); + +hi.add(BorderLayout.CENTER, editor); +hi.show(); +---- + +.RichTextArea with a formatting toolbar editing styled content +image::img/components-richtextarea.png[RichTextArea with a formatting toolbar editing styled content,scaledwidth=35%] + +The formatting commands map to the operations you would expect from a word processor and all act on the current selection: `bold()`, `italic()`, `underline()`, `strikeThrough()`, `insertOrderedList()`, `insertUnorderedList()`, `indent()`/`outdent()`, `justifyLeft()`/`justifyCenter()`/`justifyRight()`, `createLink(url)`/`removeLink()`, `setForegroundColor(rgb)`/`setHighlightColor(rgb)`, `setBlockFormat("h1")`, `setFontSize(1..7)`, `removeFormat()`, `undo()` and `redo()`. You can also insert content at the cursor with `insertHtml(String)` and `insertImage(String)`. + +TIP: To keep a formatting toolbar in sync with the selection (for example, to highlight the bold button when the caret is inside bold text) register a change listener with `addChangeListener(ActionListener)` and query the active state with `queryCommandState("bold", active -> ...)`. + +Reading the edited content is asynchronous: + +[source,java] +---- +editor.getHtml(html -> storage.save(html)); // full HTML markup +editor.getText(text -> index(text)); // plain text, markup stripped +---- + +=== CodeEditor + +The https://www.codenameone.com/javadoc/com/codename1/ui/CodeEditor.html[CodeEditor] is an IDE-style source editor with syntax highlighting, a line-number gutter, light/dark themes and asynchronous code completion. Like `RichTextArea` it's designed for both touch devices (with the virtual keyboard) and desktops (with a physical keyboard), and uses the same cross-platform editing backend with an optional native upgrade per platform. + +[source,java] +---- +Form hi = new Form("Editor", new BorderLayout()); +CodeEditor editor = new CodeEditor(); +editor.setLanguage("java"); +editor.setTheme("light"); // or "dark" +editor.setShowLineNumbers(true); +editor.setText("public class Main {\n\n}"); +hi.add(BorderLayout.CENTER, editor); +hi.show(); +---- + +The highlighting language is selected with `setLanguage(String)` and understands common languages such as `java`, `kotlin`, `javascript`, `python`, `css`, `xml`, `json` and `c`. The content is exchanged with `setText(String)` and the asynchronous `getText(SuccessCallback)`, and `setReadOnly(boolean)` turns the editor into a syntax-highlighted code viewer. + +.CodeEditor showing Java with the completion popup (light theme) +image::img/components-codeeditor.png[CodeEditor showing Java with the completion popup,scaledwidth=35%] + +==== Code completion + +Code completion is driven by a https://www.codenameone.com/javadoc/com/codename1/ui/CodeCompletionProvider.html[CodeCompletionProvider]. The editor calls the provider as the user types (or when completion is explicitly triggered), passing the full text and the caret offset; the provider returns its proposals asynchronously, which makes it suitable both for fast in-memory completion and for completion backed by a remote language server. + +[source,java] +---- +editor.setCompletionProvider((ed, code, cursor, results) -> { + String prefix = currentWord(code, cursor); + List out = new ArrayList<>(); + for (String member : new String[] {"println(", "print(", "printf(", "flush()"}) { + if (member.startsWith(prefix)) { + out.add(new CodeCompletion(member).setType("method")); + } + } + results.onSucess(out); // an empty list hides the popup +}); +---- + +Each https://www.codenameone.com/javadoc/com/codename1/ui/CodeCompletion.html[CodeCompletion] carries the text shown in the popup, the text inserted when it's chosen (which may differ), an optional category used to badge the entry (`"method"`, `"keyword"`, `"class"`, ...) and an optional detail string such as a signature. + +The editor honors light and dark themes through `setTheme(String)`: + +.CodeEditor in the dark theme +image::img/components-codeeditor-dark.png[CodeEditor in the dark theme,scaledwidth=35%] + +TIP: The built-in editing surface is fully self contained and works offline with no external dependencies. When an app actually references `CodeEditor` the build server can additionally bundle a richer code editor library; apps that don't use `CodeEditor` never pay that cost. + === AutoCompleteTextField The https://www.codenameone.com/javadoc/com/codename1/ui/AutoCompleteTextField.html[AutoCompleteTextField] allows you to write text into a text field and select a completion entry from the list in a similar way to a search engine. diff --git a/docs/developer-guide/img/components-codeeditor-dark.png b/docs/developer-guide/img/components-codeeditor-dark.png new file mode 100644 index 0000000000..a42257351e Binary files /dev/null and b/docs/developer-guide/img/components-codeeditor-dark.png differ diff --git a/docs/developer-guide/img/components-codeeditor.png b/docs/developer-guide/img/components-codeeditor.png new file mode 100644 index 0000000000..64897a7f5e Binary files /dev/null and b/docs/developer-guide/img/components-codeeditor.png differ diff --git a/docs/developer-guide/img/components-richtextarea.png b/docs/developer-guide/img/components-richtextarea.png new file mode 100644 index 0000000000..bc55eefd87 Binary files /dev/null and b/docs/developer-guide/img/components-richtextarea.png differ 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 42b2ef8cc0..abd1ba23c7 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 @@ -290,6 +290,7 @@ public File getGradleProjectDirectory() { private boolean usesAppleSignIn; private boolean usesWebauthn; private boolean usesAppReview; + private boolean usesCodeEditor; private boolean vibratePermission; private boolean smsPermission; private boolean gpsPermission; @@ -1332,6 +1333,13 @@ public void usesClass(String cls) { if (!usesAppReview && cls.indexOf("com/codename1/appreview") == 0) { usesAppReview = true; } + // CodeEditor optionally upgrades its built-in highlighter to + // the bundled CodeMirror web assets. Detected from actual + // usage so apps that never embed a code editor pay nothing + // (see the bundling step further below). + if (!usesCodeEditor && cls.indexOf("com/codename1/ui/CodeEditor") == 0) { + usesCodeEditor = true; + } if (cls.indexOf("com/codename1/location/Geofence") > -1) { if (!"true".equals(playServicesValue)) { // If play services are not currently "blanket" enabled @@ -4125,6 +4133,15 @@ public void usesClassMethod(String cls, String method) { additionalDependencies += " implementation 'com.google.android.play:review:"+reviewVersion+"'\n"; } + // CodeEditor (com.codename1.ui.CodeEditor) can upgrade its self-contained + // built-in highlighter to the bundled CodeMirror assets. We only flag the + // bundling when the app actually references CodeEditor (detected during the + // class scan above) so apps that never embed a code editor stay lean. + if (usesCodeEditor) { + debug("CodeEditor detected: enabling CodeMirror asset bundling"); + request.putArgument("codeEditor.bundleCodeMirror", "true"); + } + // OidcClient routes sign-in through androidx.browser Custom Tabs. // Pull the browser dep in automatically when the app references // anything in com.codename1.io.oidc -- otherwise apps that don't diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index 5be36212e1..566780b6c3 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -106,6 +106,7 @@ public class IPhoneBuilder extends Executor { private boolean usesLocalNotifications; private boolean usesPurchaseAPI; private boolean usesAppReview; + private boolean usesCodeEditor; private boolean usesWalletApi; private boolean usesCryptoAPI; private boolean usesCryptoGcm; @@ -755,6 +756,12 @@ public void usesClass(String cls) { if (!usesAppReview && cls.indexOf("com/codename1/appreview") == 0) { usesAppReview = true; } + // CodeEditor optionally upgrades its built-in highlighter to the + // bundled CodeMirror web assets. Gated on actual usage (CN1_USE_CODEMIRROR) + // so apps that never embed a code editor pay nothing. + if (!usesCodeEditor && cls.indexOf("com/codename1/ui/CodeEditor") == 0) { + usesCodeEditor = true; + } // Wallet issuer-provisioning natives are only compiled in when // the app actually references the API (or enables the extension // via the ios.wallet.extension hint) - see CN1_INCLUDE_WALLET. @@ -1891,6 +1898,11 @@ public void usesClassMethod(String cls, String method) { replaceInFile(CodenameOne_GLViewController_h, "//#define CN1_USE_APPREVIEW", "#define CN1_USE_APPREVIEW"); } + if (usesCodeEditor) { + File CodenameOne_GLViewController_h = new File(buildinRes, "CodenameOne_GLViewController.h"); + replaceInFile(CodenameOne_GLViewController_h, "//#define CN1_USE_CODEMIRROR", "#define CN1_USE_CODEMIRROR"); + + } } catch (Exception ex) { throw new BuildException("Failure while injecting code from build hints", ex); } diff --git a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java index 5e6ae75f36..95b3d792c5 100644 --- a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java +++ b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java @@ -143,6 +143,11 @@ public class TestCodenameOneImplementation extends CodenameOneImplementation { private PeerComponent browserComponent; private final List browserExecuted = new ArrayList<>(); private final Map browserUrls = new HashMap(); + private boolean editorNativePeerSupported = false; + private String lastEditorType; + private PeerComponent lastEditorPeer; + private final List editorCommands = new ArrayList(); + private Function editorQueryResponder; private AsyncResource backgroundMediaAsync; private Media backgroundMedia; private Media media; @@ -950,6 +955,68 @@ public List getBrowserExecuted() { return browserExecuted; } + // --- Native editor SPI test backing (RichTextArea / CodeEditor) --- + // When editorNativePeerSupported is true the editor components run against this + // deterministic native backend instead of the BrowserComponent fallback, which lets + // unit tests exercise the full command/query/event machinery without a real web view. + + @Override + public PeerComponent createNativeEditorPeer(Object editorComponent, String editorType) { + if (!editorNativePeerSupported) { + return null; + } + lastEditorType = editorType; + lastEditorPeer = new PeerComponent(new Object()) { + }; + return lastEditorPeer; + } + + @Override + public void editorPeerCommand(PeerComponent peer, String name, String arg) { + editorCommands.add(name + ":" + (arg == null ? "" : arg)); + } + + @Override + public String editorPeerQuery(PeerComponent peer, String name, String arg) { + if (editorQueryResponder != null) { + String r = editorQueryResponder.apply(name + ":" + (arg == null ? "" : arg)); + if (r != null) { + return r; + } + } + return ""; + } + + public void setEditorNativePeerSupported(boolean supported) { + this.editorNativePeerSupported = supported; + } + + public boolean isEditorNativePeerSupported() { + return editorNativePeerSupported; + } + + public String getLastEditorType() { + return lastEditorType; + } + + public PeerComponent getLastEditorPeer() { + return lastEditorPeer; + } + + public List getEditorCommands() { + return editorCommands; + } + + public String getLastEditorCommand() { + return editorCommands.isEmpty() ? null : editorCommands.get(editorCommands.size() - 1); + } + + /// Configures the canned response for editorPeerQuery. The responder receives "name:arg" + /// and returns the value to hand back to the editor, or null to fall through to "". + public void setEditorQueryResponder(Function responder) { + this.editorQueryResponder = responder; + } + @Override public void blockCopyPaste(boolean blockCopyPaste) { this.blockCopyAndPaste = blockCopyPaste; @@ -1077,6 +1144,11 @@ public void reset() { databases.clear(); browserExecuted.clear(); browserUrls.clear(); + editorNativePeerSupported = false; + lastEditorType = null; + lastEditorPeer = null; + editorCommands.clear(); + editorQueryResponder = null; mediaAsync = null; hasDragStarted = null; backgroundMediaAsync = null; diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/CodeEditorTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/CodeEditorTest.java new file mode 100644 index 0000000000..7e3c28068f --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/CodeEditorTest.java @@ -0,0 +1,229 @@ +package com.codename1.ui; + +import com.codename1.junit.FormTest; +import com.codename1.junit.UITestBase; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.layouts.BorderLayout; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Deep coverage for {@link CodeEditor} including the asynchronous {@link CodeCompletionProvider} flow. + * Tests drive the editor against the deterministic native editor backend of the test implementation so + * the command/query channel and the completion request/response cycle (with JSON serialization) are + * fully exercised without a real web view. + */ +class CodeEditorTest extends UITestBase { + + private void pump() { + for (int i = 0; i < 6; i++) { + flushSerialCalls(); + } + } + + private CodeEditor showNativeEditor() { + implementation.setEditorNativePeerSupported(true); + CodeEditor editor = new CodeEditor(); + Form f = new Form("code", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + return editor; + } + + private List cmds() { + return implementation.getEditorCommands(); + } + + @FormTest + void testNativeBackendTypeIsCode() { + CodeEditor editor = showNativeEditor(); + assertTrue(editor.isNativeEditor()); + assertTrue(editor.isEditorReady()); + assertEquals("code", implementation.getLastEditorType()); + } + + @FormTest + void testLanguageThemeLineNumbersTabSize() { + CodeEditor editor = showNativeEditor(); + editor.setLanguage("java"); + editor.setTheme("dark"); + editor.setShowLineNumbers(false); + editor.setTabSize(2); + assertEquals("java", editor.getLanguage()); + assertEquals("dark", editor.getTheme()); + assertFalse(editor.isShowLineNumbers()); + assertEquals(2, editor.getTabSize()); + List c = cmds(); + assertTrue(c.contains("setLanguage:java")); + assertTrue(c.contains("setTheme:dark")); + assertTrue(c.contains("setLineNumbers:0")); + assertTrue(c.contains("setTabSize:2")); + } + + @FormTest + void testReadOnlyMapsToEditable() { + CodeEditor editor = showNativeEditor(); + editor.setReadOnly(true); + assertTrue(editor.isReadOnly()); + assertFalse(editor.isEditable()); + assertTrue(cmds().contains("setEditable:0")); + editor.setReadOnly(false); + assertFalse(editor.isReadOnly()); + assertTrue(cmds().contains("setEditable:1")); + } + + @FormTest + void testSetTextAndInsert() { + CodeEditor editor = showNativeEditor(); + editor.setText("public class A {}"); + editor.insertAtCursor("// note"); + assertTrue(cmds().contains("setText:public class A {}")); + assertTrue(cmds().contains("insertText:// note")); + } + + @FormTest + void testConstructorWithLanguageAndText() { + implementation.setEditorNativePeerSupported(true); + CodeEditor editor = new CodeEditor("kotlin", "fun main() {}"); + Form f = new Form("code", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + assertEquals("kotlin", editor.getLanguage()); + assertTrue(cmds().contains("setLanguage:kotlin")); + assertTrue(cmds().contains("setText:fun main() {}")); + } + + @FormTest + void testGetTextQueryRoundTrip() { + implementation.setEditorNativePeerSupported(true); + implementation.setEditorQueryResponder(q -> q.equals("getText:") ? "the code" : null); + CodeEditor editor = showNativeEditor(); + AtomicReference result = new AtomicReference<>(); + editor.getText(result::set); + assertEquals("the code", result.get()); + } + + @FormTest + void testGetCursorPositionParsesInteger() { + implementation.setEditorNativePeerSupported(true); + implementation.setEditorQueryResponder(q -> q.equals("getCursor:") ? "42" : null); + CodeEditor editor = showNativeEditor(); + AtomicReference result = new AtomicReference<>(); + editor.getCursorPosition(result::set); + assertEquals(Integer.valueOf(42), result.get()); + } + + @FormTest + void testCursorPositionDefaultsToZeroOnGarbage() { + implementation.setEditorNativePeerSupported(true); + implementation.setEditorQueryResponder(q -> "not-a-number"); + CodeEditor editor = showNativeEditor(); + AtomicReference result = new AtomicReference<>(); + editor.getCursorPosition(result::set); + assertEquals(Integer.valueOf(0), result.get()); + } + + @FormTest + void testCompletionProviderEnableDisable() { + CodeEditor editor = showNativeEditor(); + assertNull(editor.getCompletionProvider()); + CodeCompletionProvider p = (ed, code, cursor, results) -> results.onSucess(new ArrayList<>()); + editor.setCompletionProvider(p); + assertSame(p, editor.getCompletionProvider()); + assertTrue(cmds().contains("setCompletionEnabled:1")); + editor.setCompletionProvider(null); + assertTrue(cmds().contains("setCompletionEnabled:0")); + } + + @FormTest + void testCompletionRequestFlowProducesJson() { + implementation.setEditorNativePeerSupported(true); + implementation.setEditorQueryResponder(q -> q.equals("getText:") ? "Sys" : null); + CodeEditor editor = showNativeEditor(); + + AtomicReference seenCode = new AtomicReference<>(); + AtomicInteger seenCursor = new AtomicInteger(-1); + editor.setCompletionProvider((ed, code, cursor, results) -> { + seenCode.set(code); + seenCursor.set(cursor); + List list = new ArrayList<>(); + list.add(new CodeCompletion("System", "System").setType("class").setDetail("java.lang")); + list.add(new CodeCompletion("System.out", "System.out")); + results.onSucess(list); + }); + + // the native peer would post this when the user triggers completion: reqId 7, cursor 3 + editor.fireEditorEvent("complete", "7:3"); + + assertEquals("Sys", seenCode.get()); + assertEquals(3, seenCursor.get()); + + String last = implementation.getLastEditorCommand(); + assertNotNull(last); + assertTrue(last.startsWith("showCompletions:7:["), "got: " + last); + assertTrue(last.contains("\"d\":\"System\"")); + assertTrue(last.contains("\"i\":\"System\"")); + assertTrue(last.contains("\"t\":\"class\"")); + assertTrue(last.contains("\"x\":\"java.lang\"")); + assertTrue(last.contains("\"d\":\"System.out\"")); + } + + @FormTest + void testCompletionJsonEscaping() { + implementation.setEditorNativePeerSupported(true); + implementation.setEditorQueryResponder(q -> ""); + CodeEditor editor = showNativeEditor(); + editor.setCompletionProvider((ed, code, cursor, results) -> { + List list = new ArrayList<>(); + list.add(new CodeCompletion("a\"b", "x\ny\\z")); + results.onSucess(list); + }); + editor.fireEditorEvent("complete", "1:0"); + String last = implementation.getLastEditorCommand(); + assertNotNull(last); + assertTrue(last.contains("a\\\"b"), "display quote should be escaped: " + last); + assertTrue(last.contains("x\\ny\\\\z"), "insert newline/backslash should be escaped: " + last); + } + + @FormTest + void testCompletionWithoutProviderIsNoOp() { + CodeEditor editor = showNativeEditor(); + int before = cmds().size(); + editor.fireEditorEvent("complete", "1:0"); + // no provider -> no showCompletions command emitted + for (String c : cmds()) { + assertFalse(c.startsWith("showCompletions:")); + } + // and it must not throw / emit anything new + assertEquals(before, cmds().size()); + } + + @FormTest + void testChangeEventStillDispatchesThroughOverride() { + CodeEditor editor = showNativeEditor(); + AtomicInteger count = new AtomicInteger(); + ActionListener l = e -> count.incrementAndGet(); + editor.addChangeListener(l); + editor.fireEditorEvent("change", null); + assertEquals(1, count.get()); + } + + @FormTest + void testBrowserFallbackUsesCodeEditorPage() { + // no native peer -> cross platform browser backend + CodeEditor editor = new CodeEditor(); + Form f = new Form("code", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + assertFalse(editor.isNativeEditor()); + assertNotNull(editor.getInternalBrowser()); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/RichTextAreaTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/RichTextAreaTest.java new file mode 100644 index 0000000000..1c333f7af5 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/RichTextAreaTest.java @@ -0,0 +1,283 @@ +package com.codename1.ui; + +import com.codename1.junit.FormTest; +import com.codename1.junit.UITestBase; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.layouts.BorderLayout; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Deep coverage for {@link RichTextArea}. The component routes every operation through the semantic + * command/query channel of {@link AbstractEditorComponent}; these tests drive it against the + * deterministic native editor backend provided by the test implementation so the full command, query, + * event and ready-queue machinery is exercised without a real web view. A couple of tests additionally + * verify the cross-platform {@code BrowserComponent} fallback selection. + */ +class RichTextAreaTest extends UITestBase { + + private void pump() { + for (int i = 0; i < 6; i++) { + flushSerialCalls(); + } + } + + private RichTextArea showNativeEditor() { + implementation.setEditorNativePeerSupported(true); + RichTextArea editor = new RichTextArea(); + Form f = new Form("rt", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + return editor; + } + + private List cmds() { + return implementation.getEditorCommands(); + } + + @FormTest + void testNativeBackendSelectedAndReady() { + RichTextArea editor = showNativeEditor(); + assertTrue(editor.isNativeEditor()); + assertTrue(editor.isEditorReady()); + assertEquals("richtext", implementation.getLastEditorType()); + assertNull(editor.getInternalBrowser()); + } + + @FormTest + void testQueuedCommandsFlushAfterReady() { + implementation.setEditorNativePeerSupported(true); + RichTextArea editor = new RichTextArea(); + // issued before the backend is ready -> must be queued, not lost + editor.setHtml("

queued

"); + editor.bold(); + assertFalse(editor.isEditorReady()); + Form f = new Form("rt", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + assertTrue(editor.isEditorReady()); + assertTrue(cmds().contains("setHtml:

queued

")); + assertTrue(cmds().contains("bold:")); + } + + @FormTest + void testConstructorWithHtml() { + implementation.setEditorNativePeerSupported(true); + RichTextArea editor = new RichTextArea("hello"); + Form f = new Form("rt", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + assertTrue(cmds().contains("setHtml:hello")); + } + + @FormTest + void testInlineFormattingCommands() { + RichTextArea editor = showNativeEditor(); + editor.bold(); + editor.italic(); + editor.underline(); + editor.strikeThrough(); + editor.removeFormat(); + editor.undo(); + editor.redo(); + List c = cmds(); + assertTrue(c.contains("bold:")); + assertTrue(c.contains("italic:")); + assertTrue(c.contains("underline:")); + assertTrue(c.contains("strikeThrough:")); + assertTrue(c.contains("removeFormat:")); + assertTrue(c.contains("undo:")); + assertTrue(c.contains("redo:")); + } + + @FormTest + void testListsIndentAndAlignment() { + RichTextArea editor = showNativeEditor(); + editor.insertOrderedList(); + editor.insertUnorderedList(); + editor.indent(); + editor.outdent(); + editor.justifyLeft(); + editor.justifyCenter(); + editor.justifyRight(); + List c = cmds(); + assertTrue(c.contains("insertOrderedList:")); + assertTrue(c.contains("insertUnorderedList:")); + assertTrue(c.contains("indent:")); + assertTrue(c.contains("outdent:")); + assertTrue(c.contains("justifyLeft:")); + assertTrue(c.contains("justifyCenter:")); + assertTrue(c.contains("justifyRight:")); + } + + @FormTest + void testLinkCommands() { + RichTextArea editor = showNativeEditor(); + editor.createLink("https://www.codenameone.com/"); + editor.removeLink(); + assertTrue(cmds().contains("createLink:https://www.codenameone.com/")); + assertTrue(cmds().contains("unlink:")); + } + + @FormTest + void testColorConversionPadsToSixHexDigits() { + RichTextArea editor = showNativeEditor(); + editor.setForegroundColor(0xff0000); + editor.setForegroundColor(0x0000ff); + editor.setForegroundColor(0x000000); + editor.setHighlightColor(0x00ff00); + List c = cmds(); + assertTrue(c.contains("foreColor:#ff0000")); + assertTrue(c.contains("foreColor:#0000ff")); + assertTrue(c.contains("foreColor:#000000")); + assertTrue(c.contains("hiliteColor:#00ff00")); + } + + @FormTest + void testBlockFormatAndFontSize() { + RichTextArea editor = showNativeEditor(); + editor.setBlockFormat("h1"); + editor.setFontSize(5); + assertTrue(cmds().contains("formatBlock:h1")); + assertTrue(cmds().contains("fontSize:5")); + } + + @FormTest + void testInsertHtmlAndImage() { + RichTextArea editor = showNativeEditor(); + editor.insertHtml("x"); + editor.insertImage("https://example.com/a.png"); + assertTrue(cmds().contains("insertHtml:x")); + assertTrue(cmds().contains("insertImage:https://example.com/a.png")); + } + + @FormTest + void testPlaceholder() { + RichTextArea editor = showNativeEditor(); + editor.setPlaceholder("Write something..."); + assertEquals("Write something...", editor.getPlaceholder()); + assertTrue(cmds().contains("setPlaceholder:Write something...")); + } + + @FormTest + void testEditableToggle() { + RichTextArea editor = showNativeEditor(); + assertTrue(editor.isEditable()); + editor.setEditable(false); + assertFalse(editor.isEditable()); + assertTrue(cmds().contains("setEditable:0")); + editor.setEditable(true); + assertTrue(editor.isEditable()); + assertTrue(cmds().contains("setEditable:1")); + } + + @FormTest + void testFocusAndBlur() { + RichTextArea editor = showNativeEditor(); + editor.focusEditor(); + editor.blurEditor(); + assertTrue(cmds().contains("focus:")); + assertTrue(cmds().contains("blur:")); + } + + @FormTest + void testGetHtmlQueryRoundTrip() { + implementation.setEditorNativePeerSupported(true); + implementation.setEditorQueryResponder(q -> q.equals("getHtml:") ? "

stored

" : null); + RichTextArea editor = showNativeEditor(); + AtomicReference result = new AtomicReference<>(); + editor.getHtml(result::set); + assertEquals("

stored

", result.get()); + } + + @FormTest + void testGetTextQueryRoundTrip() { + implementation.setEditorNativePeerSupported(true); + implementation.setEditorQueryResponder(q -> q.equals("getText:") ? "plain text" : null); + RichTextArea editor = showNativeEditor(); + AtomicReference result = new AtomicReference<>(); + editor.getText(result::set); + assertEquals("plain text", result.get()); + } + + @FormTest + void testChangeListenerFiresAndDetaches() { + RichTextArea editor = showNativeEditor(); + AtomicInteger count = new AtomicInteger(); + ActionListener l = e -> count.incrementAndGet(); + editor.addChangeListener(l); + editor.fireEditorEvent("change", null); + editor.fireEditorEvent("change", null); + assertEquals(2, count.get()); + editor.removeChangeListener(l); + editor.fireEditorEvent("change", null); + assertEquals(2, count.get()); + } + + @FormTest + void testReadyListenerFiresImmediatelyWhenAlreadyReady() { + RichTextArea editor = showNativeEditor(); + AtomicInteger count = new AtomicInteger(); + editor.addReadyListener(e -> count.incrementAndGet()); + assertEquals(1, count.get()); + } + + @FormTest + void testReadyListenerFiresAfterInitialization() { + implementation.setEditorNativePeerSupported(true); + RichTextArea editor = new RichTextArea(); + AtomicInteger count = new AtomicInteger(); + editor.addReadyListener(e -> count.incrementAndGet()); + assertEquals(0, count.get()); + Form f = new Form("rt", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + assertEquals(1, count.get()); + } + + // --- cross platform BrowserComponent fallback --- + + @FormTest + void testBrowserFallbackSelectedWhenNoNativePeer() { + // editorNativePeerSupported defaults to false -> no native peer -> browser fallback + RichTextArea editor = new RichTextArea(); + Form f = new Form("rt", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + assertFalse(editor.isNativeEditor()); + assertNotNull(editor.getInternalBrowser()); + } + + @FormTest + void testBrowserFallbackBecomesReadyOnLoadAndRoutesCommands() { + RichTextArea editor = new RichTextArea(); + Form f = new Form("rt", new BorderLayout()); + f.add(BorderLayout.CENTER, editor); + f.show(); + pump(); + assertFalse(editor.isEditorReady()); + // simulate the web view finishing load -> editor becomes ready + editor.getInternalBrowser().fireWebEvent(BrowserComponent.onLoad, new com.codename1.ui.events.ActionEvent(editor)); + pump(); + assertTrue(editor.isEditorReady()); + editor.bold(); + pump(); + boolean routed = false; + for (String s : implementation.getBrowserExecuted()) { + if (s.contains("cn1editor.cmd") && s.contains("bold")) { + routed = true; + break; + } + } + assertTrue(routed, "bold() should be routed to the browser backend as a cn1editor.cmd call"); + } +} diff --git a/scripts/android/screenshots/CodeEditor.png b/scripts/android/screenshots/CodeEditor.png new file mode 100644 index 0000000000..8708b0e41b Binary files /dev/null and b/scripts/android/screenshots/CodeEditor.png differ diff --git a/scripts/android/screenshots/CodeEditor.tolerance b/scripts/android/screenshots/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/android/screenshots/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/android/screenshots/RichTextArea.png b/scripts/android/screenshots/RichTextArea.png new file mode 100644 index 0000000000..188b587b80 Binary files /dev/null and b/scripts/android/screenshots/RichTextArea.png differ diff --git a/scripts/android/screenshots/RichTextArea.tolerance b/scripts/android/screenshots/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/android/screenshots/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 3d6d666a68..2aca86807a 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -198,6 +198,8 @@ private static int testTimeoutMs(BaseTest testClass) { new ChartTransformScreenshotTest(), new ChartRotatedScreenshotTest(), new BrowserComponentScreenshotTest(), + new RichTextAreaScreenshotTest(), + new CodeEditorScreenshotTest(), new MediaPlaybackScreenshotTest(), new SheetScreenshotTest(), new SheetSlideUpAnimationScreenshotTest(), diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CodeEditorScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CodeEditorScreenshotTest.java new file mode 100644 index 0000000000..e8f0a71106 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CodeEditorScreenshotTest.java @@ -0,0 +1,76 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.BrowserComponent; +import com.codename1.ui.CodeEditor; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.util.UITimer; + +/** + * Screenshot coverage for {@link CodeEditor}. Renders a read-only (caret-free, for a deterministic + * capture) Java snippet with the line-number gutter and syntax highlighting. + * + * Like {@link RichTextAreaScreenshotTest}, the capture is hard-bounded so platforms that can't render + * or capture the native peer produce a (blank-region) screenshot rather than stalling the suite. + */ +public class CodeEditorScreenshotTest extends BaseTest { + private Form form; + private Runnable readyRunnable; + private boolean ready; + private boolean captured; + + @Override + public boolean runTest() throws Exception { + if (!BrowserComponent.isNativeBrowserSupported()) { + done(); + return true; + } + form = createForm("Code Editor", new BorderLayout(), "CodeEditor"); + CodeEditor editor = new CodeEditor(); + editor.setLanguage("java"); + editor.setShowLineNumbers(true); + editor.setReadOnly(true); + editor.setText("public class Main {\n" + + " public static void main(String[] args) {\n" + + " // greet the user\n" + + " int count = 3;\n" + + " for (int i = 0; i < count; i++) {\n" + + " System.out.println(\"hello \" + i);\n" + + " }\n" + + " }\n" + + "}\n"); + editor.addReadyListener(evt -> { + ready = true; + maybeSettle(); + }); + form.add(BorderLayout.CENTER, editor); + form.show(); + return true; + } + + @Override + protected void registerReadyCallback(Form parent, final Runnable run) { + this.readyRunnable = run; + // Generous bound + settle: the web view's first paint can lag the editor's + // "ready" (page-load) event, especially for the larger CodeEditor, so we wait + // long enough that the rendered content is reliably on screen before capturing. + UITimer.timer(8000, false, parent, this::capture); + maybeSettle(); + } + + private void maybeSettle() { + if (ready && form != null) { + UITimer.timer(3500, false, form, this::capture); + } + } + + private void capture() { + if (captured || readyRunnable == null) { + return; + } + captured = true; + Runnable r = readyRunnable; + readyRunnable = null; + r.run(); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/RichTextAreaScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/RichTextAreaScreenshotTest.java new file mode 100644 index 0000000000..f72e075e0f --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/RichTextAreaScreenshotTest.java @@ -0,0 +1,73 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.BrowserComponent; +import com.codename1.ui.Form; +import com.codename1.ui.RichTextArea; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.util.UITimer; + +/** + * Screenshot coverage for {@link RichTextArea}. Renders a non-editable (caret-free, for a + * deterministic capture) rich text document. + * + * The editor lives inside the platform web widget, which not every platform can render into a + * screenshot (some native ports can't capture peer-component pixels). To keep the suite robust the + * capture is hard-bounded: it fires shortly after the editor becomes ready, or after a few seconds + * regardless, so a platform that can't render/capture the peer produces a (blank-region) screenshot + * instead of stalling the whole suite. + */ +public class RichTextAreaScreenshotTest extends BaseTest { + private Form form; + private Runnable readyRunnable; + private boolean ready; + private boolean captured; + + @Override + public boolean runTest() throws Exception { + if (!BrowserComponent.isNativeBrowserSupported()) { + done(); + return true; + } + form = createForm("Rich Text", new BorderLayout(), "RichTextArea"); + RichTextArea editor = new RichTextArea(); + editor.setEditable(false); + editor.setHtml("

Trip itinerary

" + + "

Meet at the main lobby by 9:00 AM. Bring a " + + "valid passport and your " + + "boarding pass.

" + + "
  • Day 1 — city tour
  • Day 2 — museum & harbor
  • " + + "
  • Day 3 — free time
"); + editor.addReadyListener(evt -> { + ready = true; + maybeSettle(); + }); + form.add(BorderLayout.CENTER, editor); + form.show(); + return true; + } + + @Override + protected void registerReadyCallback(Form parent, final Runnable run) { + this.readyRunnable = run; + // Generous bound + settle so the web view's first paint reliably completes + // before we capture (the paint can lag the editor's page-load "ready" event). + UITimer.timer(8000, false, parent, this::capture); + maybeSettle(); + } + + private void maybeSettle() { + if (ready && form != null) { + UITimer.timer(3500, false, form, this::capture); + } + } + + private void capture() { + if (captured || readyRunnable == null) { + return; + } + captured = true; + Runnable r = readyRunnable; + readyRunnable = null; + r.run(); + } +} diff --git a/scripts/ios/screenshots-metal/CodeEditor.png b/scripts/ios/screenshots-metal/CodeEditor.png new file mode 100644 index 0000000000..7e469b5003 Binary files /dev/null and b/scripts/ios/screenshots-metal/CodeEditor.png differ diff --git a/scripts/ios/screenshots-metal/CodeEditor.tolerance b/scripts/ios/screenshots-metal/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots-metal/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/ios/screenshots-metal/RichTextArea.png b/scripts/ios/screenshots-metal/RichTextArea.png new file mode 100644 index 0000000000..6913abead2 Binary files /dev/null and b/scripts/ios/screenshots-metal/RichTextArea.png differ diff --git a/scripts/ios/screenshots-metal/RichTextArea.tolerance b/scripts/ios/screenshots-metal/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots-metal/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/ios/screenshots-tv/CodeEditor.png b/scripts/ios/screenshots-tv/CodeEditor.png new file mode 100644 index 0000000000..b0ad1f0e4d Binary files /dev/null and b/scripts/ios/screenshots-tv/CodeEditor.png differ diff --git a/scripts/ios/screenshots-tv/CodeEditor.tolerance b/scripts/ios/screenshots-tv/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots-tv/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/ios/screenshots-tv/RichTextArea.png b/scripts/ios/screenshots-tv/RichTextArea.png new file mode 100644 index 0000000000..a0597d6a69 Binary files /dev/null and b/scripts/ios/screenshots-tv/RichTextArea.png differ diff --git a/scripts/ios/screenshots-tv/RichTextArea.tolerance b/scripts/ios/screenshots-tv/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots-tv/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/ios/screenshots-watch/CodeEditor.png b/scripts/ios/screenshots-watch/CodeEditor.png new file mode 100644 index 0000000000..e1501f3eef Binary files /dev/null and b/scripts/ios/screenshots-watch/CodeEditor.png differ diff --git a/scripts/ios/screenshots-watch/CodeEditor.tolerance b/scripts/ios/screenshots-watch/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots-watch/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/ios/screenshots-watch/RichTextArea.png b/scripts/ios/screenshots-watch/RichTextArea.png new file mode 100644 index 0000000000..6b1761f4e8 Binary files /dev/null and b/scripts/ios/screenshots-watch/RichTextArea.png differ diff --git a/scripts/ios/screenshots-watch/RichTextArea.tolerance b/scripts/ios/screenshots-watch/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots-watch/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/ios/screenshots/CodeEditor.png b/scripts/ios/screenshots/CodeEditor.png new file mode 100644 index 0000000000..62280766e2 Binary files /dev/null and b/scripts/ios/screenshots/CodeEditor.png differ diff --git a/scripts/ios/screenshots/CodeEditor.tolerance b/scripts/ios/screenshots/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/ios/screenshots/RichTextArea.png b/scripts/ios/screenshots/RichTextArea.png new file mode 100644 index 0000000000..fa5eb7e1fd Binary files /dev/null and b/scripts/ios/screenshots/RichTextArea.png differ diff --git a/scripts/ios/screenshots/RichTextArea.tolerance b/scripts/ios/screenshots/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/ios/screenshots/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/javascript/screenshots/CodeEditor.png b/scripts/javascript/screenshots/CodeEditor.png new file mode 100644 index 0000000000..a14a28fbbf Binary files /dev/null and b/scripts/javascript/screenshots/CodeEditor.png differ diff --git a/scripts/javascript/screenshots/CodeEditor.tolerance b/scripts/javascript/screenshots/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/javascript/screenshots/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/javascript/screenshots/RichTextArea.png b/scripts/javascript/screenshots/RichTextArea.png new file mode 100644 index 0000000000..3820653da3 Binary files /dev/null and b/scripts/javascript/screenshots/RichTextArea.png differ diff --git a/scripts/javascript/screenshots/RichTextArea.tolerance b/scripts/javascript/screenshots/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/javascript/screenshots/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/linux/screenshots/CodeEditor.png b/scripts/linux/screenshots/CodeEditor.png new file mode 100644 index 0000000000..2aa893bec0 Binary files /dev/null and b/scripts/linux/screenshots/CodeEditor.png differ diff --git a/scripts/linux/screenshots/CodeEditor.tolerance b/scripts/linux/screenshots/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/linux/screenshots/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/linux/screenshots/RichTextArea.png b/scripts/linux/screenshots/RichTextArea.png new file mode 100644 index 0000000000..8a5b255eda Binary files /dev/null and b/scripts/linux/screenshots/RichTextArea.png differ diff --git a/scripts/linux/screenshots/RichTextArea.tolerance b/scripts/linux/screenshots/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/linux/screenshots/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/mac-native/screenshots/CodeEditor.png b/scripts/mac-native/screenshots/CodeEditor.png new file mode 100644 index 0000000000..c6d656f2ad Binary files /dev/null and b/scripts/mac-native/screenshots/CodeEditor.png differ diff --git a/scripts/mac-native/screenshots/CodeEditor.tolerance b/scripts/mac-native/screenshots/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/mac-native/screenshots/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/mac-native/screenshots/RichTextArea.png b/scripts/mac-native/screenshots/RichTextArea.png new file mode 100644 index 0000000000..eeb1e3f29c Binary files /dev/null and b/scripts/mac-native/screenshots/RichTextArea.png differ diff --git a/scripts/mac-native/screenshots/RichTextArea.tolerance b/scripts/mac-native/screenshots/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/mac-native/screenshots/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/windows/screenshots/CodeEditor.png b/scripts/windows/screenshots/CodeEditor.png new file mode 100644 index 0000000000..ff23a8b6bf Binary files /dev/null and b/scripts/windows/screenshots/CodeEditor.png differ diff --git a/scripts/windows/screenshots/CodeEditor.tolerance b/scripts/windows/screenshots/CodeEditor.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/windows/screenshots/CodeEditor.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0 diff --git a/scripts/windows/screenshots/RichTextArea.png b/scripts/windows/screenshots/RichTextArea.png new file mode 100644 index 0000000000..3f5adbda5b Binary files /dev/null and b/scripts/windows/screenshots/RichTextArea.png differ diff --git a/scripts/windows/screenshots/RichTextArea.tolerance b/scripts/windows/screenshots/RichTextArea.tolerance new file mode 100644 index 0000000000..b497bdd049 --- /dev/null +++ b/scripts/windows/screenshots/RichTextArea.tolerance @@ -0,0 +1,5 @@ +# The editor renders inside the platform native web widget; absorb minor +# web-render anti-aliasing variance between CI runs (blank/black peer captures +# are well within this). +maxChannelDelta=16 +maxMismatchPercent=2.0