From e5a3f6c02d7dcf3597d9592493ca1111117c2bdd Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:53:28 +0300 Subject: [PATCH 01/12] JS port: desktop native themes, page settings, and the rendering/input fixes behind #5910 #5912 Native theme mode on the JavaScript port: - nativeTheme=native gives a desktop browser its OS theme (Fluent, Aqua, Adwaita); javascript.desktopTheme pins one. Phones and tablets keep iOS/Android themes. - javascript.titleBar=toolbar (default) keeps a desktop-styled Toolbar; the desktop themes gained TitleCommand/BackCommand/SideCommand/side-menu UIIDs for it. - javascript.titleBar=html draws an HTML title bar and menu bar above the app. - The builder ships only the native themes the app's hints can reach. - Theme/title-bar/text-selection hints now reach the running app. index.html (#5909 #5911 #5913 #5914): viewport meta, display name as title, notranslate and darkreader-lock by default with opt-out hints. Opt-in javascript.textSelection (#5915) via TextSelection.setDefaultSelectable. Fixes: - #5910: the wheel scrolled the Form itself past the content's end (core); a theme refresh on show dropped the dialog backdrop painter; the frame clear ran on frames that did not repaint the whole screen. - #5912: touch lists and pointerType were never sent to the worker; the move listeners were never invoked; mouse and touch handlers raced for the press; a tap's release overtook its press. - Toolbar commands never reached the native menu bar under commandBehavior Native. - #5774: JS RTA culled classes created by Class.forName(name).newInstance(). scripts/test-javascript-composited-rendering.mjs drives a fixture app with real wheel, mouse and touch input in Chromium, Firefox and phone emulation and reads the composited page; it fails on the previous master. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/scripts-javascript.yml | 28 +- CodenameOne/src/com/codename1/ui/Display.java | 8 + CodenameOne/src/com/codename1/ui/Form.java | 14 +- CodenameOne/src/com/codename1/ui/Label.java | 14 +- .../src/com/codename1/ui/TextArea.java | 13 +- .../src/com/codename1/ui/TextSelection.java | 37 +- .../impl/html5/HTML5Implementation.java | 356 ++++++++++++-- .../impl/html5/JSOImplementations.java | 7 + .../impl/html5/JavaScriptDesktopChrome.java | 293 ++++++++++++ .../JavaScriptPort/src/main/webapp/style.css | 140 ++++++ Themes/GnomeAdwaitaTheme.res | Bin 166062 -> 187721 bytes Themes/MacOSAquaTheme.res | Bin 147859 -> 169518 bytes Themes/WindowsFluentTheme.res | Bin 169806 -> 191465 bytes docs/developer-guide/Native-Themes.asciidoc | 5 + .../Working-With-Javascript.asciidoc | 51 ++- .../build/shared/BuildHintsDesktop.java | 63 +++ .../builders/JavaScriptBuildHints.java | 365 +++++++++++++++ .../codename1/builders/JavaScriptBuilder.java | 46 +- .../builders/JavaScriptBuildHintsTest.java | 152 ++++++ .../com/codename1/ui/DesktopChromeTest.java | 25 + .../com/codename1/ui/DialogBackdropTest.java | 88 ++++ .../codename1/ui/ScrollWheelGestureTest.java | 22 + .../com/codename1/ui/TextSelectionTest.java | 82 ++++ maven/parparvm/pom.xml | 20 +- native-themes/gnome-adwaita/theme.css | 46 ++ native-themes/macos-aqua/theme.css | 46 ++ native-themes/windows-fluent/theme.css | 46 ++ .../common/codenameone_settings.properties | 59 +++ .../javascript/composited-app/common/icon.png | Bin 0 -> 123269 bytes .../javascript/composited-app/common/pom.xml | 433 ++++++++++++++++++ .../common/src/main/css/theme.css | 92 ++++ .../codename1/jsfixture/CompositedApp.java | 59 +++ .../composited-app/javascript/pom.xml | 83 ++++ scripts/javascript/composited-app/pom.xml | 280 +++++++++++ .../test-javascript-composited-rendering.mjs | 343 ++++++++++++++ .../translator/JavascriptBundleWriter.java | 61 +++ .../translator/JavascriptReachability.java | 33 ++ .../src/javascript/browser_bridge.js | 36 +- .../src/javascript/index.html | 5 +- .../src/javascript/parparvm_runtime.js | 10 +- .../JavascriptPageSettingsTest.java | 98 ++++ .../JavascriptRuntimeSemanticsTest.java | 15 + .../translator/JsReflectiveInstanceApp.java | 48 ++ 43 files changed, 3572 insertions(+), 50 deletions(-) create mode 100644 Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JavaScriptDesktopChrome.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuildHints.java create mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/ui/DialogBackdropTest.java create mode 100644 scripts/javascript/composited-app/common/codenameone_settings.properties create mode 100644 scripts/javascript/composited-app/common/icon.png create mode 100644 scripts/javascript/composited-app/common/pom.xml create mode 100644 scripts/javascript/composited-app/common/src/main/css/theme.css create mode 100644 scripts/javascript/composited-app/common/src/main/java/com/codename1/jsfixture/CompositedApp.java create mode 100644 scripts/javascript/composited-app/javascript/pom.xml create mode 100644 scripts/javascript/composited-app/pom.xml create mode 100644 scripts/test-javascript-composited-rendering.mjs create mode 100644 vm/tests/src/test/java/com/codename1/tools/translator/JavascriptPageSettingsTest.java create mode 100644 vm/tests/src/test/resources/com/codename1/tools/translator/JsReflectiveInstanceApp.java diff --git a/.github/workflows/scripts-javascript.yml b/.github/workflows/scripts-javascript.yml index 38f36acc62a..052a6f1cec6 100644 --- a/.github/workflows/scripts-javascript.yml +++ b/.github/workflows/scripts-javascript.yml @@ -24,6 +24,8 @@ on: - 'scripts/test-javascript-surface-replay.mjs' - 'scripts/test-javascript-weak-refs.mjs' - 'scripts/test-javascript-vault-bridge.mjs' + - 'scripts/test-javascript-composited-rendering.mjs' + - 'scripts/javascript/composited-app/**' - 'vm/ByteCodeTranslator/src/javascript/browser_bridge.js' - 'scripts/run-javascript-lifecycle-tests.sh' - 'scripts/verify-javascript-lens-parity.mjs' @@ -231,7 +233,7 @@ jobs: uses: actions/cache@v5 with: path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-chromium-v2 + key: ${{ runner.os }}-playwright-chromium-firefox-v1 restore-keys: | ${{ runner.os }}-playwright- @@ -246,7 +248,7 @@ jobs: # vendor mirror makes its apt-get fail as a whole and it reports # only "Failed to install browser dependencies". bash "$GITHUB_WORKSPACE/scripts/ci/apt-get-update.sh" - npx playwright install-deps chromium + npx playwright install-deps chromium firefox # `npm install playwright` resolves the floating version, so the # cached browser binary can drift away from what the freshly # installed Playwright expects (the cache only stores the binary). @@ -255,6 +257,10 @@ jobs: # job resilient to Playwright revving its bundled chromium build # in between cache writes. npx playwright install chromium + # Firefox for the composited rendering check below: issue #5910 was reported + # from Firefox, and the two engines composite the canvas and the text layer + # differently enough that one passing says little about the other. + npx playwright install firefox # Placed here, before the workspace build, because it needs nothing from # it: the vault bridge is sliced straight out of browser_bridge.js and run @@ -287,6 +293,24 @@ jobs: # xvfb-run is required for CSS compilation which uses java.awt SKIP_PARPARVM_BUILD=1 xvfb-run ./scripts/build-javascript-port-hellocodenameone.sh "${GITHUB_WORKSPACE}/artifacts/javascript-ui-tests/hellocodenameone-javascript-port.zip" + # What the screenshot suite cannot see. Its capture reads the app's canvas back, which + # suspends the DOM text layer, so it never looks at the page a browser shows -- and + # issues #5910 (toolbar scrolled away, dialog over a blank page) and #5912 (a phone could + # not scroll or tap) came back more than once while it stayed green. This builds the app + # those issues were reported against through the Maven plugin, as a user's project is + # built, and drives it with real wheel, mouse and touch input in Chromium and Firefox. + - name: Composited rendering and input (Chromium, Firefox, phone emulation) + run: | + set -euo pipefail + export JAVA_HOME="${JAVA_HOME_17_X64}" + export PATH="$JAVA_HOME/bin:$PATH" + (cd scripts/javascript/composited-app && xvfb-run mvn -B package -DskipTests \ + -Dcodename1.platform=javascript -Dcodename1.buildTarget=local-javascript) + bundle_dir="${RUNNER_TEMP}/composited-app" + rm -rf "$bundle_dir" && mkdir -p "$bundle_dir" + unzip -q scripts/javascript/composited-app/javascript/target/compositedapp-javascript-1.0-SNAPSHOT.zip -d "$bundle_dir" + node scripts/test-javascript-composited-rendering.mjs "$bundle_dir" "${GITHUB_WORKSPACE}/artifacts/javascript-ui-tests/composited" + - name: Locate JavaScript bundle id: locate_bundle run: | diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index 147454c908f..176cbd9e6b9 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -9588,6 +9588,14 @@ private void setScrollPosition(Component target, boolean vertical, int position) private boolean scrollAxisForWheel(Component cmp, boolean vertical, int delta) { Component c = cmp; while (c != null) { + // The walk ends at the top level. Form and Window answer isScrollableY() for their + // content pane, which the walk has already visited on the way up -- but applyScroll + // would move the top level's OWN scroll position, and that translates everything it + // paints: the Toolbar scrolled off with the content, and the space it left was blank + // (issue #5910). Reaching the top level means the page is already at its edge. + if (c instanceof TopLevelContainer) { + return false; + } // A disabled component takes no wheel, exactly as it took no synthetic drag: // Form.pointerDragged gated on isEnabled, so disabling a scroller used to stop // the wheel too. The walk continues, so an enabled ancestor still gets it. diff --git a/CodenameOne/src/com/codename1/ui/Form.java b/CodenameOne/src/com/codename1/ui/Form.java index a23fb0d9fd8..ac8bbdd76b4 100644 --- a/CodenameOne/src/com/codename1/ui/Form.java +++ b/CodenameOne/src/com/codename1/ui/Form.java @@ -3299,11 +3299,17 @@ void initComponentImpl() { // an EmbeddedContainer -- would otherwise overwrite the real surface's native // menu bar with its own. if (getParent() == null) { - if (Display.getInstance().isNativeCommands()) { - Display.impl.setNativeCommands(menuBar.getCommands()); - } else if (isDesktopNativeChrome() && toolbar != null) { - // bridge the (hidden) toolbar's commands to the native desktop menu bar + if (isDesktopNativeChrome() && toolbar != null) { + // bridge the (hidden) toolbar's commands to the native desktop menu bar. + // Checked BEFORE the native command behaviour: the desktop native themes declare + // commandBehavior Native, and with a Toolbar the commands live on the Toolbar + // rather than on this form's MenuBar -- publishing the MenuBar's list there + // handed the native menu bar nothing, so it came up empty while the Toolbar + // that held the commands was hidden. getAllNativeMenuCommands() includes the + // Toolbar's MenuBar, so nothing the other branch would publish is lost. Display.impl.setNativeCommands(toolbar.getAllNativeMenuCommands()); + } else if (Display.getInstance().isNativeCommands()) { + Display.impl.setNativeCommands(menuBar.getCommands()); } } if (getParent() != null) { diff --git a/CodenameOne/src/com/codename1/ui/Label.java b/CodenameOne/src/com/codename1/ui/Label.java index 1742bc49507..019132b5b21 100644 --- a/CodenameOne/src/com/codename1/ui/Label.java +++ b/CodenameOne/src/com/codename1/ui/Label.java @@ -75,6 +75,9 @@ public class Label extends Component implements IconHolder, TextHolder { private String text = ""; private TextSelectionSupport textSelectionSupport; private boolean textSelectionEnabled; + /// True once `#setTextSelectionEnabled(boolean)` decided; until then the label follows + /// `TextSelection#setDefaultSelectable(boolean)`. + private boolean textSelectionExplicit; private Image icon; private final ActionListener iconChangeListener = new ActionListener() { @Override @@ -1499,7 +1502,11 @@ public void setAutoSizeMode(boolean autoSizeMode) { /// /// - #setTextSelectionEnabled(boolean) public boolean isTextSelectionEnabled() { - return textSelectionEnabled; + if (textSelectionExplicit) { + return textSelectionEnabled; + } + // A Button's press is its action, so it never takes the selectable default. + return !(this instanceof Button) && TextSelection.isSelectableByDefault(this); } /// Enables text selection on this label. Text selection must also be enabled on the Form in order to @@ -1518,6 +1525,7 @@ public boolean isTextSelectionEnabled() { /// - TextSelection#setEnabled(boolean) public void setTextSelectionEnabled(boolean enabled) { this.textSelectionEnabled = enabled; + this.textSelectionExplicit = true; if (enabled) { setCursor(Component.TEXT_CURSOR); } @@ -1543,12 +1551,12 @@ public TextSelection.Spans getTextSelectionForBounds(TextSelection sel, Rectangl @Override public boolean isTextSelectionEnabled(TextSelection sel) { - return textSelectionEnabled; + return Label.this.isTextSelectionEnabled(); } @Override public boolean isTextSelectionTriggerEnabled(TextSelection sel) { - return textSelectionEnabled; + return Label.this.isTextSelectionEnabled(); } @Override diff --git a/CodenameOne/src/com/codename1/ui/TextArea.java b/CodenameOne/src/com/codename1/ui/TextArea.java index d1a8cbd0ff9..81c03b56f9e 100644 --- a/CodenameOne/src/com/codename1/ui/TextArea.java +++ b/CodenameOne/src/com/codename1/ui/TextArea.java @@ -240,6 +240,9 @@ public void actionPerformed(ActionEvent evt) { } }; private boolean textSelectionEnabled; + /// True once `#setTextSelectionEnabled(boolean)` decided; until then a read-only text area + /// follows `TextSelection#setDefaultSelectable(boolean)`. + private boolean textSelectionExplicit; private TextSelection.Spans span; private TextSelection.TextSelectionSupport textSelectionSupport; @@ -2352,7 +2355,10 @@ protected Spans calculateTextSelectionSpan(TextSelection sel) { /// /// - #setTextSelectionEnabled(boolean) public boolean isTextSelectionEnabled() { - return textSelectionEnabled; + if (textSelectionExplicit) { + return textSelectionEnabled; + } + return !isEditable() && TextSelection.isSelectableByDefault(this); } /// Enables text selection on this TextArea. Text selection must also be enabled on the Form in order to @@ -2371,6 +2377,7 @@ public boolean isTextSelectionEnabled() { /// - TextSelection#setEnabled(boolean) public void setTextSelectionEnabled(boolean enabled) { this.textSelectionEnabled = enabled; + this.textSelectionExplicit = true; updateCursor(); } @@ -2397,13 +2404,13 @@ public TextSelection.Spans getTextSelectionForBounds(TextSelection sel, Rectangl @Override public boolean isTextSelectionEnabled(TextSelection sel) { - return (!isEditable() && textSelectionEnabled) || (isEditable() && !isEnabled()); + return (!isEditable() && TextArea.this.isTextSelectionEnabled()) || (isEditable() && !isEnabled()); } @Override public boolean isTextSelectionTriggerEnabled(TextSelection sel) { - return (!isEditable() && textSelectionEnabled) || (isEditable() && !isEnabled()); + return (!isEditable() && TextArea.this.isTextSelectionEnabled()) || (isEditable() && !isEnabled()); } diff --git a/CodenameOne/src/com/codename1/ui/TextSelection.java b/CodenameOne/src/com/codename1/ui/TextSelection.java index 4eac7e2b6bf..8d66ce4dc72 100644 --- a/CodenameOne/src/com/codename1/ui/TextSelection.java +++ b/CodenameOne/src/com/codename1/ui/TextSelection.java @@ -188,6 +188,7 @@ public int compare(Component o1, Component o2) { private final Spans selectedSpans = new Spans(); private final EventDispatcher textSelectionListeners = new EventDispatcher(); private final Rectangle tmpRect = new Rectangle(); + private static boolean defaultSelectable; private final Component root; private boolean rtl; private final TextSelectionTrigger trigger = getDefaultTextSelectionTrigger(); @@ -467,6 +468,40 @@ public void run() { this.root = root; } + /// Makes read-only text selectable wherever the application did not decide: labels, span + /// labels and non-editable text areas report `isTextSelectionEnabled()` true unless + /// `setTextSelectionEnabled` was called on them. Off by default. + /// + /// Buttons never take the default, and neither does text inside a lead component such as + /// a `MultiButton`: the press that would start a selection there is the press that + /// activates the control. + /// + /// Text selection still has to be enabled on the form (`Form#getTextSelection()`) for any + /// of this to be reachable. The JavaScript port does both when the + /// `javascript.textSelection` build hint is set. + /// + /// #### Parameters + /// + /// - `selectable`: true to make read-only text selectable by default + public static void setDefaultSelectable(boolean selectable) { + defaultSelectable = selectable; + } + + /// Whether read-only text is selectable by default. See `#setDefaultSelectable(boolean)`. + /// + /// #### Returns + /// + /// true when read-only text is selectable unless a component says otherwise + public static boolean isDefaultSelectable() { + return defaultSelectable; + } + + /// Whether `cmp` takes the default of `#setDefaultSelectable(boolean)`: it is on, and the + /// component is not part of a lead component, whose press belongs to the control. + static boolean isSelectableByDefault(Component cmp) { + return defaultSelectable && cmp.getLeadComponent() == null; + } + /// Gets the default trigger type for text selection. This will vary by platform. /// On mobile/touch devices, it will return `TextSelectionTrigger#LongPress`, /// and on desktop environments with a mouse, it will return `TextSelectionTrigger#Press`. @@ -545,7 +580,7 @@ public void setEnabled(boolean enabled) { f.removePointerDraggedListener(pressListener); f.removePointerReleasedListener(pressListener); f.removeDragFinishedListener(pressListener); - f.addLongPressListener(pressListener); + f.removeLongPressListener(pressListener); Display.impl.deinitializeTextSelection(this); } } diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java index 937a2b8dd86..31b737d9086 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java @@ -235,7 +235,6 @@ BufferedGraphics displayGraphics() { // and handle them directly. private EventListener nativeEventListener; - private JSFunction onMouseMoveHandle, onTouchMoveHandle, onPointerMoveHandle; private NativeFont defaultFont; private String pendingTextChanges; private TextArea currentEditingField; @@ -555,6 +554,23 @@ HTML5Graphics createSurfaceGraphics(int width, int height) { return new HTML5Graphics(this, id, width, height); } + /** + * True for a pointer event a finger produced. + * + *

A touch screen fires both families for one gesture -- pointerdown and touchstart, + * pointermove and touchmove -- and the canvas listens to both. Both handlers used to act on + * it, and because each crosses the worker boundary they raced: pointerdown marked the mouse + * down, touchstart saw that and cancelled mouse tracking without pressing, and pointerdown + * then saw the touch down and dropped its own press. Nothing pressed, so every drag that + * followed was ignored and a phone could not scroll at all, and taps only landed when the + * race went the other way (issue #5912). The touch handlers carry multi-touch, so a finger + * is theirs alone and the pointer handlers keep the mouse and the pen.

+ */ + private static boolean isTouchPointer(Event evt) { + String type = ((JSOImplementations.PointerTypeEvent) evt).getPointerType(); + return "touch".equals(type); + } + private int getClientX(MouseEvent evt) { int x = evt.getClientX(); if (x == -1) { @@ -569,7 +585,7 @@ private int getClientY(MouseEvent evt) { if (y == -1) { return y; } - return (int)((y + getScrollY_()) * devicePixelRatioValue()); + return (int)((y - chromeTopCss + getScrollY_()) * devicePixelRatioValue()); } private boolean hitTest(int x, int y) { @@ -1280,9 +1296,39 @@ void update() { } + /** + * True once a paint recorded since the last flush repaints the whole screen, so the frame + * carrying it may clear the canvas first. See {@link #pendingFrameClears}. + */ + private boolean paintCoversScreen; + + /** + * Whether the frame waiting to be drained repaints every pixel of the screen, which is what + * makes clearing the canvas before replaying it safe. + * + *

The flushed rectangle cannot answer that. {@code PaintSurface.paintDirty()} clips a + * queued component to its dirty region but flushes the component's whole bounds, so a Form + * or a Dialog that repaints only part of itself still flushes a full-screen rectangle. + * Clearing on the rectangle alone wiped everything outside the part that did repaint: a + * Dialog repainting its body left the tinted form around it transparent, which showed as + * a dialog floating on a white page (issue #5910). The clear is therefore earned by the + * paint itself -- a form painting under a full-screen clip, or a transition frame, whose + * buffered images cover the screen -- and anything else keeps the pixels it did not + * repaint, as it does on every other port.

+ */ + private boolean pendingFrameClears; + @Override public void beforeComponentPaint(Component c, Graphics g) { super.beforeComponentPaint(c, g); + if (!paintCoversScreen && c instanceof Form && isDisplayGraphics(g) + && graphics.getClipX() <= 0 && graphics.getClipY() <= 0 + && graphics.getClipX() + graphics.getClipWidth() >= displayWidth + && graphics.getClipY() + graphics.getClipHeight() >= displayHeight + && c.getAbsoluteX() <= 0 && c.getAbsoluteY() <= 0 + && c.getWidth() >= displayWidth && c.getHeight() >= displayHeight) { + paintCoversScreen = true; + } Object overlay = c.getNativeOverlay(); if (overlay != null) { NativeOverlay no = (NativeOverlay)overlay; @@ -1964,6 +2010,10 @@ public void add(String eventName, Object listener) { @Override public void handleEvent(Event evt) { + if (isTouchPointer(evt)) { + // A finger belongs to the touch handlers; see isTouchPointer. + return; + } // Set ``mouseDown=true`` IMMEDIATELY, before any JSO call // that can yield. ParparVM compiles every Java method to a // JS generator, and JSO calls (``evt.getType()``, @@ -2073,6 +2123,9 @@ public void run() { onMouseUp = new EventListener(){ @Override public void handleEvent(Event evt) { + if (isTouchPointer(evt)) { + return; + } if (nativeEventListener != null) { CancelableEvent cevt = (CancelableEvent)evt; nativeEventListener.handleEvent(evt); @@ -2151,10 +2204,27 @@ public void run() { @SuppressSyncErrors @Override public void handleEvent(Event evt) { + // Mark the touch down and the press in flight BEFORE anything that can yield, + // exactly as onMouseDown does and for the same reason. Every JSO call below + // suspends this handler while it crosses the worker boundary, and a quick tap's + // touchend is dispatched in that window: it found no touch down, dropped the + // release, and the press it belonged to was never released -- the button stayed + // pressed and nothing fired, which on a phone is every tap on the hamburger menu + // and most taps on a button (issue #5912). + final boolean touchAlreadyDown = pointerState.isTouchDown(); + pointerState.setTouchDown(true); + synchronized (pointerEventOrderLock) { + pressInFlight = true; + deferredRelease = null; + } if (nativeEventListener != null) { CancelableEvent cevt = (CancelableEvent)evt; nativeEventListener.handleEvent(evt); if (cevt.isDefaultPrevented()) { + if (!touchAlreadyDown) { + pointerState.setTouchDown(false); + } + completePressInFlight(); return; } } @@ -2182,8 +2252,12 @@ public void handleEvent(Event evt) { evt.stopPropagation(); } JavaScriptInputCoordinator.TouchStartDecision touchDecision = JavaScriptInputCoordinator.resolveTouchStart( - pointerState.isMouseDown(), pointerState.isTouchDown(), evt.getTarget() == textField || evt.getTarget() == textArea, isEditing && editingStartingUp); + pointerState.isMouseDown(), touchAlreadyDown, evt.getTarget() == textField || evt.getTarget() == textArea, isEditing && editingStartingUp); if (touchDecision.shouldIgnoreEvent()) { + if (!touchAlreadyDown) { + pointerState.setTouchDown(false); + } + completePressInFlight(); return; } if (touchDecision.shouldCancelMouseTracking()) { @@ -2219,9 +2293,15 @@ public void run() { @Override public void run() { - HTML5Implementation.this.pointerPressed(x, y); + try { + HTML5Implementation.this.pointerPressed(x, y); + } finally { + completePressInFlight(); + } } }); + } else { + completePressInFlight(); } } @@ -2267,14 +2347,33 @@ public void handleEvent(Event evt) { TouchEvent me = (TouchEvent)evt; installBacksideHooksInUserInteraction(false); - nativeCallSerially(new Runnable() { + final Runnable releaseDispatch = new Runnable() { @Override public void run() { - pointerState.setLastTouchUpPosition(pointerState.getTouchesX()[0], pointerState.getTouchesY()[0]); - HTML5Implementation.this.pointerReleased(pointerState.getTouchesX(), pointerState.getTouchesY()); - + nativeCallSerially(new Runnable() { + @Override + public void run() { + pointerState.setLastTouchUpPosition(pointerState.getTouchesX()[0], pointerState.getTouchesY()[0]); + HTML5Implementation.this.pointerReleased(pointerState.getTouchesX(), pointerState.getTouchesY()); + } + }); } - }); + }; + // A tap's touchend can overtake its touchstart, which is still suspended on a + // host call and has not pressed yet. Releasing first would leave the press + // without its release, so the release waits for the press, as onMouseUp does. + boolean releaseNow; + synchronized (pointerEventOrderLock) { + if (pressInFlight) { + deferredRelease = releaseDispatch; + releaseNow = false; + } else { + releaseNow = true; + } + } + if (releaseNow) { + releaseDispatch.run(); + } if (JavaScriptInputCoordinator.shouldCreatePreemptiveTextField(usePreemptiveNativeTextFieldApproach(), pointerState.getTouchStartTime(), currentTimeMillisecondsJS(), pointerState.getTouchStartX(), pointerState.getTouchStartY(), pointerState.getTouchesX()[0], pointerState.getTouchesY()[0])) { // Hack for iOS only to anticipate clicking on a text field createAndFocusTextFieldPreemptively(pointerState.getTouchesX()[0], pointerState.getTouchesY()[0]); @@ -2396,7 +2495,7 @@ public void handleEvent(Event evt) { MouseEvent me = (MouseEvent)evt; int x = unscaleCoord(me.getClientX()); - int y = unscaleCoord(me.getClientY()); + int y = unscaleCoord(me.getClientY() - chromeTopCss); if (!hitTest(x, y)) { _debug("1.Failed hit test at "+x+","+y); @@ -2438,7 +2537,7 @@ public void handleEvent(Event evt) { if (te.getTargetTouches().getLength() > 0) { MouseEvent me = te.getTargetTouches().get(0); int x = unscaleCoord(me.getClientX()); - int y = unscaleCoord(me.getClientY()); + int y = unscaleCoord(me.getClientY() - chromeTopCss); boolean hitTestResult = false; if (!hitTest(x, y)) { if (pointerState.isCapturingEvents()) { @@ -2485,7 +2584,7 @@ public void handleEvent(Event evt) { MouseEvent me = (MouseEvent)evt; int x = unscaleCoord(me.getClientX()); - int y = unscaleCoord(me.getClientY()); + int y = unscaleCoord(me.getClientY() - chromeTopCss); if (!hitTest(x, y)) { if (pointerState.isCapturingEvents()) { @@ -2572,9 +2671,15 @@ public void add(String eventName, Object listener, boolean capture) { // scrolling barely registered (a full drag scrolled <1%). onMouseMove / // onTouchMove gate on pointerState.isMouseDown()/isTouchDown(), so they // are no-ops outside an active press (e.g. plain hover). - onMouseMoveHandle = EventUtil.addEventListener(outputCanvas, "mousemove", onMouseMove, true); - onPointerMoveHandle = EventUtil.addEventListener(outputCanvas, "pointermove", onMouseMove, true); - onTouchMoveHandle = EventUtil.addEventListener(outputCanvas, "touchmove", onTouchMove, true); + // + // Registered through the element, like the press listeners above, and not through + // EventUtil: EventUtil hands the browser a raw functor from inside the worker, which the + // worker-callback bridge never sees, so the listener was added and never called. Every + // move of every drag was lost -- a finger could not scroll, and a mouse drag could not + // either -- while the press and the release still arrived (issue #5912). + outputCanvas.addEventListener("mousemove", onMouseMove, true); + outputCanvas.addEventListener("pointermove", onMouseMove, true); + outputCanvas.addEventListener("touchmove", onTouchMove, true); /** * The installbacksidehooks event is an event that can be triggered from native javascript to install @@ -3579,7 +3684,9 @@ public void setGraphicsLocked(boolean locked) { context.rect(frame.getCropX(), frame.getCropY(), frame.getCropW(), frame.getCropH()); context.clip(); // Wipe the drain region only when this frame is repainting the - // *entire* canvas (form transitions, full-screen redraws). Each + // *entire* canvas (form transitions, full-screen redraws) -- which the + // paint reports through pendingFrameClears, because a full-screen crop + // alone does not mean every pixel is repainted (see that field). Each // such drain carries a full paint, so stale pixels must not // bleed through from the previous drain -- without this, title // bars from prior forms accumulated across tests because the new @@ -3608,7 +3715,9 @@ public void setGraphicsLocked(boolean locked) { // the runs of a component that has gone, with nothing else queued -- is exactly that // shape, and a full-screen crop is what a form-sized component asks for when it // repaints. - if (framePaintsPixels(frame) + boolean frameClears = pendingFrameClears; + pendingFrameClears = false; + if (frameClears && framePaintsPixels(frame) && frame.getCropX() == 0 && frame.getCropY() == 0 && frame.getCropW() >= displayWidth && frame.getCropH() >= displayHeight) { @@ -4066,7 +4175,7 @@ private void updateCanvasSize() { refreshDevicePixelRatio(); JavaScriptCanvasLayout.Dimensions dimensions = JavaScriptCanvasLayout.compute( doc().getBody().getClientWidth(), - window.getInnerHeight(), + window.getInnerHeight() - chromeTopCss, devicePixelRatioValue()); canvas.setWidth(dimensions.getBackingWidth()); canvas.setHeight(dimensions.getBackingHeight()); @@ -4472,6 +4581,7 @@ public void installNativeTheme(){ tp.put("@paintsTitleBarBool", "false"); UIManager.getInstance().setThemeProps(tp); + installDesktopChromeIfRequested(); return; } catch (IOException ex){ Log.e(ex); @@ -4521,13 +4631,25 @@ private static boolean isIOSLikeBrowser() { * always points at the OS-appropriate modern .res so screenshot * tests can install it regardless of the configured default. */ - private static String resolveNativeThemeResource() { + private String resolveNativeThemeResource() { Display d = Display.getInstance(); String explicit = d.getProperty("javascript.native.theme", null); if (explicit != null && explicit.length() > 0) { return explicit; } String shared = d.getProperty("nativeTheme", d.getProperty("cn1.nativeTheme", null)); + // A desktop browser takes the desktop native theme of its operating system when the + // app asks for the native look (or pins one with javascript.desktopTheme). Phones and + // tablets -- iPadOS included, which reports a Mac user agent -- keep the branches below. + // The build prunes the bundle from the same table (JavaScriptBuildHints in the Maven + // plugin and in the cloud builder), so change the two together. + if (isDesktop() && !isIOS()) { + String desktop = desktopThemeFor(asciiLower(d.getProperty("javascript.desktopTheme", null)), + asciiLower(shared), desktopOs_()); + if (desktop != null) { + return "/" + desktop + ".res"; + } + } // Auto-detected branch chooses Liquid Glass on iOS/Mac and // Material 3 elsewhere. Used for any "modern"/"auto" path. boolean iosLike = isIOSLikeBrowser(); @@ -4583,6 +4705,165 @@ private static String resolveNativeThemeResource() { return isAndroid_() ? "/android_holo_light.res" : "/iOS7Theme.res"; } + /// The desktop theme for a desktop browser, or null to keep the mobile themes. Mirrors + /// `JavaScriptBuildHints.desktopThemeFor` in the builders, which decide what the bundle + /// carries from the same hints. + static String desktopThemeFor(String desktopHint, String shared, String os) { + if ("fluent".equals(desktopHint) || "windows".equals(desktopHint)) { + return "WindowsFluentTheme"; + } + if ("aqua".equals(desktopHint) || "mac".equals(desktopHint) || "macos".equals(desktopHint)) { + return "MacOSAquaTheme"; + } + if ("adwaita".equals(desktopHint) || "gnome".equals(desktopHint) || "linux".equals(desktopHint)) { + return "GnomeAdwaitaTheme"; + } + // Only "native" reaches the desktop themes by itself: "modern" means the mobile modern + // themes, exactly as it does for the JavaSE desktop build. + if ((desktopHint == null || "auto".equals(desktopHint)) && "native".equals(shared)) { + if ("win".equals(os)) { + return "WindowsFluentTheme"; + } + if ("mac".equals(os)) { + return "MacOSAquaTheme"; + } + return "GnomeAdwaitaTheme"; + } + return null; + } + + /// `win`, `mac` or `linux`: the desktop operating system the browser reports. Everything + /// that is neither Windows nor a Mac -- Linux, ChromeOS, the BSDs -- is closest to GNOME. + @JSBody(params={}, script="var u = navigator.userAgent || '';" + + "if (/Windows|Win32|Win64/.test(u)) return 'win';" + + "if (/Mac/.test(u)) return 'mac';" + + "return 'linux';") + private static native String desktopOs_(); + + private static String asciiLower(String s) { + if (s == null) { + return null; + } + s = s.trim(); + if (s.length() == 0) { + return null; + } + // Hint keywords are ASCII; toLowerCase() is locale sensitive. + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + b.append(c >= 'A' && c <= 'Z' ? (char) (c + 32) : c); + } + return b.toString(); + } + + // --------------------------------------------------------------------------------------- + // Desktop title bar and menu bar + // + // A desktop native theme expects the platform to own the window title and the menu bar. + // A browser page owns neither, so the javascript.titleBar build hint picks what stands in: + // + // - "toolbar" (the default): the Toolbar stays in the app and the desktop theme styles it. + // getConfiguredDesktopTitleBarMode() answers "toolbar", which outranks the theme's own + // desktopTitleBarMode constant (native for Fluent and Aqua, custom for Adwaita) -- without + // that, Form would detach the Toolbar and hand the title and the commands to hooks that + // put them nowhere. + // - "html": JavaScriptDesktopChrome draws a title bar and a menu bar in the document above + // the canvas, and this port then answers the native-title and native-command hooks the + // way the Windows and Linux ports do. + // --------------------------------------------------------------------------------------- + + /// The `javascript.textSelection` build hint: read-only text becomes selectable and copyable + /// in every form, through the framework's own TextSelection -- the canvas owns pointer input, + /// so the browser's native selection cannot reach the text (see the text layer's comment in + /// __init). The trigger is TextSelection's platform default: a press-drag with a mouse, a + /// long press on a touch screen, so a swipe over text still scrolls. + private void applyTextSelectionHint(Form f) { + if (f == null || !"true".equals(asciiLower(Display.getInstance().getProperty("javascript.textSelection", null)))) { + return; + } + TextSelection.setDefaultSelectable(true); + TextSelection sel = f.getTextSelection(); + if (!sel.isEnabled()) { + sel.setEnabled(true); + } + } + + private JavaScriptDesktopChrome desktopChrome; + + /// CSS pixels the HTML chrome takes from the top of the page; 0 without it. The body -- and + /// with it the canvas and every overlay positioned inside it -- starts below the chrome, so + /// only viewport coordinates (a pointer event's clientY) need this subtracted. + private int chromeTopCss; + + private void installDesktopChromeIfRequested() { + if (desktopChrome != null || !isDesktop() || isIOS()) { + return; + } + if (!"html".equals(asciiLower(Display.getInstance().getProperty("javascript.titleBar", null)))) { + return; + } + desktopChrome = new JavaScriptDesktopChrome((HTMLDocument) doc(), outputCanvas, desktopOs_(), + new JavaScriptDesktopChrome.CommandSink() { + public void commandChosen(final Command cmd) { + Display.getInstance().callSerially(new Runnable() { + public void run() { + dispatchNativeMenuCommand(cmd); + } + }); + } + }); + chromeTopCss = desktopChrome.getHeight(); + HTMLElement body = doc().getBody(); + body.getStyle().setProperty("top", chromeTopCss + "px"); + body.getStyle().setProperty("height", "calc(100% - " + chromeTopCss + "px)"); + updateCanvasSize(); + sizeChanged(getDisplayWidth(), getDisplayHeight()); + } + + @Override + public boolean isNativeCommandsSupported() { + return desktopChrome != null; + } + + @Override + public boolean isNativeTitle() { + return desktopChrome != null; + } + + @Override + public void refreshNativeTitle() { + if (desktopChrome == null) { + return; + } + Form f = getCurrentForm(); + // A Dialog is painted inside the form rather than owning the window; letting it retitle + // the bar would leave the wrong title behind once it is dismissed. + if (f != null && !(f instanceof com.codename1.ui.Dialog)) { + desktopChrome.setTitle(f.getTitle()); + } + } + + @Override + public String getConfiguredDesktopTitleBarMode() { + if (!isDesktop()) { + return null; + } + return desktopChrome != null ? "native" : "toolbar"; + } + + @Override + public String getDesktopTitleBarMode() { + return desktopChrome != null ? "native" : "toolbar"; + } + + @Override + public void setNativeCommands(java.util.Vector commands) { + if (desktopChrome != null) { + desktopChrome.setCommands(commands); + } + } + @Override public boolean hasNativeTheme() { return true; @@ -6984,6 +7265,12 @@ public void setGraphicsLocked(boolean locked) { graphicsLocked = locked; } }, pendingDisplay, flushedOps, x, y, width, height); + // A transition frame draws two buffered images that cover the screen and is not a + // component paint, so beforeComponentPaint never sees it. + if (paintCoversScreen || Display.getInstance().isInTransition()) { + pendingFrameClears = true; + } + paintCoversScreen = false; } beginGraphicsAtomic(); try { @@ -11722,6 +12009,10 @@ private void leaveDocument() { @Override public void setCurrentForm(Form f) { super.setCurrentForm(f); + if (desktopChrome != null && f != null && !(f instanceof com.codename1.ui.Dialog)) { + desktopChrome.setTitle(f.getTitle()); + } + applyTextSelectionHint(f); // A form built before the last scheme or density change still holds the styles it // resolved then, so it is brought up to date as it appears rather than coming back in // the old palette. @@ -14003,20 +14294,23 @@ public String getSelectedText() { return selectedText; } - @JSBody(params={"evt", "content"}, script="try { evt.clipboardData.setData('text/plain', content);} catch (e){}") - private native static void setClipboardData(Event evt, String content); - EventListener copyListener = new EventListener() { @SuppressSyncErrors public void handleEvent(Event evt) { if (selectedText == null || selectedText.isEmpty()) { return; } - if (!jQuery_is_(outputCanvas, ":focus")) { + // A copy while a native field is being edited is that field's copy. + if (isEditing) { return; } - setClipboardData(evt, selectedText); - evt.preventDefault(); + // Not through the event. This listener runs in the worker, after the browser has + // finished dispatching the copy event, so writing into its clipboardData does + // nothing -- and the canvas, which has no tabindex, never holds focus, so the old + // ":focus" gate returned before even trying. Ctrl/Cmd+C over a selection copied + // nothing at all. copyToClipboard goes through the main thread's clipboard API, + // which the key press that raised this event has just granted. + copyToClipboard(selectedText); } }; @@ -14060,7 +14354,7 @@ public void handleEvent(final MouseEvent evt) { public void run() { _log("Showing context menu"); MouseEvent me = lastMouseEvent != null ? lastMouseEvent : evt; - ContextMenu.showAt(unscaleCoord(me.getClientX()) + CN.convertToPixels(2), unscaleCoord(me.getClientY()) + CN.convertToPixels(2)); + ContextMenu.showAt(unscaleCoord(me.getClientX()) + CN.convertToPixels(2), unscaleCoord(me.getClientY() - chromeTopCss) + CN.convertToPixels(2)); } }); @@ -14207,7 +14501,17 @@ private void refreshThemeIfStale(Form f) { } f.putClientProperty(THEME_GENERATION_PROPERTY, Integer.valueOf(themeGeneration)); if (generation != 0 || themeGeneration != 0) { + // The refresh builds new Style instances, and a background painter is not something + // the theme supplies -- it was installed on the form itself. A Dialog is the case + // that matters: showModal installs the painter that draws the tinted form behind the + // dialog and only then makes the dialog current, which is what calls this. Dropping + // it left every dialog floating on a blank page (issue #5910), and a painter an + // application set with setBgPainter was lost the same way. + com.codename1.ui.Painter painter = f.getUnselectedStyle().getBgPainter(); f.refreshTheme(); + if (painter != null) { + f.getUnselectedStyle().setBgPainter(painter); + } } } diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java index 10f36189818..92e17d0cfe2 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java @@ -681,6 +681,13 @@ public interface InputEvent extends Event { + /// The part of a PointerEvent the port reads to tell a finger from a mouse. A plain mouse + /// event has no such property and answers null. + public interface PointerTypeEvent extends Event { + @JSProperty + public String getPointerType(); + } + public interface WheelEvent extends MouseEvent { @JSProperty public double getDeltaX(); diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JavaScriptDesktopChrome.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JavaScriptDesktopChrome.java new file mode 100644 index 00000000000..a782a912ce7 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JavaScriptDesktopChrome.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.html5; + +import com.codename1.html5.js.dom.Event; +import com.codename1.html5.js.dom.EventListener; +import com.codename1.html5.js.dom.HTMLDocument; +import com.codename1.html5.js.dom.HTMLElement; +import com.codename1.ui.Command; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +/// The HTML title bar and menu bar a desktop browser shows when the `javascript.titleBar=html` +/// build hint asks for them. +/// +/// A desktop native theme expects the platform to own the window title and the menu bar: the +/// Windows, Linux, macOS and JavaSE ports hide the Toolbar and hand the form title to the OS +/// window and the commands to the OS menu. A page has neither, so this draws both in the +/// document, above the canvas, and the port then answers the native-title and native-command +/// hooks exactly as those ports do. +/// +/// The menus are `
` elements, so opening, closing and keyboard activation are the +/// browser's own and cost no round trip to the worker. Only choosing an item comes back here. +/// +/// There are no minimize, maximize or close buttons: a page cannot do any of those to the +/// browser window, and a control that does nothing is worse than none. +final class JavaScriptDesktopChrome { + + /// Receives a chosen menu item; called on the worker, not the EDT. + interface CommandSink { + void commandChosen(Command cmd); + } + + /// The standard top-level menus and where each placement hint lands, in the order desktop + /// applications show them. The same table as the Windows port's cn1_windows_menu.cpp, so an + /// application's menu looks the same in the browser as it does in the native build. A page + /// has no application menu, so the App, About, Preferences and Quit hints go where Windows + /// puts them. + private static final String[][] MENU_HINTS = { + {"File", "File"}, + {"Edit", "Edit"}, + {"View", "View"}, + {"Window", "Window"}, + {"Help", "Help"}, + {"About", "Help"}, + {"Preferences", "File"}, + {"Quit", "File"}, + {"App", "File"}, + }; + + private static final String[] MENU_ORDER = {"File", "Edit", "View", "Window"}; + + private final HTMLDocument document; + private final HTMLElement root; + private final HTMLElement title; + private final HTMLElement menuBar; + private final CommandSink sink; + private final boolean mac; + private final List openMenus = new ArrayList(); + + JavaScriptDesktopChrome(HTMLDocument document, HTMLElement appSurface, String os, CommandSink sink) { + this.document = document; + this.sink = sink; + this.mac = "mac".equals(os); + root = document.createElement("div"); + root.setAttribute("id", "cn1-desktop-chrome"); + // The platform look, and its dark variant through prefers-color-scheme, live in + // style.css: an external stylesheet, so the page's content security policy is unaffected. + root.setAttribute("class", "cn1-chrome-" + os); + title = document.createElement("div"); + title.setAttribute("class", "cn1-chrome-title"); + root.appendChild(title); + menuBar = document.createElement("nav"); + menuBar.setAttribute("class", "cn1-chrome-menubar"); + menuBar.setAttribute("aria-label", "Application menu"); + root.appendChild(menuBar); + document.getBody().appendChild(root); + + // Escape closes an open menu, as it does in every native menu bar. + root.addEventListener("keydown", new EventListener() { + public void handleEvent(Event evt) { + if (((JSOImplementations.KeyEvent) evt).getKeyCode() == 27) { + closeMenus(); + } + } + }); + // A press on the app closes an open menu. Capture phase, so the app's own handling of + // the press cannot stop it first. The canvas is the whole app surface, so listening + // there -- rather than on the document and working out whether the target lies inside + // the chrome -- needs no DOM walk per press. + appSurface.addEventListener("pointerdown", new EventListener() { + public void handleEvent(Event evt) { + if (!openMenus.isEmpty()) { + closeMenus(); + } + } + }, true); + } + + /// The height the chrome takes from the top of the page, in CSS pixels. + int getHeight() { + return root.getOffsetHeight(); + } + + void setTitle(String text) { + title.setTextContent(text == null ? "" : text); + } + + /// Replaces the menus with `commands`, grouped by [Command#getDesktopMenu()]. + void setCommands(Vector commands) { + closeMenus(); + menuBar.setInnerHTML(""); + Map> groups = group(commands); + for (Map.Entry> e : groups.entrySet()) { + menuBar.appendChild(buildMenu(e.getKey(), e.getValue())); + } + } + + /// Commands keyed by top-level menu title, in menu-bar order: the standard menus first, then + /// the application's own titles in the order they first appear, then Help last. + static Map> group(Vector commands) { + Map> byTitle = new HashMap>(); + List custom = new ArrayList(); + if (commands != null) { + for (int i = 0; i < commands.size(); i++) { + Object o = commands.elementAt(i); + if (!(o instanceof Command)) { + continue; + } + Command c = (Command) o; + String name = c.getCommandName(); + if (name == null || name.length() == 0) { + continue; + } + String menu = titleForHint(c.getDesktopMenu()); + List list = byTitle.get(menu); + if (list == null) { + list = new ArrayList(); + byTitle.put(menu, list); + if (!isStandard(menu)) { + custom.add(menu); + } + } + list.add(c); + } + } + Map> ordered = new java.util.LinkedHashMap>(); + for (String m : MENU_ORDER) { + if (byTitle.containsKey(m)) { + ordered.put(m, byTitle.get(m)); + } + } + for (String m : custom) { + ordered.put(m, byTitle.get(m)); + } + if (byTitle.containsKey("Help")) { + ordered.put("Help", byTitle.get("Help")); + } + return ordered; + } + + static String titleForHint(String hint) { + if (hint == null || hint.length() == 0) { + // Where the Windows port puts a command with no placement hint. + return "Commands"; + } + for (String[] row : MENU_HINTS) { + if (row[0].equalsIgnoreCase(hint)) { + return row[1]; + } + } + // An unrecognised hint is a menu titled with the hint itself. + return hint; + } + + private static boolean isStandard(String menu) { + for (String m : MENU_ORDER) { + if (m.equals(menu)) { + return true; + } + } + return "Help".equals(menu); + } + + private HTMLElement buildMenu(String menuTitle, List commands) { + final HTMLElement details = document.createElement("details"); + details.setAttribute("class", "cn1-chrome-menu"); + HTMLElement summary = document.createElement("summary"); + summary.setTextContent(menuTitle); + details.appendChild(summary); + HTMLElement list = document.createElement("div"); + list.setAttribute("class", "cn1-chrome-menu-items"); + list.setAttribute("role", "menu"); + for (final Command c : commands) { + HTMLElement item = document.createElement("button"); + item.setAttribute("type", "button"); + item.setAttribute("role", "menuitem"); + item.setAttribute("class", "cn1-chrome-menu-item"); + if (!c.isEnabled()) { + item.setAttribute("disabled", "disabled"); + } + HTMLElement label = document.createElement("span"); + label.setTextContent(c.getCommandName()); + item.appendChild(label); + String shortcut = shortcutText(c, mac); + if (shortcut != null) { + HTMLElement accel = document.createElement("span"); + accel.setAttribute("class", "cn1-chrome-shortcut"); + accel.setTextContent(shortcut); + item.appendChild(accel); + } + item.addEventListener("click", new EventListener() { + public void handleEvent(Event evt) { + closeMenus(); + sink.commandChosen(c); + } + }); + list.appendChild(item); + } + details.appendChild(list); + // Opening one menu closes the others, as in a native menu bar. + details.addEventListener("toggle", new EventListener() { + public void handleEvent(Event evt) { + if (details.hasAttribute("open")) { + for (HTMLElement other : new ArrayList(openMenus)) { + if (other != details) { //NOPMD CompareObjectsWithEquals + other.removeAttribute("open"); + } + } + openMenus.clear(); + openMenus.add(details); + } else { + openMenus.remove(details); + } + } + }); + return details; + } + + /// The accelerator as text, the way the Windows menu writes it into the label. The page + /// does not bind the keys itself: the application's own key listeners already see them. + static String shortcutText(Command c, boolean mac) { + int key = c.getDesktopShortcutKeyChar(); + if (key <= 0) { + return null; + } + int mods = c.getDesktopShortcutModifiers(); + StringBuilder sb = new StringBuilder(); + if ((mods & Command.DESKTOP_SHORTCUT_MODIFIER_PRIMARY) != 0) { + // The primary modifier is Command on a Mac and Control everywhere else. + sb.append(mac ? "Cmd+" : "Ctrl+"); + } + if ((mods & Command.DESKTOP_SHORTCUT_MODIFIER_ALT) != 0) { + sb.append(mac ? "Option+" : "Alt+"); + } + if ((mods & Command.DESKTOP_SHORTCUT_MODIFIER_SHIFT) != 0) { + sb.append("Shift+"); + } + sb.append(Character.toUpperCase((char) key)); + return sb.toString(); + } + + void closeMenus() { + for (HTMLElement m : new ArrayList(openMenus)) { + m.removeAttribute("open"); + } + openMenus.clear(); + } +} diff --git a/Ports/JavaScriptPort/src/main/webapp/style.css b/Ports/JavaScriptPort/src/main/webapp/style.css index b8699eca8d4..0ba5649ea6f 100755 --- a/Ports/JavaScriptPort/src/main/webapp/style.css +++ b/Ports/JavaScriptPort/src/main/webapp/style.css @@ -298,3 +298,143 @@ div#cn1-video-capture > video { padding: 1mm; } + +/* The HTML title bar and menu bar of the javascript.titleBar=html build hint + (JavaScriptDesktopChrome). The port moves the body down by the chrome's height, + so the canvas and every overlay positioned inside the body start below it. The + heights are fixed so the port measures the chrome once. */ +#cn1-desktop-chrome { + position: fixed; + left: 0; + right: 0; + top: 0; + z-index: 2147483647; + font: 13px system-ui, -apple-system, "Segoe UI", Cantarell, Ubuntu, sans-serif; + color: #1f1f1f; + background: #f3f3f3; + border-bottom: 1px solid #d6d6d6; + user-select: none; + -webkit-user-select: none; +} +#cn1-desktop-chrome .cn1-chrome-title { + height: 30px; + line-height: 30px; + padding: 0 12px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +#cn1-desktop-chrome .cn1-chrome-menubar { + display: flex; + height: 28px; + padding: 0 4px; + align-items: stretch; +} +#cn1-desktop-chrome .cn1-chrome-menu { + position: relative; +} +#cn1-desktop-chrome .cn1-chrome-menu > summary { + list-style: none; + height: 100%; + line-height: 28px; + padding: 0 10px; + border-radius: 4px; + cursor: default; +} +#cn1-desktop-chrome .cn1-chrome-menu > summary::-webkit-details-marker { + display: none; +} +#cn1-desktop-chrome .cn1-chrome-menu > summary:hover, +#cn1-desktop-chrome .cn1-chrome-menu[open] > summary { + background: rgba(0, 0, 0, 0.07); +} +#cn1-desktop-chrome .cn1-chrome-menu > summary:focus-visible, +#cn1-desktop-chrome .cn1-chrome-menu-item:focus-visible { + outline: 2px solid #0067c0; + outline-offset: -2px; +} +#cn1-desktop-chrome .cn1-chrome-menu-items { + position: absolute; + left: 0; + top: 100%; + min-width: 200px; + padding: 4px; + display: flex; + flex-direction: column; + background: #f9f9f9; + border: 1px solid #d6d6d6; + border-radius: 6px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.14); +} +#cn1-desktop-chrome .cn1-chrome-menu-item { + display: flex; + justify-content: space-between; + gap: 24px; + padding: 6px 10px; + border: 0; + border-radius: 4px; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: default; +} +#cn1-desktop-chrome .cn1-chrome-menu-item:hover:not([disabled]) { + background: rgba(0, 0, 0, 0.07); +} +#cn1-desktop-chrome .cn1-chrome-menu-item[disabled] { + opacity: 0.45; +} +#cn1-desktop-chrome .cn1-chrome-shortcut { + opacity: 0.6; +} + +/* macOS: the title centred in a lighter bar, the system font, rounded selection. */ +#cn1-desktop-chrome.cn1-chrome-mac { + background: #ececec; + border-bottom-color: #cfcfcf; +} +#cn1-desktop-chrome.cn1-chrome-mac .cn1-chrome-title { + text-align: center; + font-weight: 600; +} +#cn1-desktop-chrome.cn1-chrome-mac .cn1-chrome-menu-item:hover:not([disabled]) { + background: #0a64d8; + color: #fff; +} + +/* GNOME: a HeaderBar-style bar with the title centred in bold. */ +#cn1-desktop-chrome.cn1-chrome-linux { + background: #ebebeb; + border-bottom-color: #d4d4d4; +} +#cn1-desktop-chrome.cn1-chrome-linux .cn1-chrome-title { + text-align: center; + font-weight: 700; +} +#cn1-desktop-chrome.cn1-chrome-linux .cn1-chrome-menu-items { + border-radius: 12px; +} + +@media (prefers-color-scheme: dark) { + #cn1-desktop-chrome { + color: #f0f0f0; + background: #202020; + border-bottom-color: #3a3a3a; + } + #cn1-desktop-chrome.cn1-chrome-mac { + background: #2c2c2c; + } + #cn1-desktop-chrome.cn1-chrome-linux { + background: #303030; + } + #cn1-desktop-chrome .cn1-chrome-menu > summary:hover, + #cn1-desktop-chrome .cn1-chrome-menu[open] > summary, + #cn1-desktop-chrome .cn1-chrome-menu-item:hover:not([disabled]) { + background: rgba(255, 255, 255, 0.1); + } + #cn1-desktop-chrome .cn1-chrome-menu-items { + background: #2b2b2b; + border-color: #3f3f3f; + } +} diff --git a/Themes/GnomeAdwaitaTheme.res b/Themes/GnomeAdwaitaTheme.res index c23a9f8c5e9a858e5aaaa87beb7b6a5c0ecfd8b7..7ef22acc1d65ace41dcbe8a66dbb34137ca93612 100644 GIT binary patch delta 11528 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 delta 125 zcmV-@0D}L?x(lw63Xl~959|OOlRyF$u`XE)vjGqAk(08h7qhsgtPYnR3;_nS!o4m9 zv#82LJC~ah0SdQ+2?6miw_iH}q!hPrlv0q diff --git a/Themes/MacOSAquaTheme.res b/Themes/MacOSAquaTheme.res index c4a7019a2eee574cbbd1f491d413722161b6a86e..e3d8448a92d3f67a2bf4c953edc15e268d90ba1a 100644 GIT binary patch delta 12704 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 delta 113 zcmV-%0FM8ztO}EZ36K>84qyNrlYs>lu`bvNvw#g2f&m?qftVJvU6suZlYp8AvxuuM z1(Sdc2D5ayFfy|;?n*4TXBz=W5w|Wb0g3^)nKJ=)2Dkq@0o4t+p-utNsI~b5tQWTe TCjvbOw=X*a-9EPgfC4QRCMGD) diff --git a/Themes/WindowsFluentTheme.res b/Themes/WindowsFluentTheme.res index 6e6b04568419503efc18665721cdbc728270137a..3cdfb38bd7146cfad2a262d3f4db9249dc6130e7 100644 GIT binary patch delta 11418 zcmb7KeQZ?65r6w~ci49}_W8cg_W6u$z<{qdfdnvM1Nl-*j7>w^G%3Wyj$_%J4LDKy zB|?=_L8KCU30tX2fT;be3B*FhiIk=xsZ>>hB$f(QDz!?~KcbK-Kp|28sN(L-zK?zH z?K=}BOYe4OXJ%(+XMa1(d#A&n{w?xcA`rc()zNM}FbR!gba-`^WD|nidX+B7Ba?MLSlmEw^~+WKW`UZf;I%G-uI` z+iGd|1EC6OPJu=%W4Cb`Z1x;U($j~-vPAare@G|Ie;EqfQ|R>P@o2>t| zdqV+WVP_VQWTLgTRjao+j5V<63LRz13Md(im0QB-Twg8S^-5Wlt!<#Ne{hsG9*)Vg z*ws(STG<&oOwXP=5Z#0tW-_YW4I*!;xA#8|PgduYqp=-56D z{w2|7iHL5eV_QQ37-WrE5~C~5hVd6&zOu_EZR+5pZyh#B*UqHq_~A0a_mjgJF`ay$ zB-qo>>H)SbKn#(ylVnp^_zG0eGv_nmMV1-#>T4PLg9agpJ@O?!I*pQ!0>)L8;mV< zj?=~;t)o|0g;qOJV9lBI0#vwDo~3WTa$ zQ$^Mt)jb5ry!aA@8SMIx^<*NipiIzr|ue|)qLOK==m1AtaJv=Z-yL;j(5nF4_Q88%OJrX+> zjigwan;&WiaJRb&p3p5u&sc1We4?s@1WlwFdg9zQ}mV@z?i-+2; zSgKrMfNE9ca-%3&7mt-a?os5-1+jB1rk?&MhllznhR2G%{l&piYftC%d9B&$B^N7w z@6vjG{3+VslAxU*h6Ba1;vl>B3`x+PAI9n9AJ%N|)LKhQz|=5Z0191UaVg~Nta_)O zqn}-BD5tw!Gv7y6Jo@x2z-!i|o{!<~1$fskN6BL$5DN^v&Zos0?4j zZJeD&ck1=GVq9sz(aGJhhSF{*(B3cNXJ42u1j7Q8n03YgCGgYS;4&zIaT}CKTu@k6 zh#Do0da8;zynzD?3P&l6WOlih8M$cNW|RLR^r=o;6@Y z7HJTQgn@rxbZUDRAWNIX;V2DICtsyWS~P&=>krLuCd$5>2M3f$d zJg5cqm!qLg3YsHFpmzFuNZS8j(=uNdK_STc`k_GgR>aSKaPH;?y0J18d|-HD6rn~; zR0RC(r~wfHV3B}D008cS2+TA1pb8WuE_1pY7YaBaGy`>zoh`2$P2sL3(w9H`) zo!yag3}6j#cn)v@-FC_}1TQf^{drkdHo>{&&%S%}lXkYXkwk9eS0=dev46Ib3ijN3 z5|OTK?EDszWUn-nh~LqmOjBc&~3D8Fsma)XcouLVD8dkDEzE+-joh z$TIOYndLJdUSbleDoLZw2lo@!km1IStt1gQivFOLWKn6g8ws;hX%c5gxjpVV?aN3V z3Ug!r8E$N~&JN^A#0&psTlqr3$SMm%iL?+-`ifJ$lh9LY61r=AscIJZtZ7;Iqs5N4 z-NVI!b(TU`;bo+gWSa`Nz}R;W$u+g#0%NhAvo>?(ry#$?)ETaI(Ub1<&auV(U1l+m+m2jsz3~ODev%9*2ow_vl{+5(~}7R8@<@mY)m~mrKgS0SEuymKpl{I%O?n0N7#6f z#Df|9glbuR<;oQ;4L{Au!=1}u`1d)Itw3IbNEH!LZl@CG}_QxD?Yk`53E3~xWLFAjjk&~aKmhb}e!;5eDFDLA!cfZw^AxME`1`<_OQs%^l>0 zx0-A0`e@ztKyp+XXXH9rElM(Utjd9&06Mg+f=T#d(64F%bMdFq3xm^GwiYL`y_@w# z#;lH)Ny_*<_+8$s`QWVuw0&E7uok8BuGa=llPxrlYfvPQ%St56X>=-GFZ0Q1g5$L6 ze3CtVj)*oFvpQFCB^+d>?&5vZ7UX zeNBSS9jXr2<6?N{Z2|8HqAcEcOACKO%S||dpJ#?EX!pCR2)atRk&T}qbq3SPbSffw znC|_m-f-6Rk&G~i@)ljYrr`plj+P_3k_bFRL_fG4-7e3OVK+oQ3+CGl(4Om4PLczq zWRN4~lFE7~NPUNLmK;Af13SM^udmKKEu6p};5R+aknQsjW!v1If|P3W%#iJJ6Y2E- zGS!#^w&NzSzgS)8HWo_K?QUL0S#q<`$rocDX>{^X&ZC$fOvb!}*A6vQEpi%zZA))P zKy@-NVv3Bw_T_eWXs`szE>|+hZg-``1nvQEPJlJC@7;1?e^j|r0(zt#U(Fnh>%Yx5 zCoM>2R^$HB;h~~Nw6sNmCTUH|`e`!ap}REe8HX)o3aPWGcrHh4t>1;q{q2Xc|j~o77xNxzH|GC65I;F?gs8w)Z4-6NFxdA1=VBu$r^Ko_AHw3%}PRPcW-OHQd zecmktyV|>DOq<;m$dl@L5mxLXm?#$^+l#VjIkFznSjc>$>lDW>9arumqhigoi?DmY zn?KZ8iD;VW>d;wq*WJfD?Qh(;p*8q&u@k%WPT$@+d;KB3=04t*^PMl_1JGn|9Jf89 zSHGdOuDj>cWdsKf9)De|Z-UfqDQ&d5@iblYUmhTeID0##myfiTbb%a{IW6zYE1T~d z6hIi={xW3C{khn5N>67N`&q(f|4Vq|(wEHNE;g4ARc)u{D;>~?zi@yhcl*vYz>d61 zUtW6cP!hwFS?;$IXqn#Yp9*pf*0PB z3C!V-$lr`_{zB*0KUOoTZ|GYWWTR5&rC?TOY^nels;rU8h=Xzoc}RvpDIgeK35f@Q z&k)kgLg&Uj9-`q&ggZ$|R4Jq*v_oT~;E~5uPFa=fKsLadAg);SJ%k_tGrvVIe^xg-jki<2oEQyAfRKi zx%se}BH0IB7Nv{88)T3H7BN;UwOk|t`2N+53o{M%A`szE3w%T*4c57;m(rpY3$cKIarY2u^Lo@+=u}a z*lmsg3v1`sclRynGXj99L8T7?!&+Z+&FuiCR>fq)gom5`mh40b${5I2s$#$hBbn7I z1+sxk(TE1(jf%Ml1mbxWg=2t?^G6^rM?j?kz<^Q#;DAom3D3s%jtQ}8SlLe00O2_LaJTrvE z1bk;OIQgk+sNkpS5XU+~aic!nLdM&W6})M*`bl}@|(g?oLAY^kD) zXRHif7fd7JdBGHY@_D^Zn86RWrTE2wQ>1{|5vwlN~b3})YMfG(}4m*I~Jg=ac pWJLDJaWfL|nqbD8pFl=@_-lfQ58h4dHD?^0hs?|`ey0C9{(lqZlLr6* delta 118 zcmV-+0Ez$U*9*?B3Xl~95B~rhlRyF$u`XE)vjGoGmy)PDhuzPIT| Y0+t%L+;9Rt2e%1@0wqkhBi91P6qjl(vj6}9 diff --git a/docs/developer-guide/Native-Themes.asciidoc b/docs/developer-guide/Native-Themes.asciidoc index c30fb1f17db..32dfefdac70 100644 --- a/docs/developer-guide/Native-Themes.asciidoc +++ b/docs/developer-guide/Native-Themes.asciidoc @@ -147,6 +147,11 @@ Variable, SF or Cantarell -- and the Fluent and Adwaita images fall back to the Mac's system font. On a real Windows or Linux desktop the text looks different. +In a web browser, `nativeTheme=native` selects the desktop theme of the +browser's operating system, and the `javascript.titleBar` build hint decides +whether the title and the commands stay in the Toolbar or move to an HTML title +bar and menu bar. See <>. + To preview a desktop theme in the simulator, pick the Desktop skin and choose the theme from the *Native Theme* menu. With *Auto* selected the Desktop skin follows `desktop.themeMode`, which is what the packaged diff --git a/docs/developer-guide/Working-With-Javascript.asciidoc b/docs/developer-guide/Working-With-Javascript.asciidoc index ee7c1744712..987728eb70b 100644 --- a/docs/developer-guide/Working-With-Javascript.asciidoc +++ b/docs/developer-guide/Working-With-Javascript.asciidoc @@ -519,7 +519,56 @@ For example: `MacIntel`, `Win32`, `FreeBSD i386`, "WebTV OS" [id="native_theme", reftext="Changing the Native Theme"] === Changing the native theme -The JavaScript port selects a bundled native theme from the detected browser platform. You can override this behavior by setting the `javascript.native.theme` https://www.codenameone.com/javadoc/com/codename1/ui/Display.html[Display] property to the path of a theme included with your app. Place additional JavaScript-only themes in `javascript/src/main/resources` so they aren't bundled into other platforms. +The JavaScript port picks a native theme from the build hints and the browser it runs in. The shared `nativeTheme` hint, and the per-platform `ios.themeMode` and `and.themeMode` hints, apply here as they do on a device: + +* Unset, an Android browser gets the Holo Light theme and every other browser the iOS 7 theme. +* `nativeTheme=modern` gives iOS and Mac browsers the Liquid Glass theme and every other browser Material 3. +* `nativeTheme=native` does the same on phones and tablets, and gives a desktop browser the desktop theme of its operating system: Windows 11 Fluent, macOS Aqua, or GNOME Adwaita for Linux and everything else. An iPad counts as a tablet even though it reports a Mac browser. + +Two JavaScript hints refine that: + +[cols="1,2,3", options="header"] +|=== +|Hint |Values |Description + +|`javascript.desktopTheme` +|`auto` (default) + +`fluent` / `aqua` / `adwaita` + +`none` +|`auto` follows the operating system as described above. Naming a theme gives every desktop browser that theme, whatever it runs on. `none` keeps the mobile themes on the desktop too. + +|`javascript.native.theme` +|a resource path, such as `/WindowsFluentTheme.res` +|Loads this theme in every browser, instead of the one the hints and the browser would pick. Place JavaScript-only themes in `javascript/src/main/resources` so they aren't bundled into other platforms. +|=== + +The build ships only the themes the application can reach. Pinning a desktop theme, or leaving the desktop themes out, keeps the others out of the bundle. A theme your own code opens by name, such as `Resources.open("/GnomeAdwaitaTheme.res")`, is always kept. + +==== Title bar and menu bar on the desktop + +A desktop native theme expects the operating system to own the window title and the menu bar. The native desktop ports hide the Toolbar and hand the form title to the window and the commands to the system menu bar. A web page has neither, so the `javascript.titleBar` build hint decides what stands in for them: + +* `toolbar` (default) keeps the Toolbar inside the app, styled by the desktop theme, with its hamburger side menu and overflow menu. +* `html` draws a title bar and a menu bar in the page, above the app. The title bar shows the current form's title, and the menu bar lists every Toolbar command, grouped into menus by `Command.setDesktopMenu()` exactly as the native Windows menu bar groups them. A command with no menu hint goes into a *Commands* menu. The bars follow the operating system's look and the browser's light or dark preference. There are no minimize, maximize or close buttons, because a page can't do any of those to the browser window. + +The browser tab title is the application's display name in both modes. + +=== Page settings + +The generated `index.html` sets up the page for the application: + +* The page title is the application's display name, `codename1.displayName`. +* A `viewport` meta tag gives mobile browsers the device's width, so the app starts at the phone's own scale rather than as a shrunken desktop page. Pinch zoom stays available. +* Browser translation is off: the page carries `` and `translate="no"`. The app draws its own text, and a browser translating the page under it, or loading it through a translation site, breaks it. Set `javascript.allowBrowserTranslation=true` to let the browser offer translation. +* The page carries ``, which asks the Dark Reader extension to leave the app's own colors alone. Dark Reader honors it in its default Dynamic mode only; its Filter, Filter+ and Static modes ignore it. Set `javascript.darkreaderLock=false` to omit it. + +=== Selecting and copying text + +The app's text is present in the page, so find-in-page matches it and assistive technology reads it. Selecting it with the mouse isn't available by default, because the app itself receives every press and drag. + +Set the `javascript.textSelection=true` build hint to make read-only text selectable in every form: labels, span labels and non-editable text areas. With a mouse, press and drag across the text; on a touch screen, long-press it, so a swipe over text still scrolls. Copy the selection with the platform's copy shortcut. Buttons, editable fields and text inside a lead component such as a `MultiButton` never take part, because their press belongs to the control. + +The hint is the equivalent of calling `TextSelection.setDefaultSelectable(true)` and enabling `Form.getTextSelection()` on every form. An individual component still decides for itself with `setTextSelectionEnabled()`. === Disabling the 'OnBeforeUnload' handler diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java index 49e9274bfdd..8b6e50b350d 100644 --- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java +++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java @@ -65,6 +65,47 @@ static void register(List h) { + "bundle and configures the app to use it. Setting this to `false` disables both proxy " + "generation and proxy URL injection.")); + h.add(new Hint("javascript.allowBrowserTranslation") + .group(HintGroup.JAVASCRIPT) + .type(HintType.BOOLEAN) + .def("false") + .platform("javascript") + .doc("true/false (defaults to `false`). By default the page opts out of browser " + + "machine translation (`` and " + + "`translate=\"no\"`), because a translator rewriting the page under the running " + + "app, or loading it through a translation proxy, breaks it. Set to `true` to let " + + "the browser offer translation.")); + + h.add(new Hint("javascript.darkreaderLock") + .group(HintGroup.JAVASCRIPT) + .type(HintType.BOOLEAN) + .def("true") + .platform("javascript") + .doc("true/false (defaults to `true`). Emits `` so the " + + "Dark Reader extension leaves the app's own colors alone. Dark Reader honours the " + + "lock in its default Dynamic mode only; its Filter, Filter+ and Static modes " + + "ignore it. Set to `false` to omit the tag.")); + + h.add(new Hint("javascript.desktopTheme") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("auto") + .valuePattern("auto|fluent|aqua|adwaita|none") + .platform("javascript") + .doc("The native theme a desktop browser gets. `auto` (default) picks Windows Fluent, " + + "macOS Aqua or GNOME Adwaita from the browser's operating system when " + + "`nativeTheme=native`; phones and tablets keep the iOS and Android themes. " + + "`fluent`, `aqua` or `adwaita` pins one theme for every desktop browser, so " + + "the bundle carries only that one. `none` keeps the mobile themes on the " + + "desktop too.")); + + h.add(new Hint("javascript.native.theme") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .platform("javascript") + .doc("An explicit native theme resource, such as `/WindowsFluentTheme.res`, used in " + + "every browser instead of the one the theme hints and the browser would pick.")); + h.add(new Hint("javascript.portSources") .group(HintGroup.JAVASCRIPT) .type(HintType.STRING) @@ -95,6 +136,28 @@ static void register(List h) { + "generated proxy packaging unless `javascript.proxy.target` is also set. If " + "`javascript.inject_proxy` is `false`, this build hint is ignored.")); + h.add(new Hint("javascript.textSelection") + .group(HintGroup.JAVASCRIPT) + .type(HintType.BOOLEAN) + .def("false") + .platform("javascript") + .doc("true/false (defaults to `false`). Makes read-only text -- labels, span labels and " + + "non-editable text areas -- selectable and copyable in every form, using the " + + "framework's own text selection. A mouse selects by dragging; a touch screen " + + "selects with a long press, so a swipe still scrolls.")); + + h.add(new Hint("javascript.titleBar") + .group(HintGroup.JAVASCRIPT) + .type(HintType.STRING) + .def("toolbar") + .valuePattern("toolbar|html") + .platform("javascript") + .doc("How a desktop native theme presents the form title and commands in a browser. " + + "`toolbar` (default) keeps the Toolbar in the app, styled by the desktop theme. " + + "`html` shows the title in an HTML title bar and the commands in an HTML menu " + + "bar above the app, the way the native desktop ports use the window title and " + + "the system menu bar.")); + h.add(new Hint("linux.arch") .group(HintGroup.LINUX) .type(HintType.STRING) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuildHints.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuildHints.java new file mode 100644 index 00000000000..239d21f3ec4 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuildHints.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/// The build hints of the JavaScript port, and where each one has to go. +/// +/// A JavaScript build has three audiences for a hint, and a hint that reaches the wrong one +/// silently does nothing: +/// +/// - the **running application**, which reads `Display.getProperty` -- the launcher sets these +/// (see [#launcherProperties(BuildRequest)]); +/// - the **page** the translator writes, `index.html`, which exists before any Java runs -- +/// these become translator system properties (see [#translatorOptions(BuildRequest)]); +/// - the **bundle**, which should carry only the native themes the application can reach at +/// run time (see [#themesToShip(BuildRequest, Set)]). +/// +/// The cloud builder (BuildDaemon's `ParparVMJavascriptBuilder`) carries a copy of this class; +/// keep the two identical apart from the package and the `BuildRequest` import. +public final class JavaScriptBuildHints { + + /// Hints the running application reads through `Display.getProperty`. Before this list + /// existed the launcher set none of them, so `nativeTheme` and friends only worked when the + /// application called `Display.setProperty` itself. + static final String[] RUNTIME_HINTS = { + "nativeTheme", + "cn1.nativeTheme", + "ios.themeMode", + "and.themeMode", + "cn1.androidTheme", + "javascript.native.theme", + "javascript.desktopTheme", + "javascript.titleBar", + "javascript.textSelection" + }; + + /// Every native theme the port bundle can carry in `assets/`, by resource name. + static final List ALL_THEMES = Collections.unmodifiableList(Arrays.asList( + "iOS7Theme", "iPhoneTheme", "iOSModernTheme", "iOSModern27Theme", + "androidTheme", "android_holo_light", "AndroidMaterialTheme", + "WindowsFluentTheme", "MacOSAquaTheme", "GnomeAdwaitaTheme")); + + /// The runtime property the port publishes with the OS-appropriate modern theme. Code that + /// reads it can open either modern theme, so an application naming it keeps both. + static final String MODERN_THEME_PROPERTY = "cn1.modernThemeResource"; + + private JavaScriptBuildHints() { + } + + /// Java source lines, for the generated launcher, that publish the runtime hints the + /// request sets. Hints the request does not set are left out, so the port's own defaults + /// apply. + static String launcherProperties(BuildRequest request) { + StringBuilder out = new StringBuilder(); + for (String hint : RUNTIME_HINTS) { + String value = request.getArg(hint, null); + if (value == null) { + continue; + } + out.append(" Display.getInstance().setProperty(\"") + .append(hint).append("\", \"").append(javaString(value)).append("\");\n"); + } + return out.toString(); + } + + /// `-D` options for the translator JVM, which writes `index.html` (see + /// `JavascriptBundleWriter.applyPageSettings` in the ByteCodeTranslator). Only what the + /// request actually decides is passed; the translator's defaults are the documented ones, + /// so an older builder that passes nothing still gets the viewport and the opt-outs. + static List translatorOptions(BuildRequest request) { + List out = new ArrayList(); + String title = request.getDisplayName(); + if (title != null && title.trim().length() > 0) { + out.add("-Dcodename1.javascript.title=" + title.trim()); + } + String translation = request.getArg("javascript.allowBrowserTranslation", null); + if (translation != null) { + out.add("-Dcodename1.javascript.allowBrowserTranslation=" + "true".equalsIgnoreCase(translation.trim())); + } + String darkReader = request.getArg("javascript.darkreaderLock", null); + if (darkReader != null) { + out.add("-Dcodename1.javascript.darkreaderLock=" + !"false".equalsIgnoreCase(darkReader.trim())); + } + return out; + } + + /// The native themes this application can open at run time, which is what the bundle has + /// to carry. Everything else in [#ALL_THEMES] is dead weight in the public web root -- up to + /// 2.5MB of it. + /// + /// This mirrors `HTML5Implementation.resolveNativeThemeResource()` in the JavaScript port, + /// which picks the theme from these hints AND the browser's user agent. The user agent is + /// unknown here, so the answer is the union over every browser the hints leave possible. + /// Change the two together. + /// + /// #### Parameters + /// + /// - `request`: the build hints + /// - `referencedByApp`: theme names (and [#MODERN_THEME_PROPERTY]) that appear as strings + /// in the application's own classes; an application that opens a theme itself keeps it + static Set themesToShip(BuildRequest request, Set referencedByApp) { + Set out = new LinkedHashSet(); + for (String name : ALL_THEMES) { + if (referencedByApp.contains(name)) { + out.add(name); + } + } + if (referencedByApp.contains(MODERN_THEME_PROPERTY)) { + out.add("iOSModernTheme"); + out.add("AndroidMaterialTheme"); + } + String explicit = request.getArg("javascript.native.theme", null); + if (explicit != null && explicit.trim().length() > 0) { + String name = baseName(explicit.trim()); + if (ALL_THEMES.contains(name)) { + out.add(name); + } else { + // A theme the application ships itself. If it fails to open the port falls + // back to the legacy pair, so keep that pair. + out.add("android_holo_light"); + out.add("iOS7Theme"); + } + return out; + } + String shared = lower(request.getArg("nativeTheme", request.getArg("cn1.nativeTheme", null))); + + // Desktop browsers: a desktop theme replaces the mobile branch there, but phones and + // tablets still take the mobile branch, so the two sets add up. + String desktop = desktopThemeFor(lower(request.getArg("javascript.desktopTheme", null)), shared, null); + if (desktop != null) { + out.add(desktop); + } else if (isAutoDesktop(lower(request.getArg("javascript.desktopTheme", null)), shared)) { + out.add("WindowsFluentTheme"); + out.add("MacOSAquaTheme"); + out.add("GnomeAdwaitaTheme"); + } + + // iOS-like browsers (iOS, iPadOS and a Mac that is not given a desktop theme). + String iosMode = lower(request.getArg("ios.themeMode", null)); + if (iosMode == null && shared != null) { + if ("modern".equals(shared) || "auto".equals(shared) || "native".equals(shared)) { + iosMode = "modern"; + } else if ("legacy".equals(shared)) { + iosMode = "ios7"; + } + } + if (iosMode == null || "ios7".equals(iosMode) || "flat".equals(iosMode)) { + out.add("iOS7Theme"); + } else if ("legacy".equals(iosMode) || "iphone".equals(iosMode)) { + out.add("iPhoneTheme"); + } else { + out.add("iOSModernTheme"); + } + + // Every other browser. + String androidMode = lower(request.getArg("and.themeMode", request.getArg("cn1.androidTheme", null))); + if (androidMode == null && shared != null) { + if ("modern".equals(shared) || "auto".equals(shared) || "native".equals(shared)) { + androidMode = "material"; + } else if ("legacy".equals(shared)) { + androidMode = "hololight"; + } + } + if (androidMode == null) { + // The port's historical default: Holo Light on an Android user agent, iOS 7 on the + // rest. + out.add("android_holo_light"); + out.add("iOS7Theme"); + } else if ("legacy".equals(androidMode)) { + out.add("androidTheme"); + } else if ("hololight".equals(androidMode) || "holo".equals(androidMode)) { + out.add("android_holo_light"); + } else { + out.add("AndroidMaterialTheme"); + } + return out; + } + + /// The desktop theme a desktop browser gets, or null when it keeps the mobile branch (or, + /// for `auto`, when the answer depends on the browser's OS -- see [#isAutoDesktop]). + /// + /// #### Parameters + /// + /// - `desktopHint`: `javascript.desktopTheme`, lower case, or null + /// - `shared`: `nativeTheme`, lower case, or null + /// - `os`: `win`, `mac` or `linux` to resolve `auto`; null when unknown + static String desktopThemeFor(String desktopHint, String shared, String os) { + if ("fluent".equals(desktopHint) || "windows".equals(desktopHint)) { + return "WindowsFluentTheme"; + } + if ("aqua".equals(desktopHint) || "mac".equals(desktopHint) || "macos".equals(desktopHint)) { + return "MacOSAquaTheme"; + } + if ("adwaita".equals(desktopHint) || "gnome".equals(desktopHint) || "linux".equals(desktopHint)) { + return "GnomeAdwaitaTheme"; + } + if (os != null && isAutoDesktop(desktopHint, shared)) { + if ("win".equals(os)) { + return "WindowsFluentTheme"; + } + if ("mac".equals(os)) { + return "MacOSAquaTheme"; + } + return "GnomeAdwaitaTheme"; + } + return null; + } + + /// Whether a desktop browser picks its theme by OS. Only `nativeTheme=native` asks for it: + /// `modern` means the mobile modern themes, exactly as it does for the JavaSE desktop build. + static boolean isAutoDesktop(String desktopHint, String shared) { + return (desktopHint == null || "auto".equals(desktopHint)) && "native".equals(shared); + } + + /// Scans the application's classes -- loose, or inside jars -- for the theme names in + /// [#ALL_THEMES] (as `Name.res`) and for [#MODERN_THEME_PROPERTY]. A class that names a + /// resource stores the string verbatim in its constant pool, so a byte search finds it. + /// + /// Call this on the application tree BEFORE the port is merged into it: the port names + /// every theme, and would keep them all. + static Set scanThemeReferences(File classesDir) throws IOException { + Set found = new LinkedHashSet(); + if (classesDir != null && classesDir.isDirectory()) { + scanDirectory(classesDir, found); + } + return found; + } + + private static void scanDirectory(File dir, Set found) throws IOException { + File[] children = dir.listFiles(); + if (children == null) { + return; + } + for (File child : children) { + if (child.isDirectory()) { + scanDirectory(child, found); + } else if (child.getName().endsWith(".class")) { + InputStream in = new FileInputStream(child); + try { + scanBytes(readAll(in), found); + } finally { + in.close(); + } + } else if (child.getName().endsWith(".jar")) { + ZipInputStream zin = new ZipInputStream(new FileInputStream(child)); + try { + ZipEntry e; + while ((e = zin.getNextEntry()) != null) { + if (!e.isDirectory() && e.getName().endsWith(".class")) { + scanBytes(readAll(zin), found); + } + } + } finally { + zin.close(); + } + } + } + } + + static void scanBytes(byte[] data, Set found) { + // ISO-8859-1 maps each byte to one char, so an ASCII needle is found exactly where the + // bytes are, whatever else the class file contains. + String text = new String(data, StandardCharsets.ISO_8859_1); + for (String name : ALL_THEMES) { + if (text.indexOf(name + ".res") >= 0) { + found.add(name); + } + } + if (text.indexOf(MODERN_THEME_PROPERTY) >= 0) { + found.add(MODERN_THEME_PROPERTY); + } + } + + private static byte[] readAll(InputStream in) throws IOException { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + private static String baseName(String path) { + int slash = path.lastIndexOf('/'); + String name = slash >= 0 ? path.substring(slash + 1) : path; + return name.endsWith(".res") ? name.substring(0, name.length() - 4) : name; + } + + private static String lower(String s) { + if (s == null) { + return null; + } + s = s.trim(); + if (s.length() == 0) { + return null; + } + // An ASCII fold: these are hint keywords, and toLowerCase() is locale sensitive. + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + b.append(c >= 'A' && c <= 'Z' ? (char) (c + 32) : c); + } + return b.toString(); + } + + private static String javaString(String value) { + StringBuilder b = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '\\': b.append("\\\\"); break; + case '"': b.append("\\\""); break; + case '\n': b.append("\\n"); break; + case '\r': b.append("\\r"); break; + case '\t': b.append("\\t"); break; + default: + if (c < 0x20 || c > 0x7e) { + String hex = Integer.toHexString(c); + b.append("\\u"); + for (int p = hex.length(); p < 4; p++) { + b.append('0'); + } + b.append(hex); + } else { + b.append(c); + } + } + } + return b.toString(); + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java index c8f6930fc7a..46ec678c5b4 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java @@ -39,8 +39,10 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -192,6 +194,9 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException // application looks like a database user and nothing is ever pruned. The iOS, // Windows and Linux builders scan at the same point for the same reason. recordDatabaseUsage(stageClasses); + // Same point and same reason: the port names every native theme, so a scan after it + // is merged in would keep them all. + themeReferences = JavaScriptBuildHints.scanThemeReferences(stageClasses); File portSources = locateJavaScriptPortSources(request); File portClassesStaged = stageJavaScriptPort(request, portSources, stageClasses, portClasses); @@ -211,7 +216,7 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException File parparvmCompilerJar = extractParparVMCompiler(); if (!runByteCodeTranslator(parparvmCompilerJar, stageClasses, translatorOut, translatorAppName, - request.getPackageName(), request.getMainClass(), request.getVersion())) { + request.getPackageName(), request.getMainClass(), request.getVersion(), request)) { return false; } @@ -315,7 +320,14 @@ private void recordDatabaseUsage(File stageClasses) throws IOException { } + /** + * Theme names the application's own classes mention, from + * {@link JavaScriptBuildHints#scanThemeReferences(File)}; read when the port assets are pruned. + */ + private Set themeReferences = new HashSet(); + private void pruneOptionalPortAssets(File webApp, BuildRequest request) { + pruneUnusedThemes(new File(webApp, "assets"), request); File js = new File(webApp, "js"); if (!js.isDirectory()) { return; @@ -353,6 +365,29 @@ private void pruneOptionalPortAssets(File webApp, BuildRequest request) { } } + /** + * Deletes the native themes this application cannot reach. The port bundle carries every + * theme the runtime resolver can pick between, and the resolver's choice depends on hints + * that are known here -- so an app that never asks for the desktop themes, or pins one, does + * not ship the rest into its public web root. + */ + private void pruneUnusedThemes(File assets, BuildRequest request) { + if (!assets.isDirectory()) { + return; + } + Set keep = JavaScriptBuildHints.themesToShip(request, themeReferences); + for (String theme : JavaScriptBuildHints.ALL_THEMES) { + if (keep.contains(theme)) { + continue; + } + File f = new File(assets, theme + ".res"); + if (f.isFile() && !f.delete()) { + log("WARNING: could not delete " + f + "; it will ship in the bundle"); + } + } + debug("Native themes shipped: " + keep); + } + private File locateJavaScriptPortSources(BuildRequest request) { String explicit = request.getArg("javascript.portSources", null); if (explicit != null && explicit.trim().length() > 0) { @@ -556,6 +591,10 @@ private File writeLauncher(File workDir, String launcherName, String packageName pw.println(" ParparVMBootstrap.bootstrap(new " + mainClass + "(), new Runnable() {"); pw.println(" public void run() {"); pw.print(hardeningRuntimeProperties(request)); + // The theme, title-bar and text-selection hints, read by the port through + // Display.getProperty. The native theme is installed from the lifecycle's init, + // which runs after this. + pw.print(JavaScriptBuildHints.launcherProperties(request)); pw.println(" }"); pw.println(" });"); pw.println(" }"); @@ -897,7 +936,7 @@ private File extractParparVMCompiler() throws BuildException { private boolean runByteCodeTranslator(File compilerJar, File stageClasses, File translatorOut, String translatorAppName, String packageName, String mainClass, - String version) throws Exception { + String version, BuildRequest pageRequest) throws Exception { Map env = new HashMap(); log("Running ByteCodeTranslator (javascript target) for " + mainClass); java.util.List cmd = new java.util.ArrayList(); @@ -927,6 +966,9 @@ private boolean runByteCodeTranslator(File compilerJar, File stageClasses, File if (jsPortWebApp != null && jsPortWebApp.isDirectory() && !webAppOverridden) { extraOpts.add("-Dcodename1.javascriptport.webapp=" + jsPortWebApp.getAbsolutePath()); } + // The page title and the index.html opt-outs, placed before the CN1_TRANSLATOR_OPTS + // entries: the JVM keeps the last -D for a key, so an explicit override there wins. + extraOpts.addAll(0, JavaScriptBuildHints.translatorOptions(pageRequest)); // Heap sized from the machine (see TranslatorHeap); a -Xmx in // CN1_TRANSLATOR_OPTS still takes precedence. The old fixed 512m was // tuned against the sample apps and made a large app fail with diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java new file mode 100644 index 00000000000..45d9e27f982 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/builders/JavaScriptBuildHintsTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Where the JavaScript port's build hints go: the launcher, the translator, and the choice of +/// native themes the bundle carries. +class JavaScriptBuildHintsTest { + + private static BuildRequest request(String... keyValues) { + BuildRequest r = new BuildRequest(); + for (int i = 0; i < keyValues.length; i += 2) { + r.putArgument(keyValues[i], keyValues[i + 1]); + } + return r; + } + + private static Set ship(BuildRequest r) { + return JavaScriptBuildHints.themesToShip(r, Collections.emptySet()); + } + + private static Set set(String... names) { + return new HashSet(Arrays.asList(names)); + } + + @Test + void launcherPublishesOnlyTheHintsThatAreSet() { + String src = JavaScriptBuildHints.launcherProperties(request( + "nativeTheme", "native", "javascript.titleBar", "html", "unrelated.hint", "x")); + assertTrue(src.contains("Display.getInstance().setProperty(\"nativeTheme\", \"native\");"), src); + assertTrue(src.contains("Display.getInstance().setProperty(\"javascript.titleBar\", \"html\");"), src); + assertFalse(src.contains("unrelated.hint"), src); + assertFalse(src.contains("javascript.textSelection"), src); + } + + @Test + void launcherValuesAreJavaStringLiterals() { + String src = JavaScriptBuildHints.launcherProperties(request("javascript.native.theme", "a\"b\\c\n")); + assertTrue(src.contains("\"a\\\"b\\\\c\\n\""), src); + } + + @Test + void translatorGetsTheTitleAndOnlyTheOptOutsThatWereSet() { + BuildRequest r = request(); + r.setDisplayName("My App"); + assertEquals(Collections.singletonList("-Dcodename1.javascript.title=My App"), + JavaScriptBuildHints.translatorOptions(r)); + + r = request("javascript.allowBrowserTranslation", "TRUE", "javascript.darkreaderLock", "false"); + List opts = JavaScriptBuildHints.translatorOptions(r); + assertTrue(opts.contains("-Dcodename1.javascript.allowBrowserTranslation=true"), opts.toString()); + assertTrue(opts.contains("-Dcodename1.javascript.darkreaderLock=false"), opts.toString()); + } + + @Test + void noHintsShipsOnlyTheLegacyPair() { + assertEquals(set("iOS7Theme", "android_holo_light"), ship(request())); + } + + @Test + void nativeShipsTheModernMobileThemesAndEveryDesktopTheme() { + assertEquals(set("iOSModernTheme", "AndroidMaterialTheme", + "WindowsFluentTheme", "MacOSAquaTheme", "GnomeAdwaitaTheme"), + ship(request("nativeTheme", "native"))); + } + + @Test + void modernStaysOffTheDesktopThemes() { + assertEquals(set("iOSModernTheme", "AndroidMaterialTheme"), ship(request("nativeTheme", "modern"))); + } + + @Test + void aPinnedDesktopThemeShipsOnlyThatDesktopTheme() { + assertEquals(set("iOSModernTheme", "AndroidMaterialTheme", "MacOSAquaTheme"), + ship(request("nativeTheme", "native", "javascript.desktopTheme", "aqua"))); + assertEquals(set("iOSModernTheme", "AndroidMaterialTheme"), + ship(request("nativeTheme", "native", "javascript.desktopTheme", "none"))); + } + + @Test + void anExplicitResourceShipsOnlyThatTheme() { + assertEquals(set("WindowsFluentTheme"), + ship(request("javascript.native.theme", "/WindowsFluentTheme.res", "nativeTheme", "native"))); + // Not one of ours: the port falls back to the legacy pair if it cannot open it. + assertEquals(set("iOS7Theme", "android_holo_light"), + ship(request("javascript.native.theme", "/mytheme.res"))); + } + + @Test + void platformModesAreHonoured() { + assertEquals(set("iPhoneTheme", "androidTheme"), + ship(request("ios.themeMode", "legacy", "and.themeMode", "legacy"))); + assertEquals(set("iOS7Theme", "android_holo_light"), ship(request("nativeTheme", "legacy"))); + } + + @Test + void whatTheApplicationNamesIsKept() { + Set refs = new LinkedHashSet(); + JavaScriptBuildHints.scanBytes("xx/GnomeAdwaitaTheme.resyy".getBytes(StandardCharsets.ISO_8859_1), refs); + JavaScriptBuildHints.scanBytes("cn1.modernThemeResource".getBytes(StandardCharsets.ISO_8859_1), refs); + Set shipped = JavaScriptBuildHints.themesToShip(request(), refs); + assertTrue(shipped.containsAll(set("GnomeAdwaitaTheme", "iOSModernTheme", "AndroidMaterialTheme")), + shipped.toString()); + } + + @Test + void everyShippedNameIsAKnownTheme() { + String[][] combos = { + {}, {"nativeTheme", "native"}, {"nativeTheme", "modern"}, {"nativeTheme", "legacy"}, + {"nativeTheme", "native", "javascript.desktopTheme", "fluent"}, + {"ios.themeMode", "flat", "and.themeMode", "holo"}, + }; + for (String[] combo : combos) { + for (String name : ship(request(combo))) { + assertTrue(JavaScriptBuildHints.ALL_THEMES.contains(name), name); + } + } + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/DesktopChromeTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/DesktopChromeTest.java index e56269f7696..ed033573739 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/DesktopChromeTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/DesktopChromeTest.java @@ -138,6 +138,31 @@ void aPortWithANativeMenuBarStillHidesTheToolbar() { * form takes when the application sets {@code Display.COMMAND_BEHAVIOR_NATIVE} directly * rather than through the desktop title-bar mode. */ + /** + * The desktop native themes declare {@code commandBehavior: Native}. With a Toolbar the + * commands live on the Toolbar, not on the form's MenuBar, so publishing the MenuBar's list + * at show time handed the native menu bar nothing: the menu bar came up empty and the + * commands, whose Toolbar was hidden, could not be reached at all. + */ + @FormTest + void nativeCommandBehaviourWithAToolbarPublishesTheToolbarsCommands() { + desktopMode("native"); + Display.getInstance().setCommandBehavior(Display.COMMAND_BEHAVIOR_NATIVE); + try { + Form f = new Form("My App"); + Command save = Command.create("Save", null, null); + f.getToolbar().addCommandToOverflowMenu(save); + f.show(); + DisplayTest.flushEdt(); + + Vector bridged = implementation.getLastNativeCommands(); + assertNotNull(bridged, "commands must be bridged to the native menu bar"); + assertTrue(bridged.contains(save), "the Toolbar's command must reach the native menu bar"); + } finally { + Display.getInstance().setCommandBehavior(Display.COMMAND_BEHAVIOR_DEFAULT); + } + } + @FormTest void nativeCommandBehaviourStillDrawsSoftButtonsWithNoNativeMenuBar() { implementation.setDesktop(true); diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/DialogBackdropTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/DialogBackdropTest.java new file mode 100644 index 00000000000..4f9f3030550 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/DialogBackdropTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui; + +import com.codename1.junit.FormTest; +import com.codename1.junit.UITestBase; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/// A dialog is painted over the form it was opened from: the area around the dialog shows that +/// form, tinted. +/// +/// Issue #5910 reported the area around a dialog as blank in a browser. The cause was in the +/// JavaScript port -- a theme refresh on show dropped the backdrop painter -- and +/// scripts/test-javascript-composited-rendering.mjs guards that. This guards the framework half +/// the port relies on, on the desktop path and under the legacy theme the report ran. +class DialogBackdropTest extends UITestBase { + + @FormTest + void theFormShowsAroundADesktopDialog() { + implementation.setDesktop(true); + try { + assertBackdropPainted(); + } finally { + implementation.setDesktop(false); + } + } + + @FormTest + void theFormShowsAroundADesktopDialogUnderTheLegacyTheme() throws Exception { + java.io.File res = new java.io.File("../../Themes/iOS7Theme.res"); + com.codename1.ui.util.Resources r = com.codename1.ui.util.Resources.open(new java.io.FileInputStream(res)); + com.codename1.ui.plaf.UIManager.getInstance().setThemeProps(r.getTheme(r.getThemeResourceNames()[0])); + implementation.setDesktop(true); + try { + assertBackdropPainted(); + } finally { + implementation.setDesktop(false); + } + } + + @FormTest + void theFormShowsAroundAMobileDialog() { + assertBackdropPainted(); + } + + private void assertBackdropPainted() { + Form f = new Form("Backdrop", BoxLayout.y()); + f.add(new Label("underneath")); + f.show(); + DisplayTest.flushEdt(); + + Dialog d = new Dialog("Hello", BoxLayout.y()); + d.add(new Label("Welcome")); + d.showPacked(BorderLayout.CENTER, false); + DisplayTest.flushEdt(); + + Image img = Image.createImage(d.getWidth(), d.getHeight(), 0); + d.paintComponent(img.getGraphics(), true); + int[] rgb = img.getRGB(); + int corner = rgb[d.getWidth() * (d.getHeight() - 2) + 2]; + assertNotEquals(0, corner >>> 24, "the corner outside the dialog is transparent: the form " + + "it was opened from was not painted under it"); + d.dispose(); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/ScrollWheelGestureTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/ScrollWheelGestureTest.java index 3d7bfbf3bfd..164c67644ed 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/ScrollWheelGestureTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/ScrollWheelGestureTest.java @@ -170,6 +170,28 @@ public void actionPerformed(ActionEvent evt) { assertEquals(0, target.wheels(), "a consumed wheel never reaches the component's own handler"); } + @FormTest + void aWheelPastTheEndOfThePageLeavesTheFormWhereItIs() { + Form f = scrollingForm(); + Container page = f.getContentPane(); + int x = page.getAbsoluteX() + page.getWidth() / 2; + int y = page.getAbsoluteY() + page.getHeight() / 2; + int pageMax = page.getScrollDimension().getHeight() - page.getHeight(); + for (int i = 0; i < 200 && page.getScrollY() < pageMax; i++) { + wheelAt(x, y, 0, -px(20)); + } + assertEquals(pageMax, page.getScrollY(), "the page reaches its end"); + + // Issue #5910. The form answers isScrollableY() for its content pane, so the walk used + // to carry on to the form itself and scroll THAT: the Toolbar went off the top with + // the content and the space it left was blank. + for (int i = 0; i < 5; i++) { + wheelAt(x, y, 0, -px(20)); + } + assertEquals(0, f.getScrollY(), "the form itself never scrolls"); + assertEquals(pageMax, page.getScrollY(), "and the page stays at its end"); + } + @FormTest void aWheelPastTheEndOfAnInnerScrollerMovesThePage() { Form f = scrollingForm(); diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/TextSelectionTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/TextSelectionTest.java index 5e1d1c79fa7..4353ff601b9 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/TextSelectionTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/TextSelectionTest.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.codename1.ui; import com.codename1.junit.FormTest; @@ -91,6 +113,66 @@ void testEnableDisableTextSelection() { assertSame(selection, implementation.getLastDeinitializedTextSelection()); } + @FormTest + void disablingRemovesEveryListenerEnablingAdded() { + Form form = Display.getInstance().getCurrent(); + TextSelection selection = form.getTextSelection(); + int before = longPressListenerCount(form); + selection.setEnabled(true); + assertEquals(before + 1, longPressListenerCount(form)); + selection.setEnabled(false); + // The disable path used to ADD the long press listener a second time instead of + // removing it, so a long press kept driving a selection that was switched off. + assertEquals(before, longPressListenerCount(form)); + } + + private static int longPressListenerCount(Component c) { + return c.longPressListeners == null ? 0 : c.longPressListeners.getListenerCollection().size(); + } + + @FormTest + void defaultSelectableAppliesToReadOnlyTextOnly() { + try { + Label label = new Label("text"); + Button button = new Button("button"); + TextArea readOnly = new TextArea("read only"); + readOnly.setEditable(false); + TextArea editable = new TextArea("editable"); + assertFalse(label.isTextSelectionEnabled(), "off by default"); + + TextSelection.setDefaultSelectable(true); + assertTrue(label.isTextSelectionEnabled()); + assertTrue(readOnly.isTextSelectionEnabled()); + assertFalse(button.isTextSelectionEnabled(), "a button's press is its action"); + assertFalse(editable.isTextSelectionEnabled(), "editing owns an editable field's presses"); + assertTrue(label.getTextSelectionSupport().isTextSelectionEnabled(null)); + + // An explicit choice outranks the default in both directions. + label.setTextSelectionEnabled(false); + assertFalse(label.isTextSelectionEnabled()); + button.setTextSelectionEnabled(true); + assertTrue(button.isTextSelectionEnabled()); + } finally { + TextSelection.setDefaultSelectable(false); + } + } + + @FormTest + void defaultSelectableSkipsTextInsideALeadComponent() { + try { + TextSelection.setDefaultSelectable(true); + Container lead = new Container(); + Button leadButton = new Button("lead"); + Label inside = new Label("inside"); + lead.add(leadButton).add(inside); + lead.setLeadComponent(leadButton); + assertFalse(inside.isTextSelectionEnabled(), "the press belongs to the lead component"); + assertTrue(new Label("outside").isTextSelectionEnabled()); + } finally { + TextSelection.setDefaultSelectable(false); + } + } + @FormTest void testSpanOperations() { Form form = Display.getInstance().getCurrent(); diff --git a/maven/parparvm/pom.xml b/maven/parparvm/pom.xml index 4af4c0f0dfb..66a555c1d4d 100644 --- a/maven/parparvm/pom.xml +++ b/maven/parparvm/pom.xml @@ -275,6 +275,15 @@ + + + + + + + + back to the legacy theme instead of applying. + + The desktop themes are for nativeTheme=native in a desktop + browser, by the browser's OS, or javascript.desktopTheme. + The builders then delete whatever the application's hints + cannot reach (JavaScriptBuildHints.themesToShip), so an + app ships only what it can open. --> @@ -300,6 +315,9 @@ + + + diff --git a/native-themes/gnome-adwaita/theme.css b/native-themes/gnome-adwaita/theme.css index ec9afb1441e..42f905708c3 100644 --- a/native-themes/gnome-adwaita/theme.css +++ b/native-themes/gnome-adwaita/theme.css @@ -764,7 +764,53 @@ ChatSendButton { cn1-derive: RaisedButton; padding: 1.5mm 3mm; margin: 0.5mm; } ChatAttachButton { cn1-derive: Button; padding: 1.5mm; margin: 0.5mm; } ChatVoiceButton { cn1-derive: Button; padding: 1.5mm; margin: 0.5mm; } +/* --- Toolbar commands and the side menu ------------------------------------------------ */ +/* + * Where a desktop port puts the title and the commands in the window's own title bar and menu + * bar, the Toolbar is detached and none of these paint. They exist for the places that keep + * the Toolbar: the `toolbar` title-bar mode, and a browser, which has no window title bar or + * menu bar to hand them to (the JavaScript port's default, `javascript.titleBar=toolbar`). + * Without them the Toolbar's buttons and the side menu fell through to the framework's mobile + * defaults on a desktop-styled form. + * + * TitleCommand is every toolbar button, the side-menu hamburger included; BackCommand the back + * arrow; MenuButton/MenuButtonRight the legacy menu buttons; SideNavigationPanel the sliding + * panel and SideCommand its rows. + */ +TitleCommand { color: var(--text-color, #2e3436); background-color: transparent; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +TitleCommand.hover { color: var(--text-color, #2e3436); background-color: #dededf; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +TitleCommand.pressed { color: var(--text-color, #2e3436); background-color: #d3d3d4; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +BackCommand { color: var(--text-color, #2e3436); background-color: transparent; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +BackCommand.hover { color: var(--text-color, #2e3436); background-color: #dededf; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +BackCommand.pressed { color: var(--text-color, #2e3436); background-color: #d3d3d4; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +MenuButton { color: var(--text-color, #2e3436); background-color: transparent; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +MenuButtonRight { color: var(--text-color, #2e3436); background-color: transparent; border-radius: 1.6mm; padding: 1.6mm 3.2mm 1.6mm 3.2mm; margin: 0; } +SideNavigationPanel { background-color: var(--window-bg-color, #fafafa); border-right: 0.26mm solid var(--separator-color, #d8d4d0); padding: 1.6mm 0 1.6mm 0; margin: 0; } +RightSideNavigationPanel { background-color: var(--window-bg-color, #fafafa); border-left: 0.26mm solid var(--separator-color, #d8d4d0); padding: 1.6mm 0 1.6mm 0; margin: 0; } +SideCommand { color: var(--text-color, #2e3436); background-color: transparent; border-radius: 1.6mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.hover { color: var(--text-color, #2e3436); background-color: #dededf; border-radius: 1.6mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.pressed { color: var(--text-color, #2e3436); background-color: #d3d3d4; border-radius: 1.6mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.selected { color: var(--accent-color, #3584e4); background-color: #dededf; border-radius: 1.6mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +RightSideCommand { color: var(--text-color, #2e3436); background-color: transparent; border-radius: 1.6mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +RightSideCommand.hover { color: var(--text-color, #2e3436); background-color: #dededf; border-radius: 1.6mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } + @media (prefers-color-scheme: dark) { + TitleCommand { color: #ffffff; background-color: transparent; border-radius: 1.6mm; } + TitleCommand.hover { color: #ffffff; background-color: #3a3a3a; border-radius: 1.6mm; } + TitleCommand.pressed { color: #ffffff; background-color: #444444; border-radius: 1.6mm; } + BackCommand { color: #ffffff; background-color: transparent; border-radius: 1.6mm; } + BackCommand.hover { color: #ffffff; background-color: #3a3a3a; border-radius: 1.6mm; } + BackCommand.pressed { color: #ffffff; background-color: #444444; border-radius: 1.6mm; } + MenuButton { color: #ffffff; background-color: transparent; border-radius: 1.6mm; } + MenuButtonRight { color: #ffffff; background-color: transparent; border-radius: 1.6mm; } + SideNavigationPanel { background-color: #242424; border-right: 0.26mm solid #3d3846; } + RightSideNavigationPanel { background-color: #242424; border-left: 0.26mm solid #3d3846; } + SideCommand { color: #ffffff; background-color: transparent; border-radius: 1.6mm; } + SideCommand.hover { color: #ffffff; background-color: #3a3a3a; border-radius: 1.6mm; } + SideCommand.pressed { color: #ffffff; background-color: #444444; border-radius: 1.6mm; } + SideCommand.selected { color: #3584e4; background-color: #3a3a3a; border-radius: 1.6mm; } + RightSideCommand { color: #ffffff; background-color: transparent; border-radius: 1.6mm; } + RightSideCommand.hover { color: #ffffff; background-color: #3a3a3a; border-radius: 1.6mm; } Form { background-color: #242424; color: #ffffff; } Label { color: #ffffff; } diff --git a/native-themes/macos-aqua/theme.css b/native-themes/macos-aqua/theme.css index a97bc923e40..fcc840fd3b9 100644 --- a/native-themes/macos-aqua/theme.css +++ b/native-themes/macos-aqua/theme.css @@ -685,7 +685,53 @@ ChatSendButton { cn1-derive: RaisedButton; padding: 1.5mm 3mm; margin: 0.5mm; } ChatAttachButton { cn1-derive: Button; padding: 1.5mm; margin: 0.5mm; } ChatVoiceButton { cn1-derive: Button; padding: 1.5mm; margin: 0.5mm; } +/* --- Toolbar commands and the side menu ------------------------------------------------ */ +/* + * Where a desktop port puts the title and the commands in the window's own title bar and menu + * bar, the Toolbar is detached and none of these paint. They exist for the places that keep + * the Toolbar: the `toolbar` title-bar mode, and a browser, which has no window title bar or + * menu bar to hand them to (the JavaScript port's default, `javascript.titleBar=toolbar`). + * Without them the Toolbar's buttons and the side menu fell through to the framework's mobile + * defaults on a desktop-styled form. + * + * TitleCommand is every toolbar button, the side-menu hamburger included; BackCommand the back + * arrow; MenuButton/MenuButtonRight the legacy menu buttons; SideNavigationPanel the sliding + * panel and SideCommand its rows. + */ +TitleCommand { color: var(--text-color, #000000); background-color: transparent; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +TitleCommand.hover { color: var(--text-color, #000000); background-color: #e0e0e0; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +TitleCommand.pressed { color: var(--text-color, #000000); background-color: #d6d6d6; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +BackCommand { color: var(--text-color, #000000); background-color: transparent; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +BackCommand.hover { color: var(--text-color, #000000); background-color: #e0e0e0; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +BackCommand.pressed { color: var(--text-color, #000000); background-color: #d6d6d6; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +MenuButton { color: var(--text-color, #000000); background-color: transparent; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +MenuButtonRight { color: var(--text-color, #000000); background-color: transparent; border-radius: 1.32mm; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 0; } +SideNavigationPanel { background-color: var(--window-bg-color, #ececec); border-right: 0.26mm solid var(--separator-color, #dcdcdc); padding: 1.6mm 0 1.6mm 0; margin: 0; } +RightSideNavigationPanel { background-color: var(--window-bg-color, #ececec); border-left: 0.26mm solid var(--separator-color, #dcdcdc); padding: 1.6mm 0 1.6mm 0; margin: 0; } +SideCommand { color: var(--text-color, #000000); background-color: transparent; border-radius: 1.32mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.hover { color: var(--text-color, #000000); background-color: #e0e0e0; border-radius: 1.32mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.pressed { color: var(--text-color, #000000); background-color: #d6d6d6; border-radius: 1.32mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.selected { color: var(--accent-color, #007aff); background-color: #e0e0e0; border-radius: 1.32mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +RightSideCommand { color: var(--text-color, #000000); background-color: transparent; border-radius: 1.32mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +RightSideCommand.hover { color: var(--text-color, #000000); background-color: #e0e0e0; border-radius: 1.32mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } + @media (prefers-color-scheme: dark) { + TitleCommand { color: #ffffff; background-color: transparent; border-radius: 1.32mm; } + TitleCommand.hover { color: #ffffff; background-color: #3c3c3c; border-radius: 1.32mm; } + TitleCommand.pressed { color: #ffffff; background-color: #464646; border-radius: 1.32mm; } + BackCommand { color: #ffffff; background-color: transparent; border-radius: 1.32mm; } + BackCommand.hover { color: #ffffff; background-color: #3c3c3c; border-radius: 1.32mm; } + BackCommand.pressed { color: #ffffff; background-color: #464646; border-radius: 1.32mm; } + MenuButton { color: #ffffff; background-color: transparent; border-radius: 1.32mm; } + MenuButtonRight { color: #ffffff; background-color: transparent; border-radius: 1.32mm; } + SideNavigationPanel { background-color: #323232; border-right: 0.26mm solid #48484a; } + RightSideNavigationPanel { background-color: #323232; border-left: 0.26mm solid #48484a; } + SideCommand { color: #ffffff; background-color: transparent; border-radius: 1.32mm; } + SideCommand.hover { color: #ffffff; background-color: #3c3c3c; border-radius: 1.32mm; } + SideCommand.pressed { color: #ffffff; background-color: #464646; border-radius: 1.32mm; } + SideCommand.selected { color: #007aff; background-color: #3c3c3c; border-radius: 1.32mm; } + RightSideCommand { color: #ffffff; background-color: transparent; border-radius: 1.32mm; } + RightSideCommand.hover { color: #ffffff; background-color: #3c3c3c; border-radius: 1.32mm; } Form { background-color: #323232; color: #ffffff; } Label { color: #ffffff; } diff --git a/native-themes/windows-fluent/theme.css b/native-themes/windows-fluent/theme.css index 374829cdbe6..50c3dbf38fe 100644 --- a/native-themes/windows-fluent/theme.css +++ b/native-themes/windows-fluent/theme.css @@ -789,7 +789,53 @@ ChatSendButton { cn1-derive: RaisedButton; padding: 1.5mm 3mm; margin: 0.5mm; } ChatAttachButton { cn1-derive: Button; padding: 1.5mm; margin: 0.5mm; } ChatVoiceButton { cn1-derive: Button; padding: 1.5mm; margin: 0.5mm; } +/* --- Toolbar commands and the side menu ------------------------------------------------ */ +/* + * Where a desktop port puts the title and the commands in the window's own title bar and menu + * bar, the Toolbar is detached and none of these paint. They exist for the places that keep + * the Toolbar: the `toolbar` title-bar mode, and a browser, which has no window title bar or + * menu bar to hand them to (the JavaScript port's default, `javascript.titleBar=toolbar`). + * Without them the Toolbar's buttons and the side menu fell through to the framework's mobile + * defaults on a desktop-styled form. + * + * TitleCommand is every toolbar button, the side-menu hamburger included; BackCommand the back + * arrow; MenuButton/MenuButtonRight the legacy menu buttons; SideNavigationPanel the sliding + * panel and SideCommand its rows. + */ +TitleCommand { color: var(--text-color, #1a1a1a); background-color: transparent; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +TitleCommand.hover { color: var(--text-color, #1a1a1a); background-color: #eaeaea; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +TitleCommand.pressed { color: var(--text-color, #1a1a1a); background-color: #e5e5e5; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +BackCommand { color: var(--text-color, #1a1a1a); background-color: transparent; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +BackCommand.hover { color: var(--text-color, #1a1a1a); background-color: #eaeaea; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +BackCommand.pressed { color: var(--text-color, #1a1a1a); background-color: #e5e5e5; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +MenuButton { color: var(--text-color, #1a1a1a); background-color: transparent; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +MenuButtonRight { color: var(--text-color, #1a1a1a); background-color: transparent; border-radius: 1.06mm; padding: 1.6mm 2.6mm 1.6mm 2.6mm; margin: 0; } +SideNavigationPanel { background-color: var(--window-bg-color, #f3f3f3); border-right: 0.26mm solid var(--separator-color, #e5e5e5); padding: 1.6mm 0 1.6mm 0; margin: 0; } +RightSideNavigationPanel { background-color: var(--window-bg-color, #f3f3f3); border-left: 0.26mm solid var(--separator-color, #e5e5e5); padding: 1.6mm 0 1.6mm 0; margin: 0; } +SideCommand { color: var(--text-color, #1a1a1a); background-color: transparent; border-radius: 1.06mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.hover { color: var(--text-color, #1a1a1a); background-color: #eaeaea; border-radius: 1.06mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.pressed { color: var(--text-color, #1a1a1a); background-color: #e5e5e5; border-radius: 1.06mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +SideCommand.selected { color: var(--accent-color, #0078d4); background-color: #eaeaea; border-radius: 1.06mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +RightSideCommand { color: var(--text-color, #1a1a1a); background-color: transparent; border-radius: 1.06mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } +RightSideCommand.hover { color: var(--text-color, #1a1a1a); background-color: #eaeaea; border-radius: 1.06mm; padding: 2.1mm 3.2mm 2.1mm 3.2mm; margin: 0.5mm 1.6mm 0.5mm 1.6mm; text-align: left; } + @media (prefers-color-scheme: dark) { + TitleCommand { color: #ffffff; background-color: transparent; border-radius: 1.06mm; } + TitleCommand.hover { color: #ffffff; background-color: #2d2d2d; border-radius: 1.06mm; } + TitleCommand.pressed { color: #ffffff; background-color: #292929; border-radius: 1.06mm; } + BackCommand { color: #ffffff; background-color: transparent; border-radius: 1.06mm; } + BackCommand.hover { color: #ffffff; background-color: #2d2d2d; border-radius: 1.06mm; } + BackCommand.pressed { color: #ffffff; background-color: #292929; border-radius: 1.06mm; } + MenuButton { color: #ffffff; background-color: transparent; border-radius: 1.06mm; } + MenuButtonRight { color: #ffffff; background-color: transparent; border-radius: 1.06mm; } + SideNavigationPanel { background-color: #202020; border-right: 0.26mm solid #2d2d2d; } + RightSideNavigationPanel { background-color: #202020; border-left: 0.26mm solid #2d2d2d; } + SideCommand { color: #ffffff; background-color: transparent; border-radius: 1.06mm; } + SideCommand.hover { color: #ffffff; background-color: #2d2d2d; border-radius: 1.06mm; } + SideCommand.pressed { color: #ffffff; background-color: #292929; border-radius: 1.06mm; } + SideCommand.selected { color: #4cc2ff; background-color: #2d2d2d; border-radius: 1.06mm; } + RightSideCommand { color: #ffffff; background-color: transparent; border-radius: 1.06mm; } + RightSideCommand.hover { color: #ffffff; background-color: #2d2d2d; border-radius: 1.06mm; } Form { background-color: #202020; color: #ffffff; } Label { color: #ffffff; } diff --git a/scripts/javascript/composited-app/common/codenameone_settings.properties b/scripts/javascript/composited-app/common/codenameone_settings.properties new file mode 100644 index 00000000000..d9a2c61f7a8 --- /dev/null +++ b/scripts/javascript/composited-app/common/codenameone_settings.properties @@ -0,0 +1,59 @@ +#set( $symbol_pound = '#' ) +#set( $symbol_dollar = '$' ) +#set( $symbol_escape = '\' ) +codename1.android.keystore= +codename1.android.keystoreAlias= +codename1.android.keystorePassword= +codename1.arg.ios.newStorageLocation=true +# Modern native themes (iOS liquid-glass + Material 3) - opt-in. +# Desktop integration (only takes effect when the app runs on the desktop). titleBar mode is +# one of: native (OS title bar + native menu bar), custom (undecorated, CN1-drawn title bar) or +# toolbar (legacy in-app CN1 Toolbar). interactiveScrollbars enables grab-able, click-to-page +# desktop scrollbars. These are honored by the generated desktop Stub. +codename1.arg.desktop.titleBar=native +codename1.arg.desktop.interactiveScrollbars=true +codename1.arg.java.version=8 +# No theme hints: scripts/test-javascript-composited-rendering.mjs locates the button and the +# title through the DOM text layer, and the legacy theme is the one that promotes them there. +# It is also exactly what the cloud build the issues were reported against ran. +codename1.displayName=CompositedApp +codename1.icon=icon.png +codename1.ios.appid=Q5GHSKAL2F.com.codename1.jsfixture +codename1.ios.certificate= +codename1.ios.certificatePassword= +codename1.ios.debug.certificate= +codename1.ios.debug.certificatePassword= +codename1.ios.debug.provision= +codename1.ios.provision= +codename1.ios.release.certificate= +codename1.ios.release.certificatePassword= +codename1.ios.release.provision= +# On-device debugging for iOS apps. When set, the iOS build emits a +# JDWP-compatible debug agent that dials out to a proxy on your laptop +# so any IDE that speaks JDWP (IntelliJ, jdb, VS Code) can attach to +# the running app on the native iOS simulator or a tethered device. +# See the "On-Device Debugging (iOS)" chapter of the developer guide +# for the full setup (the IntelliJ Run/Debug configs that come with +# this project's .idea/ directory are wired against these hints). +#codename1.arg.ios.onDeviceDebug=true +#codename1.arg.ios.onDeviceDebug.proxyHost=127.0.0.1 +#codename1.arg.ios.onDeviceDebug.proxyPort=55333 +# Block the app at boot until the debugger attaches (default: false). +#codename1.arg.ios.onDeviceDebug.waitForAttach=true +# On-device debugging for Android apps. When set, the Android build +# marks the APK debuggable and skips R8/proguard so the Dalvik/ART +# JDWP socket is reachable through adb. Pair with the IntelliJ +# "CN1 Android On-Device Debug" and "CN1 Attach Android" run configs +# bundled with this project, or with the cn1:android-on-device-debugging +# Maven goal. See the "On-Device Debugging (Android)" chapter of the +# developer guide for the wireless-debugging instructions and the full +# adb flow. +#codename1.arg.android.onDeviceDebug=true +codename1.kotlin=false +codename1.languageLevel=5 +codename1.mainName=CompositedApp +codename1.packageName=com.codename1.jsfixture +codename1.secondaryTitle=Hello World +codename1.vendor=CodenameOne +codename1.version=1.0 +codename1.cssTheme=true diff --git a/scripts/javascript/composited-app/common/icon.png b/scripts/javascript/composited-app/common/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4fa5dd25258fbaa99b15a11d47c83ce4a9a8ac GIT binary patch literal 123269 zcmYJ4bzECPv-g90vEmdd?rz1Q#ogWAwYa;xQ>3`NI}|9vt+=~eaY)|ux%av6UrA2# zIcIivcJ?>lnM5fmNTMMVA_D*bG-;`?DgXd9l&#f8(U`n4F32?75$ON8BPSyqv5{j7qDI_QMdx& zVMN;oO`X6LC~xTNw_QH^J+9Rzs`S3{D2xfSggVvJ^X-ZMG14fPmO^T$qOihq`W>QK zl{~@d*K6T0vnTYJDz==6wZ7dS8LCeutsYEu_%eZu?oGm|aOi!BBYxau>?&gfrB@!r zVDy8DHnhnSc7gH<8c1G@>TZlo9?Aip&F1@>@Vc&wyjb*kk@urmGZgFiu~$8B)_4aV z1K7P4fCS&KY`81^8{a90;kaorh%io~Xx8dbxZ$>M7{?6t+5~1h7QL$~xiPePMk4Uv zcbq6;#kYzmw6Z04Xo1yA68*I&VGtJp*Mps?IA&4yf?jGe3wKL}pBD(3vmI&0>Vk(pS5rE!OhMUw-N9#fNXpyF(Rn{m_C6= zO$(-G0B4rQ``N=nei|pi0d7}0Ku^I36vP4kXLQ(_MQ_H)Fo0&v|GnF`O`5RY2mtT{ zzx?0r?ijIi|;PYG|U{16!SF-%*njew#5?-2B67cK(QP;RRX z+RlE7{EuPrw$e|wBV1fhi~l>~1 z0w_5O-*)JMfU?gF+p!2;We^-2dK#n0G)8hYIanDKwB(>=OKPz0BCem*s2-yG6 z3O%%@iZ^3;wEw$><{%Z(>+fH+{v5>>WhJOi^riHe0tezU1rWQ4I^V1>XO{vlXYY)h z=ghig$x(j+w=y?X`YJM&(M2ZA@&XO8qX+DAgZ|@xHIi(B1{@8yj%px|-_`g4)+o$G z%)e{f(|@G@v)=QcpG5j<+>HHXx3pD27-0Y#6+sKEAeYDdzXiAvFsRGt$TlLhvwaYcZcJ?fq^ z{Z0TDP0AeoAc6xxXduB4S(`s^SlJ!@soP((B_W#)qoCF&H|7j65njtA8qc~E=ihXX zbyz}`21bWwR3+~ax@E=c9&*I~3k1R>X#oa#HPvrYJFi&8;`))xz$Dr#sU}1WoAbT2 z1d_*`_VH`h^n}Vu2FiYtgtK-q6kQD6S6W-Szyag4iw6vK5MEs42 z5&or)@)p^@50~=P6>cv)GIGi`-F!Q@LF;J1gfcH+5yPh33%B6XXGuxew8)72$_Hgz ziLaOw!Tp&W_}(y~Q3xPFDr(~5J(8UpsrC}%Y?5+W=b+AgG_I#kyYC8&OzVtA-QX>? zjJlDe_ihjG$IVaxX%QgLUi0ihQ}o(NoBjl?#1aunE9Rgs`izjYtMJ}%5DN1@cR+Lb zOWeK_3Uz_B(ei)18K@(rDXFBLKI-fyy`d(tV&G!Mn3Gs7yc>Qnu3$QD7#S5xbZVzZ z+Gn)br>O@R)QA{=Km{0ztp*y{uHPFYnIoYiT)Pl79-ilCSXfBMciHwEp*^IM^CXz& zH~d6XPWK~Ab7=|lE=If8pI6Qmv`1V&4$%19q`ZUKqxj~pQn?sHN;mdod5eGFrZxiD zlMpR(VGgTjrfMfgqu^TBPJT*4X`KBXsd;=e^bwljiy`*?yo@ipF73eQpQgLKG;*Ag zB7T8X14;BBbUskElrwi?(HEXbZgCmCaqOLO=DQz=jaB7AyDWpgL9D$|7OEeKf0{Pdx73mt4aP z>hv`;(6n;VcQvq@?l(TLC68OJHwK$-%rj3mbuJ#qi%-Sd;BPL?0LnlL7E8mIT88^y ziqfu*qOn#_+Q_5&i3LJ+SYq96~ji`k?pAqA@7Gx$oE4ET|bcJ8IQ zRWH}rs-3$Iz+QXBqBBIxKdRbpq4_Z!V4xuzQlsl~hhsf*;JT?s~fTU6ss5h8?EanB<(uPG&2r`Ip3imR6c}p@7!78NR5n z7^~c$#@Qga5Dj_~RT;Hlxa#W-_q!D|djIDTN4iY#`@(@+x*cS*F># zZm{n53y+GT zfSi!1S#Frp*P3e>#XoVuZsLg4mX+LlpWxlToZhP=$OIK&3=RV%U@FAe2HHI=6}n-V z*wH!XW&IBFH6Bg)jKqX=D-806aud8{y#UE(Ei@t=Fux8wd)0UX{)BEy?RaP<=?X`0 zjUoroDlxx{H77HEy*yZUio^k#=q*me-*72-qnad4Y60+UUBoh}Ek$z3e{1hayMIz< z9uP^DREkCISSF)|2fn6MkIY!rrdMcyYRa2*{r~_}xzkU{`X%rbfD5Dl7pl>zebU*kWaj1pP?DG8M$Kl3x!acig82pH#<^ zNMr?lk%pC62?=0^AnqWu5S#M3C398}Y1j$t;Hs_kHUgIiK(_g5JvKC`Dx8ZR43bJO^H z8*M;km*rtVxUJ2iW*V@Dn8CMaFn?L6A{i2UG#${veK>lH!zdyrnRrc%{eJjWu9f4_LjtnuQ^ z^44x0H>2V=fJFrBJboZD2xr*x$pdRdMDrsd5dXBJ_cwk!emOOBNRlO)**g9VL^JD7 z^d+TNLua1rxw-d48xuC>?^$q=+mr}l+%I1?FCb5addsdPt$xjLARccc)O?Pid-YDT z%tskn!w>09VE$voij1?5MPBq>c7w)?D1C($7ctz{skij9!i6^v)?59vlqS@KEpx_& z@Ck40Oy)Gve?%3d!!GrgbR~v2s&u1@8V#FPKyl-sBnlJ+0v zAK1Qb2#e2-2-s7*#6FuGA~dTaoy{6&n+SOW;0N6!{`3S$B&8)GR;Fq z`>PdPl75TjraNOkNKc1fIiPL$Nf%YUePFf&8YTWY^Fo;5ZtkBLZpV*oY7bM&T9sug zdD>g_kF10tja_fRv!(ah61h)aV(^>1pF1YNzugID;d2m)pwcp-qad>!bV2Yg!Whsd zom!-%2s(8*#_!B^B4^FV24%B3{*#3LVpN+*Q+$yb7)b)oEcJEaD8i*#|!c^e^YgqwF z4?Ca$UucZTF|S_pxQsfAG+sZI+RPG3wRB7=0m=jpEaJxBcbU|=vR6J&(Il0Bo!4YY zHWj1#@PU_}ahtGyE~o?-Rk8!x(hrAZMiA~04~6P*>vwl0bSA3)C)R1H*rr6A$8ME^ zhf#ww#234kVD_-+SM;Ur<}zT*!lg5?M91Uitoo#?EQ~$rhy%LCIhARFdkPYRy$sz+ z^=wdHKDC&;^})%?YZ;?VJvn_@ebFm83?zy8bCBKiPE!}!_ja)I+M18LV1lCU6ZoR_qYjGu=N(%?VE8h zl%&&~>N9LhhmiARG2=p7z(g?QEYL)lK%fMsv3$m}QWxv183D%P^wq7ug(T%{Md`HL zaNa=;c;W=%4enI#wlO&{M?`G$UfDdwJdtk#Fv>> zD`rQPAjY|c{;|fM!T2k*&w`QnUp6sg^Goo-$4<+EVdn^uYfMGV{H{+&z2y}HNr>s0 z69qBk=DMHH&}B;V6MP1LFvv=CkeTSRQ7yh-+R$Y{bEiHv52Xocq714RK?4LogUBwiW=aNrZy{dqTASe>+RmE95bt*9LlZDf!HFJW<#+|cR zm1%!cql(Cx-lCS3yo@?lw{qBCo(-4XS#3Ip1Y73UXBuo!scC%k%(`wpNnob8i4kNd zW-XYk8;EKGee{c5hDYV0Ww{j~O+TsF^EScltLi}?K>HdVU`1h6ilmI>-x}CLHvm*?rpQ+k zW2$(np)E4ZxV7folNvdapbPagA0rZ~ma8y;wx>4JFDZ`K+$k(&F+nS3U#T=VADc}cjg*{~q<-(xpxMNP#_PK;#aq3p7XUF~KR1@lQ` z!Fw5#xT=4}iv8s4fUT*@L7$Y{;7^FR5<$&Jky1Uo>()=H>)x)4xu5W#4K>i`-cw?w z{VhI7k#Kb{hLwUkDvAy?Fs!F9i9Pq^nQ-mhZi&9FJ$VbPGG(iKGsXSNuw7*=Hrn6* zg^U{-cedXU1$yza8LM*f&0DknbBRBC$*TDKeRQqYN_H>i5jJ9Ynu<{c6IoJQBpSy2!ixo&K6#uDp*DE9hHN|FrVC`KI! z9odJaj0=QYecF-1EI$Xil!yLCvrgN`V|WrJHK?k*kQaSCU9z%B;oTI*>Yw^qyo6Ql znwHc%5u_kXh15&pb+BHpW5h-TeZL8c0|sCo1u)sb0NfNrIAE3Wgex?#&-Bxr%n)(F z0X@idnpA0_Mi7M3q)5MNxs7J%j;i+qt2+MH#X)KaOV_FWC#pL`C9_tnM4-VlkL4^C z6gP!!f0j*3>wt?h0o0g!AA^yq4%PEw)2kpaq5Dl!m`xjFoq4s6**YHAzXh7-L`PlF z`zz+zK}m26XRA{GOGEvOy*95h4ly%}f@6Oulix5S%^TOhkopr6QW+m|13hgwMJmS{ z2R>a*8X|5>eKRK*YVQhYK*WeET}+CNfMfn@9#W+e+!EC~_#=ILF*@1RV!_|1aN4-% zHlOgKM_!2sI{H*1DlSk+QVI3&<=g^32n%|c)(3nTyWPs@rc=3prH13Lmq=VALtMxa z! zvmwDMz-rLbT?CtgEcdJ_M+i9U(ZNR1E?5POghryNsxKnr(?~0J+k$w@<}PB3VShuV zndCWIqD}2qp1LdPi1~n+h~=vx-zp$8v^r7%DhvVwk{~EAFVKrn0K2@_Zy4}JzMX+a zD???BY#8u>yBJ-F^|cDrA!HmU;i`{c-l2!Iu)e7q+YQyfnwkvtqtu%@cX~5Ws`g$x z=@u{PmgOr0*z0xJ;)6aME8IQSz~CS)=0{qOK3MK{nVT|s^-GUG5gFsj4I=6Qd}o(_ zxtZO)URnr&xy(B7+D0tdPWZ;}>L61EFDA?Q+Z?V=U0szW2uP9X*fs>))|dI~Vhh=@ zGVCzVU|T;tEEyYv=A{FEJsHPNA;h+F0t`dt*-&8mPBlSOoGBI;65Y!+xcc-Wkb zR?C)0#%nmk2DTL^0%-R8azn$?A8P(12<;4IHm~wf;#{8oa^@&&Vxuoy*~7uuX{eCp zG{9GF?EhH*85xPO1@1s;EM5Z-1+ohKtAKW~LsJtYy|pVCp7vn=#u*2?4Db%JfMFu) zA4+R+dH*Jr3yA0t$T`kaBDz!+&cGhDX`dQ_>gcyWI% zG*noe=1m`^CJA==$%A3T3T*)!W)!wOmMD%v?WER)=MqK7(-#~QAbf+lz==J&XH_D%4^^WO!^y!3(cz#)iBsR(iGHJnbK-$27(msw23TvwN_#5v;a|?gMd$8yP#T=b*xHUI|S%y zM6?Ur_J2Bw;C>(bP0y97tg0=;h3UgM@Q1ir`}E~U-0pix;>*6(1;g5^W!v@aQXiL9 zoO5FNqVa0r-K?02uENY2Emba?k)o(N#;+c6i}MNd)CW!-M88rITKWBPgqC0gecS5WtEyZ>$%?cYeL3A>VW`_)Qme^SN85i{Fh2I^T zynm(6vm(tG1S9y&8AjR+E8NibTB7!kVw)fB#>Fyfx&f8~IOtWt@?VZZdR=f|F7J>b zTs+U~3p%V#TaO8v0e;$gGB%@o!B)7MLVXvaX;AlJ@}3F6uL44_MVoIE%q?RK^T+4J zOl$FCIKKM96ppkGEFB}!!p%4!T~5#)kV4Z`(X)+abe7af6lLkND{s!qnkJ{HQ>Ap; zwR4o2sh<$;>&?gc{RIY$%-ER1Tid|y*G?fO(n2;V=6X_uUUSxTA=WuP&^vg_mnWL5 zNw$pcq^;xVLOm!-l|5nB#Etnxg-u)ooc5cm(0ZbrD1mKXHuL*Sp z4!iSFkbR{Co6`;0^Wo@}OYmKZoO)U?mx3EVT9Lo2cDiZ4ZR^ zjw$=KKihtl0POL2H~s2wA4s5C^j{}p2X@xN*OwudRnpHJP8``bM~?V$vjqz2CVvu? z)HM&~vW9t`z-%muVRT1@zh%C-xG1jvC?S~_OvUSdN_ime(?Py8)M!q`x7Lwd^c_Lc zuZKI+8FyKLRmlr#l!C&@RwE420T@UD8!=v~EYkSZYv4cnVnlEHR(oGY0E(+Z<0Xe( z<{3qeXkr77mo)A9gex09mF%LG(yLCY*Eb1NAIa~Q-H_p-;{T38!CuhjLHEhe?{A&X z@M?>#cB2_my18=IB5lUF89M>J&!fv>nxLNNi}RO^nu4R!3sgq!(UXUR+`$s|altLF zJG>xzYyo|BiT+kvw+=oS+~5pZtk|1PD(?f~HY*sTbOJ?GIeaL-W%ZBU%1K)Ac2mfI zI&9d{UhBJH@oc?StUh{S&K)Jxv*9EX3B*0F3MmNP@v;yvhg`k7xw*}3ZA~XHR;_fp zGgN8Tm6nx7Oid}+tx+RF%amYIP*5O}YjwC#i6H_H*b)5Sh{d;_smC>HcjG6#mGC+q%9_a@_?jBl2F!=8b zx=k3N!5@hJbX0bXB%-rO@S`2NHE#VJ#Z89Vac=X-M%e;MKD=E!8w6Z%0ZXtPfW~9V zRMmPN$!J8}^}f$8U-b&iRVor?O0NC#2L?o^@&se9UIC%TQjU%+q8Ltp zkCh*;HizdG1&4uLw(XBUz)`we53=V4ad#HsJ0eFPY?&4R1kPsfhe5CBVY_{6oJVxY}Cg7w?R@r6oy4MRa?6dqi@45|UCidNy8ODdr4{{OOeO z16I@!m5N`_cSjvgJJ5MYJDtqo#;n}j5*>Qf8RHEWlZ3IRqrB}N=n)TnIceZ)>r3CY zwGJ?zf0(#F5w8*#ozc!&;KJW}z){fGv04{ImRMIIyOriSk zwTYgwx<;qX5?I>@dAbGrsJ!Y)3t|H`UO4kyu&5Gij^WB4ed%kHSKiwDd;VPbQkU!~ zcK-GViheJq-Pc1j6^k~1A0CpYkH##Tis!9WDn5#0DA9g|crJc;kh!+|aQ>K?lRnL-Vzle^vK8vEpChyopz@iUF{h=FYoUim{-q>ZU1!PTdGUX zwYBGy{5P+ux~`B7H&$9*%TH`|TCjH_SX7T;*G0->KfFXO7k*b04o(@iRFTb5v zpkNl1f+tgb@oY0OR>?KLJk=WPUSsXNM&VTU0#W8997K4-m>d|Q?Tl0#=}XkyBG>|% z{umE@vJcibohQZP2)b__wV^3CDdXcjbfunY6J=;vz3u4nj7=Kt8$$u#etWoDk*7~H zS!p!izw-*kSg_$5Wc|g8DmPyZW-9r`DGwP&t9|tF=Td8v>{IGn0raiM8-WC!n()|l zS;#efcrnCavi`?+q$>g+g2olbqN1W);eb3<@Ee=~Xa{@UcN%6L6ayxza(LCwV_tRj zI^krJaxu+Mg`(h6KU}vTIIM=0ah22-p;i$y^_%zeVW2C#tsLW1`+`(J(+M)G@+EJ3 z`mi2xT#mSIOv}*zb0i0?Nkev*WAj}$*!=PO`)A_8cyX6!V8En%(_%&A&H;_riKB2o zhawnb)L9GSE3z5PRH)(t(^U*!fn=^|NH@l2lZx7BR$XfM`Qxs6(A92{=CZ z;rCz~q@hGI#M69CQrm3mjg1XROjBaSB_JT!c})Ou2>b7{ff61AQrAeH6uZ@Q7zgww z53E|Nwt9ojC<8C-gB^%k0Afz%lxLezljk#pd;q4okH*}It04t=Qo%xzpjsprWDOT>JP(c)WiLZ&=$LsLE3~>SaXnC6PP6Amw=`2oY7J zd&ZNb^4M2*?+@pED%@V2`+V&FPo~J7l0!5pIdZlKzep$oP=m`c|jwlyIW{O z`}GW4*?PV}o%7R77g=m4=#l-gKuBr+hd$P(%a&(c6??ro!^H#80(3`3Bu3I;j8sC&+Bpl8&cr%VrT23Hx=D~dGj4YE?^OwO&Xjd!>nJ^jfCeL&A$lP;?meljtm zKOpsCqdE%_l=ABG`aZC+MoE97my4F*oQTk2OYbR`fZ(-zHA7QHIS!- z!A`g%nYb*Ex7Gbj`ufTv!q!iOVv)+|Z?o@9BZB?CO^gy9amtc;iD-lYNDz?b|j+4kvTQ4qX1; z-?Oo@iUb4*^Q=3;NIqQcoCv=%`8!x;*en-jVW#sZ2WWbwk#@f>JuOLsq9A_%^73*D zBxRPL=+dbDy21nU;|l-wwyIMjL7hULG46aFMtXCe|3S^jJ`XBwdH!{0h8V#nk$%$EKSDwtfs9IvJ}LlPzykNCnt=+ z8r?RR#|;lx(1dl~Tg=uTQ(3^op&IMal5?S;20jUTu2)js`;wvYXrGXRp&_YkIwLvi zkhkro#KF4M9=%ZfIFC^Fd)6)k-bL&}k>d$E+dHmrQ1+4NpWdN(u54YN{weN%zQ7?pE zt#_p8cYP-hEvTzYuwQF!@O%9(At~u~D&9B!sZL;u+f+_>XZ{zV%=#O!@AilA%k=qH zQ;zv~dV|}^oVN=Jr{>4%v@>kNkO+R}V8lLMWf+hr-=Hg`ve? zSJG{H1t{6AuPDi-4&bWwWtvTqW6y7medT86o1ykzRN@ROKF}7>_J9ch^y0CY%rcgJ zuyV?^3sQVK)rl>wYljNOwme@x!jypG{Q+KsecI$vCv)4TH-m1g9eJ9{fZz1pa*q$s zw9qT#(9qDm1k1B;o_r3s^R(cj5`EeeND`FkJ{^$ux}KaDxHR^ug8nSvuQ|#4hT-1! z<_4o-Z*E>*p11bK;;-!#AVIs?aGt=WP9`I4A}1~3G!3mqp+5OW4f!*{=4f6-J0r0?OM`y z8=YxPx~+0fucQK=iKOqxl4@1ztE;OJZAga5iVCyjzG%T3J}~uz-|Mg`U~<}G?uiG? z$P#W3JLx`z)1)-&$Bw%F&@B1r0mjSso4#6T+nzkSQ>&7pGl=?`M{lu}$%tP!LZtZ~ zCm%wXVEdN4%o=5w@g@igxb-vHHJgVB{qXScC!qM%o@T~h*Ku92$9KH@PMAZmKx7)Y z2I-)WA7~HMFG}Q4<^xE|CVyA09}M$Hc&j#2RK_as&YrN;v0jI87HbLUMS#O^M2!w3 z8Q#4CQ1CC@(_q8tLm_RLDGe(AgQs6zObDWrNAi}2#okgiZ+m}g=@0wR)-=Q~yhvUj zF7`T(@`}{xXJ==Fth2G#r(5mk|NI%r=CYM4o9}+wjS+ZBdb~OG__1pla0%W;NMkfz zN?Gp#K!(pw{r0s|eTJ7VdqeKyd(6ip{^VmmW+5btGEY!^8J~({S|IlLJ03rE>)17|>0-(g}T<*z;0C zE#RSQ=UUmP>A3BSzcPYSDUT1LBR_UEDnT)z_3WT^KJqClz$J9Gk5=R;0i<^Y-zyj8 zqXtN>^_sR#U`^XbxTw^&`i)@_haWNXo%=Pzo z&|3z7Ua`~o5reVrxfi$YvSyzuDfu)-BRnPI!*6modCun??b0FVqf3--&IPH))SaUa zL83Aw^r&2Ro-y049d=y#{@@oI>jN)t?J4)=G^}#^SHO*2jL_lTAUQ*9knN#&XD9dQyONK-%+XjJX+et_@* z)DItSSpsg?-WFbkhnM>WW8v<+nc9vs>u>u;Li-p*e~LksNQFB%Vw=gecqy@cM18L8 z6v;_iiZB?Jhk;)F^>dP#CFJI3EZ9Umqwi!tD0dnL?>k5Hl^4YO7%m{8)_G~eLzOAR zYuA*t@Tnnz{Wb~x_N7c%`e2anR`%6t?lSel+*DB{7kg%DY4nH3=2#rh=I>rofvKcE zP{6afLj6|{G&|t?338UBZ?Yulp~#&A*jW7nFPNbUN=e(vD%D26kuztNGne9g?H+Np zR@;q}j#dpL%O)Tb#n|p-h8#E)5r*cYlAuVQC50rmX2g6JxyrXP>w~RGvu@pOQ1b2f zQnYriGBbOs+@r!kToDL@jRJH_C2XdKabv$>e8Am~yIqL$uZ_6q2+Swih-o{rW|ZY~ zk()OgPiI^LMDgo zVn`0@vZU|P;B{y50DjUoc$vAnbsUe-Dj|ZG-?nIqd|phrT})wS&e>u`wObo$WMmxgEat46#h2=J+&dZ`Rdk<7d)qhoDW{>+lxRK`Yn$))zmullgTlKC5>$=55gvtE>5V%s$~i{=OKO5KSuM7o4VSYT>oU^wD!EfDhAxL zTni)|jAxkQ!{;5zYYXSQUGWGpj#qZQ

g@MUJI7HpSpMR*={}r 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 + + 4.0.0 + + com.codename1.jsfixture + compositedapp + 1.0-SNAPSHOT + + com.codename1.jsfixture + compositedapp-common + 1.0-SNAPSHOT + jar + + + + + com.codenameone + codenameone-core + provided + + + + + com.codenameone + codenameone-javase + test + + + org.junit.jupiter + junit-jupiter + 5.9.3 + test + + + + + + + + + + + install-codenameone + ${user.home}/.codenameone/guibuilder.jar + + + + org.apache.maven.plugins + maven-antrun-plugin + + + + validate + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + kotlin + + + + ${basedir}/src/main/kotlin + + + + 1.3.72 + true + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + + org.jetbrains + annotations + 13.0 + + + com.codenameone + java-runtime + provided + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + initialize + + read-project-properties + + + + ${basedir}/codenameone_settings.properties + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + -no-reflect + -no-jdk + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + -no-reflect + -no-jdk + + + + + + + + + + + + + javase + + + codename1.platform + javase + + + + javase + + + + + org.codehaus.mojo + exec-maven-plugin + + java + true + + -Xmx1024M + + -classpath + + ${exec.mainClass} + ${cn1.mainClass} + + + + + + + + + + simulator + + javase + + + + + + ios-debug + + + iphone + + + ios + + + + + ios-release + + + iphone + true + + + ios + true + + + + + javascript + + javascript + javascript + + + + + android + + android + android + + + + + win + + windows + win + + + + + windows + + desktop_windows + javase + + + + + mac + + desktop_macosx + javase + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + initialize + + read-project-properties + + + + ${basedir}/codenameone_settings.properties + + + + + + + + com.codenameone + codenameone-maven-plugin + + + + + transcode-svg + generate-sources + + transcode-svg + + + + generate-gui-sources + process-sources + + generate-gui-sources + + + + cn1-process-classes + process-classes + + bytecode-compliance + css + + process-annotations + + + + + attach-test-artifact + test + + attach-test-artifact + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + + + + + + + diff --git a/scripts/javascript/composited-app/common/src/main/css/theme.css b/scripts/javascript/composited-app/common/src/main/css/theme.css new file mode 100644 index 00000000000..d486db674bc --- /dev/null +++ b/scripts/javascript/composited-app/common/src/main/css/theme.css @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/** Define Theme Constants here */ +#Constants { + includeNativeBool: true; + defaultSourceDPIInt: "0"; +} + +/** Style for Button class */ +Button { + font-family: "native:MainLight"; + font-size: 3mm; +} + +/** Style for App Title Bar Text */ +Title { + font-family: "native:MainLight"; + font-size: 6mm; +} + +/** Style for Dialog body */ +DialogBody { + font-family: "native:MainLight"; + font-size: 2.8mm; +} + +/** Style for Dialog title bar text */ +DialogTitle { + font-family: "native:MainLight"; + font-size: 4.5mm; +} + +/** Style for the side menu */ +SideNavigationPanel { + background: white; + padding: 2mm 1mm 1mm 1mm; +} + +@media platform-ios { + /** iOS Only styles for side menu. */ + SideNavigationPanel { + /** Extra top padding to deal with notch on iPhoneX */ + padding: 6mm 1mm 1mm 1mm; + } +} + +/** Style for commands in side menu. */ +SideCommand { + padding: 1mm; + border: none; + text-decoration: none; + color: black; + font-family: "native:MainLight"; + font-size: 4mm; + border-bottom: 2px solid #cccccc; +} + +/* Dark mode: the native theme handles dark mode for standard components + * (Form, Toolbar, Button, ...). The side menu below is the only thing this + * template hard-codes to light colors, so it's the only thing we flip for + * dark mode -- keeping the rest of the UI native. Add your own rules here as + * needed, or delete this block if you only want a light theme. */ +@media (prefers-color-scheme: dark) { + SideNavigationPanel { + background: #0f172a; + } + SideCommand { + color: #e2e8f0; + background: #0f172a; + border-bottom: 2px solid #334155; + } +} diff --git a/scripts/javascript/composited-app/common/src/main/java/com/codename1/jsfixture/CompositedApp.java b/scripts/javascript/composited-app/common/src/main/java/com/codename1/jsfixture/CompositedApp.java new file mode 100644 index 00000000000..dfdb1b11f9e --- /dev/null +++ b/scripts/javascript/composited-app/common/src/main/java/com/codename1/jsfixture/CompositedApp.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.jsfixture; + +import com.codename1.components.SpanLabel; +import com.codename1.system.Lifecycle; +import com.codename1.ui.Button; +import com.codename1.ui.Dialog; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BoxLayout; + +/// The application scripts/test-javascript-composited-rendering.mjs drives: the Initializr's +/// Hello World screen plus one long SpanLabel, which is exactly the app issues #5910 and #5912 +/// were reported against. Kept that small on purpose -- every failure the test looks for +/// showed up on this screen and nothing more is needed to see them. +public class CompositedApp extends Lifecycle { + private static final String LOREM = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. "; + + @Override + public void runApp() { + Form hi = new Form("Hi World", BoxLayout.y()); + Button helloButton = new Button("Hello World"); + hi.add(helloButton); + helloButton.addActionListener(e -> hello()); + hi.getToolbar().addMaterialCommandToSideMenu("Hello Command", + FontImage.MATERIAL_CHECK, 4, e -> hello()); + StringBuilder text = new StringBuilder(); + for (int i = 0; i < 6; i++) { + text.append(LOREM); + } + hi.add(new SpanLabel(text.toString().trim())); + hi.show(); + } + + private void hello() { + Dialog.show("Hello Codename One", "Welcome to Codename One", "OK", null); + } +} diff --git a/scripts/javascript/composited-app/javascript/pom.xml b/scripts/javascript/composited-app/javascript/pom.xml new file mode 100644 index 00000000000..8595dc39bff --- /dev/null +++ b/scripts/javascript/composited-app/javascript/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.codename1.jsfixture + compositedapp + 1.0-SNAPSHOT + + com.codename1.jsfixture + compositedapp-javascript + 1.0-SNAPSHOT + + compositedapp + + + UTF-8 + 1.8 + 1.8 + javascript + javascript + local-javascript + + + + + src/main/javascript + + + src/main/resources + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.1 + + + default-jar + none + + + + + com.codenameone + codenameone-maven-plugin + ${cn1.plugin.version} + + + build-javascript + package + + build + + + + + + + + + + + ${project.groupId} + ${cn1app.name}-common + ${project.version} + + + ${project.groupId} + ${cn1app.name}-common + ${project.version} + tests + test + + + + + + + + + diff --git a/scripts/javascript/composited-app/pom.xml b/scripts/javascript/composited-app/pom.xml new file mode 100644 index 00000000000..391d6f6eca5 --- /dev/null +++ b/scripts/javascript/composited-app/pom.xml @@ -0,0 +1,280 @@ + + + 4.0.0 + + com.codename1.jsfixture + compositedapp + 1.0-SNAPSHOT + pom + + compositedapp + compositedapp + https://www.codenameone.com + + + + GPL v2 With Classpath Exception + https://openjdk.java.net/legal/gplv2+ce.html + repo + A business-friendly OSS license + + + + + common + + + + 8.0-SNAPSHOT + 8.0-SNAPSHOT + UTF-8 + + 1.8 + 11 + 1.7.11 + 3.8.0 + 1.8 + 1.8 + 1.8 + compositedapp + + + + + + + + + + com.codenameone + java-runtime + ${cn1.version} + + + com.codenameone + codenameone-core + ${cn1.version} + + + com.codenameone + codenameone-javase + ${cn1.version} + + + + com.codenameone + cn1-binaries-javase + ${cn1.version} + pom + runtime + + + + com.codenameone + codenameone-buildclient + ${cn1.version} + system + ${user.home}/.codenameone/CodeNameOneBuildClient.jar + + + + + + + + + + + + com.codenameone + codenameone-maven-plugin + ${cn1.plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + maven-antrun-plugin + org.apache.maven.plugins + 3.1.0 + + + + + + com.codenameone + codenameone-maven-plugin + ${cn1.plugin.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + com.codenameone + codenameone-maven-plugin + ${cn1.plugin.version} + + + + + + + + + + + codenameone + https://repo.codenameone.com/maven2 + + true + + + false + + + + + + codenameone-plugins + https://repo.codenameone.com/maven2 + + true + + + false + + + + + + javascript + + + codename1.platform + javascript + + + + javascript + + + + ios + + + codename1.platform + ios + + + + ios + + + + win + + + codename1.platform + win + + + + win + + + + linux + + + codename1.platform + linux + + + + linux + + + + + backend + + + codename1.platform + backend + + + + backend + + + + android + + + codename1.platform + android + + + + android + + + + javase + + + codename1.platform + javase + + true + + + javase + + + + cn1libs + + + ${basedir}/cn1libs/pom.xml + + + + cn1libs + + + + + diff --git a/scripts/test-javascript-composited-rendering.mjs b/scripts/test-javascript-composited-rendering.mjs new file mode 100644 index 00000000000..fa2664a9976 --- /dev/null +++ b/scripts/test-javascript-composited-rendering.mjs @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * What a user of the JavaScript port actually sees, measured on the COMPOSITED page. + * + * The screenshot suite cannot catch this class of bug by construction: its capture reads the + * app's own canvas back, and doing so suspends the DOM text layer, so it never sees the page a + * browser shows -- the canvas, the text layer over it and whatever the input path did to both. + * Issues #5910 and #5912 were exactly that shape and came back more than once while that suite + * stayed green. Every check here drives real input and reads the browser's own screenshot or the + * output canvas from outside the app. + * + * Checks, against scripts/javascript/composited-app (the Initializr Hello World plus one long + * SpanLabel, the app both issues were reported against): + * + * desktop, per browser (Chromium and Firefox by default): + * - a wheel scroll leaves the Toolbar exactly where it was and moves the content (#5910: + * the wheel used to scroll the Form itself, taking the Toolbar with it and leaving a + * blank band at the bottom); + * - a Dialog is drawn over its form, tinted, not over a blank page (#5910); + * - a mouse drag scrolls the content (the move events never reached the app); + * phone (Chromium with touch emulation): + * - a swipe scrolls the content (#5912: it did nothing at all); + * - a tap on the hamburger opens the side menu, and a tap on a button fires it (#5912); + * - with the side menu open, none of the form's DOM text floats over the menu's shade. + * + * Usage: + * node scripts/test-javascript-composited-rendering.mjs [artifacts-dir] + * + * Environment: + * CN1_COMPOSITED_BROWSERS desktop browsers, comma separated (default "chromium,firefox") + * CN1_COMPOSITED_TIMEOUT boot timeout in seconds (default 180) + */ +import fs from 'fs'; +import http from 'http'; +import path from 'path'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const { chromium, firefox, webkit, devices } = require('playwright'); + +const bundleDir = process.argv[2]; +const outDir = process.argv[3] || path.resolve('artifacts/javascript-composited'); +if (!bundleDir || !fs.existsSync(path.join(bundleDir, 'index.html'))) { + console.error('usage: test-javascript-composited-rendering.mjs [artifacts-dir]'); + process.exit(2); +} +fs.mkdirSync(outDir, { recursive: true }); +const bootTimeout = Number(process.env.CN1_COMPOSITED_TIMEOUT || '180') * 1000; +const desktopBrowsers = (process.env.CN1_COMPOSITED_BROWSERS || 'chromium,firefox') + .split(',').map((s) => s.trim()).filter(Boolean); + +const TYPES = { + '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', + '.wasm': 'application/wasm', '.json': 'application/json', '.png': 'image/png', '.gif': 'image/gif', + '.ttf': 'font/ttf', '.otf': 'font/otf' +}; +const server = http.createServer((req, res) => { + let p = decodeURIComponent(req.url.split('?')[0]); + if (p === '/') p = '/index.html'; + const file = path.join(bundleDir, path.normalize(p)); + if (!file.startsWith(path.resolve(bundleDir))) { res.writeHead(403); res.end(); return; } + fs.readFile(file, (err, data) => { + if (err) { res.writeHead(404); res.end(); return; } + res.writeHead(200, { 'Content-Type': TYPES[path.extname(file)] || 'application/octet-stream' }); + res.end(data); + }); +}); +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); +const url = `http://127.0.0.1:${server.address().port}/`; + +const failures = []; +function check(ok, label, detail) { + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ' -- ' + detail : ''}`); + if (!ok) failures.push(label); +} + +async function boot(page, logs) { + page.on('console', (m) => logs.push(`${m.type()}: ${m.text()}`)); + page.on('pageerror', (e) => logs.push(`pageerror: ${e.message}`)); + await page.goto(url); + await page.waitForFunction(() => window.cn1Started, null, { timeout: bootTimeout }); + // The first form paints over a few frames after the lifecycle reports it started. + // Case-insensitive: the Android theme a phone user agent gets upper-cases button labels. + await page.waitForFunction(() => Array.from(document.querySelectorAll('#cn1-text-layer span')) + .some((s) => s.textContent.trim().toLowerCase() === 'hello world'), null, { timeout: bootTimeout }); + await page.waitForTimeout(1500); +} + +function span(page, text) { + return page.evaluate((t) => { + const s = Array.from(document.querySelectorAll('#cn1-text-layer span')) + .find((e) => e.textContent.trim().toLowerCase() === t.toLowerCase() + || e.textContent.toLowerCase().startsWith(t.toLowerCase())); + if (!s) return null; + const r = s.getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2, top: r.y, bottom: r.y + r.height }; + }, text); +} + +// Pixels of the output canvas, read from the page rather than by the app, in CSS coordinates. +function canvasPixels(page, points) { + return page.evaluate((pts) => { + const c = document.getElementById('codenameone-canvas'); + const ctx = c.getContext('2d'); + const scale = c.width / c.clientWidth; + const top = c.getBoundingClientRect().top; + return pts.map(([x, y]) => Array.from(ctx.getImageData(Math.round(x * scale), + Math.round((y - top) * scale), 1, 1).data)); + }, points); +} + +function luminance(px) { + return 0.2126 * px[0] + 0.7152 * px[1] + 0.0722 * px[2]; +} + +// Fraction of differing bytes between two PNG screenshots of the same size, over a band of rows. +// Decoding PNG here would need a dependency the workflow does not install, so the comparison is +// done in the page, which can decode them for free. +async function diffFraction(page, a, b, band) { + return page.evaluate(async ({ a, b, band }) => { + const load = (b64) => new Promise((res) => { const i = new Image(); i.onload = () => res(i); i.src = 'data:image/png;base64,' + b64; }); + const [ia, ib] = await Promise.all([load(a), load(b)]); + const w = ia.width; + const y0 = Math.max(0, Math.floor(band[0] * ia.height / window.innerHeight)); + const y1 = Math.min(ia.height, Math.ceil(band[1] * ia.height / window.innerHeight)); + const cv = document.createElement('canvas'); cv.width = w; cv.height = ia.height; + const cx = cv.getContext('2d'); + cx.drawImage(ia, 0, 0); const da = cx.getImageData(0, y0, w, y1 - y0).data; + cx.clearRect(0, 0, w, ia.height); cx.drawImage(ib, 0, 0); const db = cx.getImageData(0, y0, w, y1 - y0).data; + let diff = 0; + for (let i = 0; i < da.length; i += 4) { + if (Math.abs(da[i] - db[i]) + Math.abs(da[i + 1] - db[i + 1]) + Math.abs(da[i + 2] - db[i + 2]) > 24) diff++; + } + return diff / (da.length / 4); + }, { a: a.toString('base64'), b: b.toString('base64'), band }); +} + +// Fraction of pixels in a band that differ from the band's most common colour: near zero for an +// empty stretch of background, well above it where text is drawn. +async function bandContent(page, png, band) { + return page.evaluate(async ({ a, band }) => { + const img = await new Promise((res) => { const i = new Image(); i.onload = () => res(i); i.src = 'data:image/png;base64,' + a; }); + const y0 = Math.floor(band[0] * img.height / window.innerHeight); + const y1 = Math.ceil(band[1] * img.height / window.innerHeight); + const cv = document.createElement('canvas'); cv.width = img.width; cv.height = img.height; + const cx = cv.getContext('2d'); cx.drawImage(img, 0, 0); + const d = cx.getImageData(0, y0, img.width, y1 - y0).data; + const counts = new Map(); + for (let i = 0; i < d.length; i += 4) { const k = (d[i] << 16) | (d[i + 1] << 8) | d[i + 2]; counts.set(k, (counts.get(k) || 0) + 1); } + let top = 0; for (const v of counts.values()) { if (v > top) top = v; } + return 1 - top / (d.length / 4); + }, { a: png.toString('base64'), band }); +} + +async function desktop(name, browserType) { + const browser = await browserType.launch(); + try { + // Dialog first, on a fresh page: the wheel below scrolls the button out of view. + { + const logs = []; + const page = await browser.newPage({ viewport: { width: 1000, height: 450 } }); + await boot(page, logs); + const corners = [[6, 440], [994, 440], [994, 70]]; + const before = await canvasPixels(page, corners); + const button = await span(page, 'Hello World'); + await page.mouse.click(button.x, button.y); + await page.waitForTimeout(2500); + await page.screenshot({ path: path.join(outDir, `${name}-dialog.png`) }); + const after = await canvasPixels(page, corners); + const painted = after.every((px) => px[3] === 255); + const tinted = after.every((px, i) => px[3] === 255 && luminance(px) < luminance(before[i]) - 15); + check(painted, `${name}: the form stays painted around a Dialog`, + `alpha around the dialog: ${after.map((p) => p[3]).join(',')}`); + check(tinted, `${name}: the form around a Dialog is tinted`, + `luminance ${before.map(luminance).map(Math.round).join(',')} -> ${after.map(luminance).map(Math.round).join(',')}`); + fs.writeFileSync(path.join(outDir, `${name}-dialog-console.txt`), logs.join('\n')); + await page.close(); + } + { + const logs = []; + const page = await browser.newPage({ viewport: { width: 1000, height: 450 } }); + await boot(page, logs); + const title = await span(page, 'Hi World'); + const toolbarBottom = Math.max(20, Math.floor(title.bottom)); + const a = await page.screenshot({ path: path.join(outDir, `${name}-scroll-0.png`) }); + await page.mouse.move(500, 300); + for (let i = 0; i < 8; i++) { await page.mouse.wheel(0, 120); await page.waitForTimeout(120); } + await page.waitForTimeout(1500); + const b = await page.screenshot({ path: path.join(outDir, `${name}-scroll-1.png`) }); + const toolbarDiff = await diffFraction(page, a, b, [0, toolbarBottom]); + const contentDiff = await diffFraction(page, a, b, [toolbarBottom + 20, 440]); + check(toolbarDiff < 0.01, `${name}: the Toolbar stays put while the content scrolls`, + `${(toolbarDiff * 100).toFixed(2)}% of the toolbar band changed`); + check(contentDiff > 0.05, `${name}: the wheel scrolls the content`, + `${(contentDiff * 100).toFixed(2)}% of the content changed`); + // Past the end of the page the wheel used to scroll the Form itself, leaving a blank band + // in the form's background colour -- painted, just empty -- so this looks for text there. + const content = await bandContent(page, b, [330, 440]); + check(content > 0.01, `${name}: the bottom of the page still shows content after scrolling`, + `${(content * 100).toFixed(2)}% of the bottom band is not background`); + fs.writeFileSync(path.join(outDir, `${name}-scroll-console.txt`), logs.join('\n')); + await page.close(); + } + { + // A mouse drag scrolls too. The move listeners were registered in a way the worker + // bridge never delivered, so the press and release arrived and every move between them + // was lost -- a drag moved nothing on any platform. + const logs = []; + const page = await browser.newPage({ viewport: { width: 1000, height: 450 } }); + await boot(page, logs); + const title = await span(page, 'Hi World'); + const a = await page.screenshot(); + await page.mouse.move(600, 400); + await page.mouse.down(); + for (let i = 1; i <= 15; i++) { await page.mouse.move(600, 400 - i * 15); await page.waitForTimeout(20); } + await page.mouse.up(); + await page.waitForTimeout(1500); + const b = await page.screenshot({ path: path.join(outDir, `${name}-drag.png`) }); + const moved = await diffFraction(page, a, b, [Math.floor(title.bottom) + 20, 440]); + check(moved > 0.05, `${name}: a mouse drag scrolls the content`, `${(moved * 100).toFixed(2)}% changed`); + fs.writeFileSync(path.join(outDir, `${name}-drag-console.txt`), logs.join('\n')); + await page.close(); + } + } finally { + await browser.close(); + } +} + +async function phone() { + const browser = await chromium.launch(); + try { + const context = await browser.newContext({ ...devices['Pixel 7'] }); + { + const logs = []; + const page = await context.newPage(); + await boot(page, logs); + const vp = page.viewportSize(); + const title = await span(page, 'Hi World'); + const band = [Math.floor(title.bottom) + 30, vp.height - 10]; + const a = await page.screenshot({ path: path.join(outDir, 'phone-swipe-0.png') }); + const cdp = await context.newCDPSession(page); + const x = Math.round(vp.width / 2); + const y0 = Math.round(vp.height * 0.8); + const y1 = Math.round(vp.height * 0.3); + await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y: y0 }] }); + for (let i = 1; i <= 15; i++) { + await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y: Math.round(y0 + (y1 - y0) * i / 15) }] }); + await page.waitForTimeout(16); + } + await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }); + await page.waitForTimeout(2000); + const b = await page.screenshot({ path: path.join(outDir, 'phone-swipe-1.png') }); + const moved = await diffFraction(page, a, b, band); + check(moved > 0.05, 'phone: a swipe scrolls the content', `${(moved * 100).toFixed(2)}% changed`); + fs.writeFileSync(path.join(outDir, 'phone-swipe-console.txt'), logs.join('\n')); + await page.close(); + } + { + const logs = []; + const page = await context.newPage(); + await boot(page, logs); + const vp = page.viewportSize(); + const title = await span(page, 'Hi World'); + const a = await page.screenshot(); + // The hamburger sits at the left end of the Toolbar, level with the title. + await page.touchscreen.tap(22, Math.round(title.y)); + await page.waitForTimeout(2500); + const b = await page.screenshot({ path: path.join(outDir, 'phone-menu.png') }); + const opened = await diffFraction(page, a, b, [title.bottom + 10, vp.height - 10]); + check(opened > 0.2, 'phone: a tap on the hamburger opens the side menu', `${(opened * 100).toFixed(2)}% changed`); + // The form's text beside the open menu is under the menu's shade. The DOM text layer + // sits above the whole canvas, so any of the form's runs still in the DOM would float + // over the shade, crisp and untinted; they have to be drawn on the canvas instead. + const floating = await page.evaluate(() => Array.from(document.querySelectorAll('#cn1-text-layer span')) + .filter((s) => { const r = s.getBoundingClientRect(); return r.width > 0 && r.right > window.innerWidth * 0.9 + && getComputedStyle(s).visibility !== 'hidden'; }) + .map((s) => s.textContent.slice(0, 20))); + check(floating.length === 0, 'phone: the form\'s text stays under the side menu\'s shade', + floating.length ? `DOM text over the shade: ${JSON.stringify(floating.slice(0, 5))}` : ''); + fs.writeFileSync(path.join(outDir, 'phone-menu-console.txt'), logs.join('\n')); + await page.close(); + } + { + const logs = []; + const page = await context.newPage(); + await boot(page, logs); + const vp = page.viewportSize(); + const before = await canvasPixels(page, [[6, vp.height - 6]]); + const button = await span(page, 'Hello World'); + await page.touchscreen.tap(Math.round(button.x), Math.round(button.y)); + await page.waitForTimeout(2500); + await page.screenshot({ path: path.join(outDir, 'phone-dialog.png') }); + const after = await canvasPixels(page, [[6, vp.height - 6]]); + check(luminance(after[0]) < luminance(before[0]) - 15 && after[0][3] === 255, + 'phone: a tap on a button fires it', `corner luminance ${Math.round(luminance(before[0]))} -> ${Math.round(luminance(after[0]))}`); + fs.writeFileSync(path.join(outDir, 'phone-dialog-console.txt'), logs.join('\n')); + await page.close(); + } + } finally { + await browser.close(); + } +} + +try { + const types = { chromium, firefox, webkit }; + for (const name of desktopBrowsers) { + if (!types[name]) { check(false, `unknown browser ${name}`); continue; } + await desktop(name, types[name]); + } + await phone(); +} catch (err) { + check(false, 'the run completed', err && err.stack ? err.stack : String(err)); +} finally { + server.close(); +} + +if (failures.length) { + console.error(`\n${failures.length} check(s) failed; screenshots and console logs are in ${outDir}`); + process.exit(1); +} +console.log('\nAll composited rendering and input checks passed.'); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java index cdcf5ced2c8..969b8b863b4 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java @@ -1636,9 +1636,70 @@ private static void writeIndex(File outputDirectory) throws IOException { stubs.append("\n"); } index = index.replace("", stubs.toString().trim()); + index = applyPageSettings(index, + System.getProperty(PAGE_TITLE_PROPERTY), + Boolean.parseBoolean(System.getProperty(ALLOW_TRANSLATION_PROPERTY, "false")), + !"false".equalsIgnoreCase(System.getProperty(DARKREADER_LOCK_PROPERTY, "true"))); Files.write(new File(outputDirectory, "index.html").toPath(), index.getBytes(StandardCharsets.UTF_8)); } + /// The application's display name, which becomes the page title. The builders pass the + /// `codename1.displayName` of the project; without it the page keeps the generic title. + static final String PAGE_TITLE_PROPERTY = "codename1.javascript.title"; + + /// `true` lets the browser offer machine translation of the page (the + /// `javascript.allowBrowserTranslation` build hint). Off by default: an application renders + /// its own text into a canvas and a DOM text layer, and a translator rewriting that layer + /// under the running app -- or loading the page through a translation proxy -- breaks it. + static final String ALLOW_TRANSLATION_PROPERTY = "codename1.javascript.allowBrowserTranslation"; + + /// `false` drops the Dark Reader opt-out (the `javascript.darkreaderLock` build hint). The app + /// paints its own palette, including its own dark mode, so an extension recolouring the DOM + /// around the canvas only produces a page that disagrees with itself. + static final String DARKREADER_LOCK_PROPERTY = "codename1.javascript.darkreaderLock"; + + static final String DEFAULT_PAGE_TITLE = "Codename One JavaScript Port"; + + /// Fills the page-level placeholders of the `index.html` template. + /// + /// Only `` tags and an attribute are added, never inline script or style, so the CSP + /// hashes [JavascriptSecurityHeaders] computes from this page afterwards are unaffected. + /// + /// The viewport is unconditional: without it every mobile browser lays the page out on a + /// ~980px virtual viewport and scales it down, so the app starts as a shrunken desktop page. + /// It deliberately leaves zoom enabled. + static String applyPageSettings(String index, String title, boolean allowTranslation, boolean darkReaderLock) { + StringBuilder meta = new StringBuilder(); + meta.append(""); + if (!allowTranslation) { + meta.append("\n"); + } + if (darkReaderLock) { + meta.append("\n"); + } + String pageTitle = title == null || title.trim().length() == 0 ? DEFAULT_PAGE_TITLE : title.trim(); + return index + .replace("__HTML_ATTRS__", allowTranslation ? "" : " translate=\"no\"") + .replace("", meta.toString()) + .replace("__APP_TITLE__", escapeHtml(pageTitle)); + } + + private static String escapeHtml(String text) { + StringBuilder out = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + switch (c) { + case '&': out.append("&"); break; + case '<': out.append("<"); break; + case '>': out.append(">"); break; + case '"': out.append("""); break; + case '\'': out.append("'"); break; + default: out.append(c); + } + } + return out.toString(); + } + /** * Native-interface JS implementations self-register into * {@code cn1_native_interfaces} (they end with {@code })(cn1_get_native_interfaces());}). diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptReachability.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptReachability.java index 98612115633..a5d4e9f39e8 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptReachability.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptReachability.java @@ -807,6 +807,8 @@ private void visitMethod(BytecodeMethod method) { markClassInstantiated(declaringOwner); } } + } else if (instr instanceof Ldc) { + markReflectivelyNamedClass(((Ldc) instr).getValue()); } else if (instr instanceof Invoke) { Invoke inv = (Invoke) instr; handleInvoke(inv); @@ -829,6 +831,37 @@ private void visitMethod(BytecodeMethod method) { } } + /** + * A string constant that is the binary name of a class in the program -- {@code + * "common.Salt"} -- is how an application names a class it creates by reflection: + * {@code Class.forName(name).newInstance()}, usually behind a helper, so the call and the + * string need not even be in the same method. RTA never sees a {@code NEW} for such a class, + * so it kept the class and culled every method on it, and the first call through the + * interface it implements failed with "Missing virtual method ... on common_Salt" (issue + * #5774). The string is treated as an instantiation, and the no-argument constructor that + * {@code newInstance()} runs is kept with it. + * + * Only an exact binary name of a class that is in the program counts, so ordinary strings + * cost nothing; a name that merely happens to spell a class keeps that class's methods, + * which is the safe direction. + */ + private void markReflectivelyNamedClass(Object constant) { + if (!(constant instanceof String)) { + return; + } + String name = (String) constant; + if (name.length() == 0 || name.length() > 256 + || name.indexOf(' ') >= 0 || name.indexOf('/') >= 0) { + return; + } + String cls = JavascriptNameUtil.sanitizeClassName(name.replace('.', '/')); + if (!byName.containsKey(cls)) { + return; + } + markClassInstantiated(cls); + enqueueResolved(cls, "", "()V", true); + } + /** * Walk backwards from {@code invokeIndex} collecting the two most * recent ``LDC class`` operands (the two arguments of diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index 82174968812..eebc9cc03e2 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -1771,6 +1771,26 @@ // without losing information. We extend this as more event types show // up in real user code; the bulk (mouse/key/wheel/resize/popstate) is // covered below. + function serializeTouchList(list) { + var out = []; + for (var i = 0; i < list.length; i++) { + var t = list[i] || (typeof list.item === 'function' ? list.item(i) : null); + if (!t) { + continue; + } + out.push({ + identifier: t.identifier | 0, + clientX: +t.clientX || 0, + clientY: +t.clientY || 0, + pageX: +t.pageX || 0, + pageY: +t.pageY || 0, + screenX: +t.screenX || 0, + screenY: +t.screenY || 0 + }); + } + return out; + } + function serializeEventForWorker(evt) { if (evt == null || typeof evt !== 'object') { return evt; @@ -1789,6 +1809,8 @@ if ('pageY' in evt) out.pageY = +evt.pageY || 0; if ('screenX' in evt) out.screenX = +evt.screenX || 0; if ('screenY' in evt) out.screenY = +evt.screenY || 0; + if ('pointerType' in evt) out.pointerType = evt.pointerType == null ? '' : String(evt.pointerType); + if ('pointerId' in evt) out.pointerId = evt.pointerId | 0; if ('button' in evt) out.button = evt.button | 0; if ('buttons' in evt) out.buttons = evt.buttons | 0; if ('detail' in evt) out.detail = evt.detail | 0; @@ -1848,10 +1870,16 @@ if (evt.source && typeof storeHostRef === 'function') out.source = storeHostRef(evt.source); // preventDefault / stopPropagation are fire-and-forget from the worker // side (we eagerly call them on the main-thread event just in case). - // touches arrays are serialised shallow — most user code reads the - // first touch's clientX/Y which is the same as the top-level fields - // except on real multi-touch, but the port.js shims use the flat - // fields already. + // + // A TouchEvent has no clientX/clientY of its own: its coordinates live + // only in the three touch lists, so those are copied point by point. + // They used to be left out on the theory that the flat fields covered + // them, and they do not exist on a touch event -- the port's touch + // handlers read getTargetTouches(), got null and threw on every touch, + // so a phone could neither scroll nor reliably tap (issue #5912). + if (evt.touches) out.touches = serializeTouchList(evt.touches); + if (evt.targetTouches) out.targetTouches = serializeTouchList(evt.targetTouches); + if (evt.changedTouches) out.changedTouches = serializeTouchList(evt.changedTouches); if (evt.target && typeof storeHostRef === 'function') { out.target = storeHostRef(evt.target); } diff --git a/vm/ByteCodeTranslator/src/javascript/index.html b/vm/ByteCodeTranslator/src/javascript/index.html index 890bb4b5881..2a4b0587370 100644 --- a/vm/ByteCodeTranslator/src/javascript/index.html +++ b/vm/ByteCodeTranslator/src/javascript/index.html @@ -1,8 +1,9 @@ - + -Codename One JavaScript Port + +__APP_TITLE__