Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/windows-cross-build-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,18 @@ jobs:
# HTTP/1.1, which avoids the framing errors) and let cargo retry.
CARGO_NET_RETRY: "10"
CARGO_HTTP_MULTIPLEXING: "false"
# --manifest-version 16 (SDK 10.0.22621): the current VS 17 manifest
# lists Win11SDK_10.0.26100 without the per-arch header packages xwin
# 0.9.0 looks for, so the default splat fails with "unable to find
# x86_64 headers". Every target here builds against 22621.
run: |
set -e
# Retry the whole install/splat a few times for non-cargo download
# hiccups (the xwin SDK fetch from Microsoft).
n=0
until [ "$n" -ge 3 ]; do
cargo install xwin --version 0.9.0 --locked \
&& xwin --accept-license --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \
&& xwin --accept-license --manifest-version 16 --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \
&& break
n=$((n+1))
echo "xwin install/splat attempt $n failed; retrying after backoff..."
Expand Down
114 changes: 112 additions & 2 deletions .github/workflows/windows-cross-compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- 'maven/windows/**'
- 'scripts/gen-windows-uwp-stubs.py'
- 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java'
- 'maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java'
- 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java'
- 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml'
- '!vm/**/README.md'
Expand All @@ -31,6 +32,7 @@ on:
- 'maven/windows/**'
- 'scripts/gen-windows-uwp-stubs.py'
- 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java'
- 'maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java'
- 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java'
- 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml'

Expand Down Expand Up @@ -94,19 +96,23 @@ jobs:
uses: actions/setup-java@v5
with: { distribution: 'temurin', java-version: '8' }

- name: Install xwin + lay out the Windows SDK (x64)
- name: Install xwin + lay out the Windows SDK (x64 + arm64)
env:
# Harden against transient crates.io / download flakes ("curl [16]
# HTTP2 framing layer", "[55] OpenSSL SSL_read unexpected eof"):
# force HTTP/1.1 and let cargo retry.
CARGO_NET_RETRY: "10"
CARGO_HTTP_MULTIPLEXING: "false"
# --manifest-version 16 (SDK 10.0.22621): the current VS 17 manifest
# lists Win11SDK_10.0.26100 without the per-arch header packages xwin
# 0.9.0 looks for, so the default splat fails with "unable to find
# x86_64 headers". Every target here builds against 22621.
run: |
set -e
n=0
until [ "$n" -ge 3 ]; do
cargo install xwin --version 0.9.0 --locked \
&& xwin --accept-license --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \
&& xwin --accept-license --manifest-version 16 --arch x86_64,aarch64 splat --output "$RUNNER_TEMP/xwin-sdk" \
&& break
n=$((n+1))
echo "xwin install/splat attempt $n failed; retrying after backoff..."
Expand Down Expand Up @@ -178,12 +184,14 @@ jobs:
CN1_CLANG_CL: clang-cl
CN1_LLVM_RC: llvm-rc
CN1_UWP_EXE_OUT: ${{ github.workspace }}/uwp-smoke/app.exe
CN1_KIOSK_EXE_OUT: ${{ github.workspace }}/kiosk-smoke/app.exe
run: |
cd vm
JAVA_HOME="$JDK_8_HOME" mvn -B -pl tests -am test \
'-Dtest=CleanTargetIntegrationTest#crossCompilesWindowsExeWithXwin' \
'-Dsurefire.failIfNoSpecifiedTests=false'
test -f "$CN1_UWP_EXE_OUT"
test -f "$CN1_KIOSK_EXE_OUT"

# The development certificate windows.xbox builds create, made by the
# builder's own openssl commands; the Windows job signs with it, so a
Expand All @@ -209,6 +217,14 @@ jobs:
# The manifest is pinned to WindowsNativeBuilder.buildXboxAppxManifest by
# WindowsNativeBuilderArchTest, so the run below installs what windows.xbox
# builds ship.
- name: Upload the kiosk smoke app
uses: actions/upload-artifact@v4
with:
name: kiosk-smoke
path: kiosk-smoke/app.exe
if-no-files-found: error
retention-days: 3

- name: Upload the UWP smoke app
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -316,3 +332,97 @@ jobs:
path: uwp-run/
if-no-files-found: ignore
retention-days: 7

# The windows.embedded=true build on a real desktop: the window must cover the
# primary monitor with no frame, stay topmost, and survive the shortcuts the
# kiosk hook blocks. Shell Launcher itself needs IoT Enterprise, which no
# runner has; the deploy script is unit-tested in build-engine instead.
kiosk-run:
name: launch the kiosk build (Windows x64)
needs: cross-compile
runs-on: windows-latest
timeout-minutes: 15
steps:
- name: Download the kiosk smoke app
uses: actions/download-artifact@v4
with:
name: kiosk-smoke
path: kiosk-smoke

- name: Launch, check the window, send the blocked shortcuts
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing, System.Windows.Forms
Add-Type @'
using System;
using System.Runtime.InteropServices;
public static class K {
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
[DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h, uint m, IntPtr w, IntPtr l);
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra);
public static void Chord(byte mod, byte key) {
keybd_event(mod, 0, 0, UIntPtr.Zero); keybd_event(key, 0, 0, UIntPtr.Zero);
keybd_event(key, 0, 2, UIntPtr.Zero); keybd_event(mod, 0, 2, UIntPtr.Zero);
}
}
'@
New-Item -ItemType Directory -Force kiosk-run | Out-Null
$log = Join-Path (Resolve-Path kiosk-run) 'app.log'
$env:CN1_LOG_FILE = $log
$p = Start-Process -FilePath (Resolve-Path kiosk-smoke/app.exe) -PassThru
$h = [IntPtr]::Zero
for ($i = 0; $i -lt 60 -and $h -eq [IntPtr]::Zero; $i++) {
Start-Sleep -Seconds 1
if ($p.HasExited) { throw "the kiosk app exited with $($p.ExitCode) before showing a window" }
$p.Refresh(); $h = $p.MainWindowHandle
}
if ($h -eq [IntPtr]::Zero) { throw "the kiosk app showed no window in 60s" }
Start-Sleep -Seconds 3
$style = [K]::GetWindowLong($h, -16); $ex = [K]::GetWindowLong($h, -20)
$r = New-Object K+RECT; [void][K]::GetWindowRect($h, [ref]$r)
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
Write-Host ("style=0x{0:X8} ex=0x{1:X8} rect={2},{3},{4},{5} screen={6}" -f $style, $ex, $r.L, $r.T, $r.R, $r.B, $b)
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height
$g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size); $g.Dispose()
$bmp.Save('kiosk-run/screen.png', [System.Drawing.Imaging.ImageFormat]::Png)
if (($style -band 0x80000000) -eq 0) { throw "the kiosk window is not WS_POPUP" }
if (($style -band 0x00C00000) -ne 0) { throw "the kiosk window has a caption" }
if (($ex -band 0x8) -eq 0) { throw "the kiosk window is not topmost" }
if ($r.L -ne $b.Left -or $r.T -ne $b.Top -or $r.R -ne $b.Right -or $r.B -ne $b.Bottom) {
throw "the kiosk window does not cover the primary monitor"
}
# Alt+F4 would close an ordinary window; the kiosk hook swallows it.
# The hook filters only while the kiosk window is in front, so the
# check means nothing unless it is: a refused SetForegroundWindow
# would send the chord to another window and pass vacuously. A lone
# Alt tap releases the foreground lock for this process.
for ($i = 0; $i -lt 10 -and [K]::GetForegroundWindow() -ne $h; $i++) {
[K]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero); [K]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
[void][K]::SetForegroundWindow($h)
Start-Sleep -Milliseconds 300
}
if ([K]::GetForegroundWindow() -ne $h) { throw "the kiosk window never became the foreground window" }
[K]::Chord(0x12, 0x73)
[K]::Chord(0x12, 0x09)
Start-Sleep -Seconds 3
if ($p.HasExited) { throw "Alt+F4 reached the kiosk window and closed it" }
if (-not (Test-Path $log)) { throw "CN1_LOG_FILE was not created" }
if ([K]::GetForegroundWindow() -ne $h) { throw "Alt+Tab moved the foreground away from the kiosk window" }
# Control: the same window does close when asked directly, so its
# surviving Alt+F4 above was the hook, not an app that ignores closing.
[void][K]::PostMessage($h, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero)
if (-not $p.WaitForExit(15000)) { Stop-Process -Id $p.Id -Force; throw "WM_CLOSE did not close the kiosk app" }

- name: Upload the kiosk capture
if: always()
uses: actions/upload-artifact@v4
with:
name: kiosk-run-capture
path: kiosk-run/
if-no-files-found: ignore
retention-days: 7
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.linux;
package com.codename1.impl;

import com.codename1.ui.Button;
import com.codename1.ui.Component;
Expand All @@ -35,13 +35,14 @@
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.GridLayout;

/// Text editing on the embedded Linux target, where there is no native text
/// widget and usually no physical keyboard. A field being edited opens a
/// Text editing on the embedded targets (Linux on DRM/fbdev, Windows in kiosk
/// mode), where there is no native text widget or no system touch keyboard to
/// rely on, and usually no physical keyboard. A field being edited opens a
/// dialog holding a lightweight copy of it and an on-screen keyboard; a
/// physical keyboard types into the same field, so a panel with one works the
/// same way. Done delivers the text through `Display.onEditingComplete`,
/// Cancel restores the original.
final class LinuxEmbeddedEditor {
public final class EmbeddedTextEditor {
private static final String[][] LETTERS = {
{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"},
{"a", "s", "d", "f", "g", "h", "j", "k", "l", "'"},
Expand All @@ -53,10 +54,12 @@ final class LinuxEmbeddedEditor {
{"!", "?", ";", "\"", "+", "=", "_", "<", ">", "~"}
};

private LinuxEmbeddedEditor() {
private EmbeddedTextEditor() {
}

static void edit(final Component cmp, int maxSize, int constraint, final String text) {
/// Opens the editor for `cmp`; the arguments are those of
/// `CodenameOneImplementation.editString`.
public static void edit(final Component cmp, int maxSize, int constraint, final String text) {
final boolean multiLine = cmp instanceof TextArea && !((TextArea) cmp).isSingleLineTextArea();
final TextField field = new TextField(text == null ? "" : text);
field.setConstraint(constraint);
Expand All @@ -78,10 +81,10 @@ static void edit(final Component cmp, int maxSize, int constraint, final String
public void run() {
keys.removeAll();
String[][] rows = symbols[0] ? SYMBOLS : LETTERS;
for (int r = 0; r < rows.length; r++) {
Container row = new Container(new GridLayout(1, rows[r].length));
for (int k = 0; k < rows[r].length; k++) {
String label = rows[r][k];
for (String[] keyRow : rows) {
Container row = new Container(new GridLayout(1, keyRow.length));
for (String key : keyRow) {
String label = key;
if (shift[0] && !symbols[0]) {
label = upper(label);
}
Expand Down Expand Up @@ -150,6 +153,9 @@ public void actionPerformed(ActionEvent evt) {
}
}
});
// Docked to the bottom edge by showPacked below: framework chrome, never an
// operating system window of its own, whatever the theme defaults to.
dlg.setNativeWindowMode(false);
dlg.setDisposeWhenPointerOutOfBounds(false);
dlg.setScrollable(false);
dlg.getContentPane().setPreferredH(Display.getInstance().getDisplayHeight() / 2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2587,7 +2587,7 @@ public boolean isAsyncEditMode() {
@Override
public void editString(final Component cmp, int maxSize, int constraint, String text, int initiatingKeycode) {
if (Embedded.VALUE) {
LinuxEmbeddedEditor.edit(cmp, maxSize, constraint, text);
com.codename1.impl.EmbeddedTextEditor.edit(cmp, maxSize, constraint, text);
return;
}
lowLevelTextInput.stopCurrent();
Expand Down
14 changes: 14 additions & 0 deletions Ports/WindowsPort/nativeSources/cn1_windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,20 @@ extern CN1WindowsContext cn1Win;
void cn1WindowsLog(const char* message);
void cn1WinApplyPendingResize(void);
int cn1WinCreateWindow(const char* utf8Title, int width, int height);

/* The embedded (kiosk) target, cn1_windows_embedded.cpp. Each is a no-op in a
* build without CN1_WINDOWS_EMBEDDED. */
int cn1WinEmbeddedActive(void);
/* Rotates the target monitor if asked, then answers the window style and the
* screen rectangle a kiosk window covers. */
void cn1WinEmbeddedPlaceWindow(DWORD* style, DWORD* exStyle, RECT* rc);
/* After the window is shown: topmost, the cursor policy, the keyboard filter. */
void cn1WinEmbeddedWindowShown(HWND hwnd);
/* A forced density in DPI, or 0 for the monitor's own. */
int cn1WinEmbeddedDpi(void);
/* Points stdout and stderr at CN1_LOG_FILE, which cn1:deploy-device sets: a
* shell has no console, and the device's log is what cn1:device-logs reads. */
void cn1WinEmbeddedRedirectLog(void);
void cn1WinPushEvent(CN1EventType type, int x, int y, int keyCode);
/* Same, but tagged with the desktop window the event came from. */
void cn1WinPushWindowEvent(int windowId, CN1EventType type, int x, int y, int keyCode);
Expand Down
Loading
Loading