Native desktop windows - #5556
Conversation
Adds a Desktop Windows chapter next to Desktop Integration, covering the whole feature: where windows exist and where they throw, the Form/Window relationship through TopLevelContainer, lifecycle and close vetoes, chrome and the two coordinate systems, modality, monitors and per-monitor DPI, events, peers and native editing, and the Mac Catalyst opt-in. Two things are called out rather than buried, because they are what will actually catch someone out. getComponentForm() returns null inside a Window, and the failure mode is silence rather than an exception, since most code guards on null and quietly does nothing -- so a component that will not scroll or focus in a window has a named cause. And Catalyst multi-window needs the macNative.multiWindow build hint, because a second window is a second scene and that requires a process-wide Info.plist key. Vale reports zero issues at suggestion level, LanguageTool zero matches across the guide, and the paragraph capitalization check passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a port-level test for the riskiest edit in this work, which had no coverage before and sits in the paint path where a regression shows up as wrong pixels rather than an exception. Two canvases must resolve to two distinct screen buffers -- sharing one is exactly what would make a second window draw into the first window's pixels. And isScreenGraphics has to answer true for a secondary window's buffer as well as the primary one, but still false for a mutable image: drawNativePeerImpl uses that answer to decide whether to undo the zoom scale, so a wrong answer mis-scales a window's peer components. 222 JavaSE port tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compiling CN1MacWindows.m against the actual Mac Catalyst SDK -- which the earlier commit never did -- turned up three defects that would have shipped. Scene-to-window matching was a race. Creation returns a slot immediately and requests the scene asynchronously, and the arriving scene was handed to the first unattached slot. Two windows opened in quick succession could therefore swap identities. Scenes are delivered in request order, so the pending slots are now a FIFO, enqueued on the same main-thread turn as the request; a window destroyed before its scene arrives leaves the queue. The presented frame was a use-after-free waiting to happen. flushGraphics allocates a local Java int[], and the native side wrapped that pointer in a CGBitmapContext, then used the resulting image on a later main-queue turn -- by which time the array is garbage and the collector may have reclaimed or moved it. The pixels are now copied, and handed to a CGDataProvider with a release callback rather than a bitmap context: CGBitmapContextCreateImage is copy-on-write, so it is not defined when the backing buffer becomes free to release, whereas the provider makes that lifetime explicit. The alpha format was wrong. getRGB returns straight ARGB and the image declared kCGImageAlphaPremultipliedFirst, which would darken every pixel that is not fully opaque. A window's content is opaque, so it now skips the alpha channel. Also uses slotForScene, which was dead code, to reject a scene that was already adopted. Verified by compiling both CN1MacWindows.m and CodenameOne_GLSceneDelegate.m for arm64-apple-ios-macabi against the real SDK: clean with -Wall. The same file built for plain iOS exports zero CN1MacWindow symbols, confirming the whole implementation compiles out and the iOS binary is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compiling the native sources -- which the port commits never did -- turned up three defects, one of them serious. WM_CN1_DESKTOPWINDOW was defined as WM_APP + 24, which WM_CN1_WIDGET already uses. Widget ops and desktop-window ops would have been delivered to each other's handlers, both of them casting the same LPARAM to a different struct. Moved to WM_APP + 25; the duplicate is now checked for rather than assumed absent. The two COM release calls in the Windows window layer did not resolve. This port compiles its Direct2D translation units as C++ and resolves COBJMACROS-style call sites through an explicit shim in cn1_windows_comc.h, which defines only the methods the port actually uses -- and it had no Release entry for either the HWND render target or the solid colour brush. Added both, in the shim's existing style, rather than reaching around it. On Linux, the GtkWidget-typed accessors were declared in cn1_linux.h. That header is included by translation units that have no GTK on their include path, and declaring a GtkWidget* there breaks them. Moved to cn1_linux_gfx.h, which is the header that includes gtk and where the equivalent existing declarations already live. Verified with the real toolchains available here: cn1_linux_desktopwindow.c is clean under -Wall against GTK 3, and every Windows translation unit including the new one now reports zero errors of its own. The remaining diagnostics in both ports reproduce identically on master and come from compiling Linux and Windows sources on a Mac. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ault Findings from actually building and running the Catalyst app on a Mac, which no earlier commit had done. MacWindowManager never implemented capture(), so it inherited the base class's null. Every windowed screenshot test failed with "Window capture returned null". On this platform the window's content is already rendered into a mutable image, so a capture is that raster. The screenshot harness waited a fixed 1.2s on a UITimer bound to the current form. Catalyst creates its window asynchronously -- it asks the system to activate a scene and is handed one back later -- so a fixed delay is both too long on the fast ports and too short here, and the timer's bound form is not the window anyway. It now polls for the window actually being renderable, re-queuing through callSerially rather than sleeping: the paint that makes it renderable happens on that very thread, so blocking there would stop the condition ever becoming true. macNative.multiWindow now defaults to false for the sample as well. That is measured, not cautious: with multiple scenes enabled, this suite's OrientationLockScreenshotTest captures its landscape frame and then times out after 20s trying to restore portrait. Catalyst treats a multiple-scene app's windows more like Mac windows and honours orientation requests less, so the regression belongs to the Info.plist key rather than to the window code. This gives the warning already in IPhoneBuilder a concrete mechanism instead of folklore. What the run did confirm: the Info.plist key is emitted correctly, CN1MacWindows.m compiles clean under Xcode's own flags, the app boots with multiple scenes enabled and runs all 178 tests without the crash the older comment described, and MultiWindowApiTest passes on the supported path -- so a real Catalyst Window is created, registered, resolves getTopLevelContainer() to itself, reports null from getComponentForm(), reports its monitor and scale, lays out to its own size rather than the display's, and deregisters on dispose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two corrections from further runs on real hardware. The previous commit blamed multiple scenes for OrientationLockScreenshotTest timing out while restoring portrait. That was wrong. With the key still enabled the test passed in the following runs, so it was a slow-machine flake -- the machine was compiling at the time -- not a consequence of the Info.plist key. The hint stays off by default anyway, on the honest grounds that it changes Catalyst windowing process-wide and an application should opt into that rather than have it changed underneath it. The screenshot harness was also asking the wrong question. It waited for the window to report itself showing at its requested size, but a window reports the size it was asked for before the platform has actually produced anything -- on Catalyst the scene arrives asynchronously -- so both were true within milliseconds and the capture then failed. Readiness is now "a capture succeeds", which is exactly the condition the next line depends on and is correct on every port. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running the Catalyst suite showed every windowed screenshot emitting a blank frame: the sizes differed correctly per window, but the content did not, and the harness reported the captures as duplicates of each other. The cause is that a window's raster exists from the moment it is shown, so a capture taken before the first paint cycle returns an empty frame of the right size rather than failing. The harness had no way to tell the two apart. Window now records when a paint cycle has completed and exposes hasPaintedOnce(), and the screenshot harness waits on that as well as on the capture succeeding. This is useful beyond the tests: any tooling that wants a window's content rather than its dimensions needs the same distinction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built and ran the conformance suite as a Mac Catalyst app on real hardware with multi-window enabled: 0 failures across all 178 tests, and all 14 windowed screenshots captured with distinct hashes and no duplicates -- including the modal case, whose background window is non-blank while a modal is up, which is the property that proves the event loop keeps servicing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The extra macNative.multiWindow switch existed only because Mac Catalyst scenes were unverified. They are verified now -- the whole conformance suite runs as a Catalyst app with multiple scenes enabled -- so gating it behind a second opt-in only meant CI never exercised the feature. UIApplicationSupportsMultipleScenes is a process wide Info.plist key, so it is still keyed off macNative.enabled rather than set unconditionally: that key is true for the Mac Catalyst slice only and false for iPhone and iPad builds, which keeps the iOS output byte for byte identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inspecting the Mac Catalyst captures rather than only their hashes showed three defects that distinct hashes had hidden. A Catalyst scene was never asked for the geometry the window was created with, so the system handed it the main scene's size. The window then laid out into a raster that did not match the request: several captures came out at the main display size with the window's content in the corner. The scene now requests the pending geometry as soon as it connects, and both that request and setBounds convert Codename One's pixels to UIKit's points. getBounds reports pixels to match getWidth and getHeight. The readiness probe accepted a window that had painted and could be captured, neither of which implies the size settled -- which is how the mismatch reached a golden in the first place. It now also requires the window and the captured image to be exactly the requested size, so a platform that cannot grant it fails loudly instead of baking a wrong baseline. A window used its own Window and WindowContentPane UIIDs, which no theme written before desktop windows existed defines, so it painted nothing and came up black. A window is a top level surface, so it now takes the Form, ContentPane and TitleArea styles every theme already has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
When f is a Window, this invokes the inherited Container.keyPressed(), because Window does not override the key handlers. That implementation only forwards to a container lead component, so ordinary focused controls receive no physical-key input, focus traversal never runs, and the listeners stored by Window.addKeyListener() are never fired. Window needs form-equivalent key pressed, released, repeated, and long-press dispatch.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Compared 12 screenshots: 12 matched. |
The guide gate requires every source block to come from a tagged fixture under a compiled source root, so the snippets are checked by javac rather than only by eye. This chapter had them inline. Two of them did not survive the move as written: one relied on an ellipsis inside a switch and another on a call that has no declaration, so both are now complete code. Also documents the styling a window starts out with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closing one window and opening another failed on Mac Catalyst with "scene invalidated before create completion": the system does not hand out a scene session while a previous destruction is still in flight, and the window that asked was left without one. That is an ordinary sequence, so a closed window now parks its scene for the next window to adopt rather than destroying it. The size query also answered with the size that was requested while the scene did not exist yet, so a window looked correctly sized during exactly the interval when nothing was known about it. It now answers zero until there is something real to measure, and show() keeps the requested size until a port delivers a real one instead of collapsing the window to nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cloudflare Preview
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 338ee1a6f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 163 screenshots: 163 matched. Benchmark ResultsDetailed Performance Metrics
|
Every one of these was a real defect. show() never initialized the hierarchy, unlike the setCurrent() path a Form goes through, so initComponent() never ran, the look and feel was never bound and native peers were never attached for anything added before the window was shown. Modality was only registered by showModal(), so a window given a modality type and then shown normally set the platform's modal flag while the framework kept delivering input to the windows behind it. The framework blocker and the native flag now move together, exactly once, through acquireModal/releaseModal -- which also fixes a much worse leak: the Windows port implements the native flag by disabling the main HWND, and nothing ever re-enabled it, so the application was unusable after any modal window closed. That is counted rather than flagged, so a modal nested inside another does not release the outer one's block. hide() left the component tree visible, so its components kept queuing paints onto a surface that is never painted; hasPendingPaints() then kept the event dispatch thread awake spinning on work that would never drain. The whole window overlay had no sized layers, unlike Form's, so Sheet, InteractionDialog and ToastBar had nothing to render into -- and it was made the hit testing root unconditionally, so once anything installed a layer every click landed on the overlay and the content and title stopped responding. Both now match Form. A failed native window creation produced a phantom window that painted through null graphics; it throws now. An owned window was never disposed with its owner despite the API promising it. The vetoable close listeners were fired a second time from dispose(), so one user close ran a save twice; dispose() now reports the new WindowEvent.Type.Disposed instead. setMinimumWindowSize() only stored the value: it reaches the port now and is clamped when a resize is delivered, for ports that cannot express it. The mouse wheel had no window-aware path at all -- Linux requested scroll events without connecting a handler, the Windows window procedure had no wheel case, and JavaSE resolved the target from the current form -- so rolling the wheel over a window either did nothing or scrolled the main form. And a JavaSE peer inside a window had only its attachment redirected: positioning still converted from the main canvas and removal still went to the original frame, so it was offset by the two frames' distance and left attached after removal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 163 screenshots: 163 matched. Benchmark ResultsDetailed Performance Metrics
|
Each one fails against the code as it was: the hierarchy left uninitialized by show(), a phantom window from a failed native create, a modal flag set once and never cleared, close listeners fired twice for one close, an owned window surviving its owner, a minimum size that reached nothing and clamped nothing, and a hidden window leaving paint work behind that keeps the event dispatch thread awake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 13311a9bed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two independent reasons the capture disagreed with the window it came from, both visible as a screenshot at the wrong size. The content view was created while the window still had zero bounds and relied on an autoresizing mask to grow. The mask distributes a resize *delta*, so a view that starts at zero stays at zero -- and since the size query read that view, the window reported nothing at all. The content view is now pinned to the controller's view on every layout, and the size query reads the controller's view, which is what the window manager lays out and what viewDidLayoutSubviews reports back. The raster was sized from the scene's drawable. The scene arrives and resizes asynchronously, so a raster allocated mid-resize was left holding an intermediate size that nothing later reconciled: the framework then painted into it at its own size and the capture disagreed with the window. It is sized from the framework's window now, which is what was laid out and painted; the drawable is only the fallback until a window exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1670ca0579
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Key events never reached a window's focused component. Window inherited Container's handler, which only forwards to a lead component, so focused buttons saw nothing, arrow traversal did not work and addKeyListener never fired in a secondary window. It dispatches keys itself now, the same way Form does minus the menu bar it has no equivalent of. Hiding a modal window left it blocking. HIDE_ON_CLOSE, or a plain hide(), cleared visibility while the window stayed on the modal stack with the platform's own modal flag still set, so what it covered kept rejecting input for a window nobody could reach any more. showModal() also treats hidden as finished, since parking the caller for a window that will never be disposed is a hang. Modality told the port nothing about its scope. A port implements it by disabling the window that is blocked, so window scoped modality was disabling the main window -- an unrelated part of the application. The SPI now carries the scope and the blocked window's peer. An owned window's native ownership was never established: every port ignored parentPeer, so no platform knew the child had to stay above its owner. Windows passes the owner HWND, Linux sets the transient parent (which is also what scopes GTK's modality), and JavaSE creates an owned window as a JDialog, since Swing expresses ownership only through the owner passed at construction and JFrame has no owned form. setUtilityWindow() only set a field. It reaches the port now: WS_EX_TOOLWINDOW on Windows, GDK_WINDOW_TYPE_HINT_UTILITY on Linux and Window.Type.UTILITY on JavaSE. A minimized window went on being treated as displayed, because Window inherited Container's inert hideNotify: it kept being painted and an animation in it would keep the event dispatch thread awake indefinitely. Finally, a resize kept paint work that had been computed against the old geometry. A port that reallocates its buffer on resize painted those stale rectangles into the fresh, larger one and left the rest unpainted -- which is what the Mac Catalyst captures showed as content in the corner of a bigger frame. The queue is dropped and the whole window repainted, and hasPaintedOnce() resets so nothing captures the half painted surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bea352c1ee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb4ca866ef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| gtk_window_move(GTK_WINDOW(w->window), op->a, op->b); | ||
| gtk_window_resize(GTK_WINDOW(w->window), op->c > 0 ? op->c : 1, op->d > 0 ? op->d : 1); |
There was a problem hiding this comment.
Translate Linux outer bounds before calling GTK resize
For decorated GTK windows using server-side window-manager chrome, gtk_window_resize() sizes the client window rather than the outer frame, while Window.setWindowBounds() defines its dimensions as including native chrome. Consequently, restoring or centering a saved rectangle creates a window larger than requested, and the gtk_window_get_size() round-trip below reports the client size rather than the actual desktop extent. Convert between outer and client dimensions using the frame extents so Linux honors the same bounds contract as the other ports.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The finding is correct, and checking it turned up something wider than the report, so I want to lay out the evidence before changing anything.
Window.setWindowBounds() documents its dimensions as native geometry including chrome. Comparing what each port actually produces for the same 600x450 request, from the committed baselines:
| port | requested | captured |
|---|---|---|
| Windows | 600x450 | 584x411 |
| Linux x64 | 600x450 | 600x450 |
| Linux arm64 | 600x450 | 600x450 |
| Mac Catalyst | 600x450 | 600x450 |
Windows honours the contract -- 16 wide and 39 high of chrome come off the client area. Linux does not, exactly as you describe: gtk_window_resize() sizes the client window and gtk_window_get_size() reads it back. Catalyst does not either, which your comment does not mention and which matters for the same reason.
So this is one contract honoured by one port out of three, not a Linux bug in isolation.
What I have not done is fix it, and I would rather say why than quietly leave it or quietly break something:
- Correcting Linux and Catalyst changes every windowed capture on those ports from the requested size to the requested size minus chrome. That is 14 baselines on Linux x64, 14 on Linux arm64 and 14 on Catalyst -- 42 images that would all have to be re-recorded.
- I cannot run GTK or Catalyst here, so I could neither verify the frame-extent arithmetic nor look at the re-recorded images for anything beyond their dimensions. Re-baselining 42 screenshots I cannot inspect is precisely the move that would have buried the double-composited title earlier in this PR.
- There is a timing trap in the obvious implementation:
gdk_window_get_frame_extents()returns the client rectangle before the window is realized, so insets read as zero at creation -- whensetWindowBoundsis most often called -- and the compensation silently does nothing.
My recommendation is to fix all three ports together in a change of its own, with the baselines re-recorded in the same commit and looked at by someone who can run those platforms. If you would rather I do it inside this PR I will, but I did not want to spend a green Linux port and 42 unverifiable baselines on a judgement I can make but cannot check.
Leaving this thread open deliberately, since it is not fixed.
There was a problem hiding this comment.
Now fixed in c1e1a7680e, on both ports, per the decision to do it here rather than in a follow-up.
Linux. Every request and readback is converted through the frame extents. The conversion cannot happen where the request arrives, for the reason flagged earlier: gdk_window_get_frame_extents() reports the client rectangle until the window is realized and the window manager has attached a frame. So the requested outer size is held and applied from configure-event once the chrome is measurable, with the pending flag cleared before the resize that re-enters that handler.
The zero-inset case needed care, because "realized but not yet framed" and "genuinely borderless" are the same reading. It is disambiguated by asking whether decorations were requested, not by the numbers -- answering "no chrome" for a decorated window would bake it in permanently.
Catalyst turned out to disagree with itself, which is worth recording. CN1MacWindowSetBounds already requested a system frame and carried a comment saying so, while creation converged on a content size -- so the same window reported a different size depending on which call had placed it. Creation now requests the system frame too, and the settler compares system frame against system frame via effectiveGeometry.systemFrame (same macCatalyst 16.0 availability as initWithSystemFrame:). Because request and readback are finally the same quantity, the computed chrome correction is gone: the settler only ever re-asks for the frame it first asked for, which structurally removes the 120x120-minimum and 1700x400-overshoot failures the old capped correction existed to contain.
Size restrictions had to move with it. UISceneSizeRestrictions is in content points, so pinning a non-resizable window to the requested outer size would demand a content area the size of the whole window and the frame could never be granted. Restrictions are relaxed for the request, and a fixed-size window is pinned once its geometry settles, to the content it actually got.
Correcting my own estimate: it is not 42 baselines. Linux CI runs bare Xvfb with no window manager, so nothing attaches a frame, the insets are genuinely zero, and the code defers rather than deducting a phantom chrome -- there the client area really is the outer size. Linux goldens should be unchanged; only mac-native's 14 are expected to move.
Verification, and its limits. I have no Linux machine and the local Catalyst build is currently blocked by an unrelated ParparVM issue (Window/Desktop/TopLevelContainer are not translated, so the generated test includes headers that were never emitted -- the translator is reading the current core, since recent classes like CSSColor translate fine). So both are verified through CI, not by hand here.
What I could test directly is the inset arithmetic, which I extracted into a pure function for exactly that reason. All ten decision cases pass, including the deferral trap, and the end-to-end result is the part worth reading:
requested outer -> client, with 16x39 of chrome:
400x300 -> 384x261
600x450 -> 584x411
900x700 -> 884x661
1000x400 -> 984x361
Those are byte-for-byte the sizes the Windows goldens already contain. The three ports now compute the same thing.
…ture the list host Three review findings. A live resize of the main surface queued every notification. 400 of them consume 999 of the stack's 1000 slots, after which the final size is dropped -- the hierarchy stays laid out for a size the surface no longer has -- and the releases behind it go with it, since a size change may use the termination reserve. That last part is mine: making size changes non-droppable is what let them exhaust the reserve. A queued size packet is now overwritten by a newer size instead of followed, the way drags already coalesce there, which keeps the ordering the packed stack gives. Guarding addMonitorListener() against a null implementation stopped the throw but not the silence: the ports start watching for display changes when their window manager is first created, so a listener registered before init and never followed by anything touching the desktop heard nothing. Display.init() now starts the watch if anything is listening. GenericListCellRenderer registered its monitor on a resolved top level and deregistered from a freshly resolved one, so a list removed or reparented while a ticker ran left the monitor on the original for good. That last one is the same class I said was swept, and it escaped a third distinct flaw in the sweep: the pattern required "x = getTopLevelContainer()" and this is "x = parentList.getTopLevelContainer()". Re-run allowing any receiver, it is the only risky site left; the other eight re-resolve inside deinitialize(), where the component is still attached. 5354 core tests; PMD, SpotBugs and Checkstyle at zero. The resize test grows the stack by 999 slots without the coalescing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78e6d90152
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Both scene-manifest checks scanned the entire injection, so an unrelated dictionary carrying UIApplicationSupportsMultipleScenes as true, or something shaped like a window role, answered for a manifest that enables and configures neither. The build passed and Window was still unsupported on the device, which is the exact failure this guard exists to prevent. They now run against the manifest's value element. A nested UISceneConfigurations dictionary inside it is still in scope, since the element walk honours nesting. The test asserts both sides: the unscoped lookup answers true for the bad manifest, the scoped one answers false. 25 tests here, 889 in the plugin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ad4acaeb8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…plist value The modern pull-to-refresh spinner allocated a fresh Animation on every paint and registered it. registerAnimated de-duplicates by identity, so a new instance is never the one already there: the animation list grew by one per frame and nothing ever came off it, leaving the event dispatch thread awake for good once a refresh had run. One animation is kept for the spinner now, registered each frame as the same instance, and released from the top level that took it when spinning ends. The plist value walk searched for its closing tag with a raw indexOf, so a comment containing "</dict>" closed the element early. Validation then read a prefix of the manifest and rejected a build that is correctly configured. It skips comments now, which is the rule every other search in that guard already followed and the one I failed to carry into the nesting scan when I wrote it. 26 tests there, 890 in the plugin, 5354 core; PMD, SpotBugs and Checkstyle at zero. The commented-closing-tag case fails against the raw search. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 519efc38ac
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The activation filter I added for window pointers kept a single set of state, so two windows with a contact down at the same time shared it -- a supported path on Linux, where each window tracks its own touch sequence. A press or release in one window reset the other's filter, and a drag activated in one carried the other's first jitter straight past its threshold. It is keyed by window now, in slots claimed on press and released on release, the way the framework already keys press targets, long presses and drag histories. More simultaneous window drags than there are slots lets the gesture through rather than swallowing it: the filter is a refinement, and suppressing real input is the worse failure. The threshold state was the only part of this that I left global, having made the region and the surface size window-aware in the same change -- so the review is right that it was the outlier. 148 tests in WindowTest, 5355 core; PMD, SpotBugs and Checkstyle at zero. The new case drags window B on one pixel of jitter when the state is shared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 540a283d11
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
setTimeout() scheduled the timer immediately, against whatever resolveHost() answered at that moment. Called before the dialog is shown -- which is the natural order -- that is the current form, so a popup later anchored in a window had its timeout bound to the wrong surface, and if that form was replaced its animations stopped and the dialog never timed out. In an application with no form at all, resolveHost() answers null and UITimer.schedule() threw. The timeout is recorded and started when the dialog is shown, from both show paths, once there is a host to bind it to. Setting one while already showing still starts it immediately. Two tests: a timeout set before an anchored popup ends up bound after the window is resolved, and setting one with no form at all no longer throws. The second throws a NullPointerException against the old code. 5357 core tests; PMD, SpotBugs and Checkstyle at zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be0898d763
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # docs/website/data/port_status_reports/android.json # docs/website/data/port_status_reports/ios-gl.json # docs/website/data/port_status_reports/ios-metal.json # docs/website/data/port_status_reports/javascript.json # docs/website/data/port_status_reports/linux-arm64.json # docs/website/data/port_status_reports/linux-x64.json # docs/website/data/port_status_reports/mac-native.json # docs/website/data/port_status_reports/tvos.json # docs/website/data/port_status_reports/watchos.json # docs/website/data/port_status_reports/windows-arm64.json # docs/website/data/port_status_reports/windows-x64.json # scripts/hellocodenameone/conformance/test_port_status.py
getWindowManager() lazily created the manager with an unsynchronized null check, and both of its entry points are reachable off the EDT -- Desktop.isSupported() and the Window constructor are callable from any thread. Two threads could each see a null field and each construct one. The cost is not a wasted allocation. The constructor starts the monitor-topology poller, a daemon timer that wakes every two seconds. Only the last manager stays reachable through the field, so only that one can ever be stopped: the loser's poller keeps running for the life of the process, reporting every topology change a second time and outliving the deinitialize() meant to end it -- across a later Display restart included. Both the lazy init and the matching teardown in deinitialize() now run under one lock. JavaSEWindowManagerLazyInitTest races 12 threads through getWindowManager() for 25 rounds and asserts they all get the same instance, since a second instance is precisely a second poller. Without the fix it fails on round 0 with 5 managers built and 4 unstoppable pollers left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a2f952b974
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two-finger touchpad scrolling did nothing on the Linux port -- in secondary windows and, identically, in the main window, which had the same gap from the start. A touchpad reports no discrete steps, so GDK emits only a GDK_SCROLL_SMOOTH event for it: gdk/wayland/gdkdevice-wayland.c emits a discrete event only when the device reports discrete_x/discrete_y, and the XI2 backend turns scroll valuators straight into a smooth event. Neither widget selected GDK_SMOOTH_SCROLL_MASK, and gdkwindow.c drops a smooth event before delivery when the window has not selected that mask, so the scroll callbacks were never invoked at all for a touchpad. Both widgets now select the mask and translate the smooth deltas. Selecting it also makes GDK drop the pointer-emulated discrete events a real wheel produces, so the discrete branches and the smooth branch cannot both fire for one physical movement. The deltas cannot be forwarded one for one: they are fractions of a notch and arrive continuously, while wheelUnits() on the Java side floors any sub-notch delta to a whole notch -- forwarding each one would turn a gentle drag into a page-per-frame stampede. cn1LinuxTakeWholeNotches() dispatches whole notches and carries the remainder, per window so two windows cannot consume each other's partial notches, and an is_stop marker clears it so a gesture cannot inherit a partial notch from the previous one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Window.setWindowBounds() defines its dimensions as native geometry including chrome. Windows honoured that -- a 600x450 request yields a 584x411 client area -- while Linux and Mac Catalyst both sized the client area to the requested figure, so the same request produced a different window on each of the three ports and a bounds round trip disagreed with itself by the chrome. Linux: gtk_window_resize() and gtk_window_get_size() both speak the client area, so every request and readback is now converted through the frame extents. The conversion cannot happen where the request arrives: gdk_window_get_frame_extents() reports the client rectangle until the window is realized and the window manager has attached a frame, which is normally after setWindowBounds has been called -- and zero insets are indistinguishable from a genuinely borderless window except by asking whether decorations were requested. So the requested outer size is held and applied from configure-event once the chrome is measurable, with the pending flag cleared before the resize that re-enters that handler. Catalyst: the port already disagreed with itself. CN1MacWindowSetBounds requested a system frame and said so, while creation converged on a *content* size, so a window reported a different size depending on which call had placed it. Creation now requests the system frame too, and the settler compares system frame against system frame. Because request and readback are finally the same quantity, the computed chrome correction is gone -- the settler only ever re-asks for the frame it first asked for, which removes the overshoot the old computed correction was capped to contain. Size restrictions had to move with it: UISceneSizeRestrictions is in content points, so pinning a non-resizable window to the requested outer size would demand a content area the size of the whole window and the frame could never be granted. Restrictions are relaxed for the request and a fixed-size window is pinned once its geometry settles, to the content it actually got. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 10f627222f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
In pureTouch mode a component shows its selection only while a contact is down on it, which shouldRenderSelection(Component) answers from a single flag and the global pointerX/pointerY. With a contact down in two windows whichever window's packet ran last owned both. Two distinct failures came out of that. A press/release cycle in one window cleared the flag while another window was still held, so the held window's component silently dropped its selection. And a component was tested with c.contains(pointerX, pointerY) against whatever window moved the pointer last -- window coordinates are window relative, so that is not merely the wrong point, it is a point in a different coordinate space. The flag and the coordinates it is tested with are now per window, tracked in the same slot table as the other per-window pointer state. Window zero keeps the singleton and the global coordinates, so the single-window path is unchanged. The component-less shouldRenderSelection() answers "any window has a press down", which is what it meant when there was only one. hideNotify() clears every window rather than one, because losing input is not a per-window event. WindowSelectionStateTest covers both halves and each fails without the fix on its own assertion. UITestBase clears the new table between tests: a window left held would otherwise carry into the next test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WindowsWindowManager never overrode WindowManager.capture(), so it returned the base null and Window.capture() took its documented fallback: re-rendering the component hierarchy into a fresh image. That fallback draws what the window *should* be showing, so it cannot tell a correct window from one whose raster and hierarchy disagree, and it contains no native peer or editor at all -- which is most of what the windowed screenshot suite exists to check. The Windows windowed goldens were therefore passing without ever exercising the Direct2D secondary-window surface. The main-surface capture could not simply be reused. It encodes the render target's WIC bitmap, and a secondary window draws into an ID2D1HwndRenderTarget, which Direct2D offers no readback for -- there is no bitmap to hand the encoder, which is what its own "window target is not WIC-backed" branch already said. So the window is asked to render itself into a DC through PrintWindow instead. PW_RENDERFULLCONTENT is the load-bearing flag: without it a Direct2D or DirectComposition surface comes back blank. PW_CLIENTONLY keeps the frame out, so the result is the rectangle the framework laid out and the goldens are sized to. The flag is #defined defensively rather than assumed, because an older SDK header omits it and losing it silently would produce empty goldens rather than a compile error. Verified with scripts/check-native-signatures.sh, which is the check that matters for a new native: a wrong symbol name still compiles and links, and the dead-code pass then drops the Java method, leaving the feature inert. Windows reports 480 native methods all resolving, 0 fatal. Expect the windows-x64 and windows-arm64 windowed goldens to move: they were recorded from the re-render fallback and will now be real readbacks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c7a7cd216
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Each time a Mac Catalyst window's content size changed, getNativeGraphics allocated a new mutable image and overwrote the previous one without releasing it. IOSImplementation.NativeImage frees its native peer only from finalize(), and the Java wrapper is a few dozen bytes in front of a width*height*4 native allocation -- so the collector sees almost no pressure while a live resize hands out a new multi-megabyte raster per size step. The dispose path had the same shape: it nulled the field and left the raster to a finalizer, and that is the largest single allocation a window owns. Both now release explicitly. Nothing outside holds the raster across the swap: capture() deliberately copies the pixels into an independent image rather than handing the live one out, and the paint loop re-fetches this graphics every frame rather than caching it -- so the released image has no remaining reader. No test: the iOS port has no test module, and this is not reachable from core-unittests. Verified by compiling the port and by SpotBugs over the ios module, which reports zero findings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c5ee65734
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The window drag activation filter claims one of eight slots on press and releases it on release. A window disposed -- or losing the native pointer -- while a press is still down never delivers that release, so the slot stayed claimed. Window ids are never reused, so it was held by a dead id for the life of the process. After eight such windows there are no slots left. windowDragSlot() then answers -1 and the drag path takes its deliberate passthrough, which exists so that more simultaneous drags than slots are let through rather than swallowed. From that point on every window loses the filter entirely: a pixel of jitter after a press is delivered as a real drag, which activates drag and drop and moves a draggable component on what was meant as a click. Display's windowInputCancelled() and windowDisposed() already cleared the framework's own per-window input records, and the comment in Window.dispose() noted that they only forget "the framework's records" -- the implementation's slot was exactly the state neither of them could reach. Both now call releaseWindowInputState(), which is a no-op for the main surface. WindowTest.disposingWindowsMidPressDoesNotExhaustTheDragSlots presses in nine windows and disposes each without a release, then checks a fresh window still filters jitter and still passes real movement. Without the fix it reports the jitter as a drag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f01f76ff0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
plistValueElementEnd tracked nesting depth with literal "<array>" and "</array>" strings while the rest of this parser matches elements structurally through plistElementIndex, which already accepts "<array >" and "<dict custom=\"x\">" as the same elements. So valid XML formatting on a container tag was invisible to the nesting scan. Both directions were wrong. A nested opening written "<array >" was missed, so the first inner "</array>" closed the outer element and the role was truncated before its delegate -- a correctly configured manifest failing the build. A closing tag written "</array >" was missed entirely, the scan found no close, and the caller fell back to the rest of the fragment -- which is the worse one: a later role's delegate then vouches for a window role that names somebody else, exactly the hole the CarPlay case exists to close, reopened by a space. Going structural needs one thing the literal matching got right by accident: "<array/>" is an element but not a nesting level, and plistElementIndex matches it. Counting it would leave the depth permanently ahead and swallow the real closing tag. plistNestedElementIndex skips self-closing tags on purpose, and plistCloseElementIndex is the closing-tag counterpart of plistElementIndex, allowing only whitespace between the name and the ">". The whitespace test asserts the accepting direction is refused: without the fix it reports true for a manifest whose window role names another delegate. The self-closing and close-tag-prefix tests pass either way by construction -- they guard the new structural matching rather than reproduce the old bug, which is why they are written against the CarPlay shape. 910 plugin tests pass; SpotBugs zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d5dc9c1430
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…roll over Two gaps against the Form path, both in Window.pointerPressed. The press handle was created after the window's pointer-pressed listeners ran. A listener can enter a nested event loop -- showModal() does -- and the matching physical release is then processed inside it. With the handle created afterwards that nested release found no gesture to clear, and the method went on to install a fresh press whose release had already happened, leaving the component latched until some later gesture freed it. Form creates its handle before firing listeners; so does the framework's own press record, for exactly this reason. Moved. A press landing on a still-gliding container stopped the motion, cleared pressedCmp and returned. Stopping the glide is right, but it was only half of what Form does: Form re-enters the drag path so the same physical gesture takes the scroll over, while here every following drag packet had no target and the user had to lift and press again. The press now cancels the glide, primes drag and drop, and re-enters through this window's drag path -- not Display.pointerDragged(), which is the main surface's and would deliver to the current Form instead. It hands pressedCmp to the component the scroll was taken over from. Form reaches the same place differently, by re-resolving the component under the pointer whenever it has no pressed one; giving this window's drag path that same fallback was tried and rejected -- it changed routing for every gesture and broke five unrelated tests. While here, drag events now carry setPointerPressedDuringDrag as Form's do, read and cleared in the scalar path and reported without clearing in the multi-pointer one, matching Form on both counts. Both tests fail without their own fix. Each disposes its window in a finally, because an assertion that throws before dispose leaves a window showing and times out the next test's setup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Window.capture() falls back to re-rendering the component hierarchy when the window manager returns no pixels. That fallback produces a plausible image of the right size, so a capture path that never reads the real surface looks exactly like one that does -- which is how the missing Windows override went unnoticed in the first place. The Windows manager now says so, once per process, when the native capture comes back empty. Without it the only way to tell a live readback from a silent fallback is to find a pixel that differs, and the windowed goldens are byte-identical either way: the harness stops editing before capturing, so no native editor is in frame to give it away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 150a2f2890
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
setWindowLocation already carries a comment about this trap: the read has to happen on the event dispatch thread with the write, because setWindowBounds marshals itself and reading beforehand queues a move carrying the old size. centerOnDesktop(), centerOn() and restore() had the same shape and were missed. Both centring methods read the window's bounds, compute a position and only then call something that marshals. From a background thread that computes against geometry a queued resize is about to replace, so a caller that resized and then centred got a window centred for the size it no longer has. restore() is an ordering problem rather than an arithmetic one. showOwnerChain() may queue the owner's show(), while the native restore ran immediately -- so the child could reach the platform ahead of its owner, and a WindowManager call happened off the event dispatch thread, which is the only context that SPI is defined in. All three now marshal the whole method. Checked the rest of the class by enumerating every public method that touches the window manager or reads bounds rather than grepping for the pattern: setTitle, setResizable, setDecorated, setAlwaysOnTop, setUtilityWindow, setWindowIcon, setMinimumWindowSize, minimize, toggleMaximize and requestWindowFocus are each a single call with no read to go stale, and every port marshals internally, so they are left alone deliberately. Both tests fail without their own fix: the centring one is out by exactly half the size difference, and the restore one sees the port called before the queue drains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codename One has no windowing API. Even on JavaSE, Mac, Win32 and Linux, where the OS has real windows, an app gets exactly one, welded to a single global "current Form":
CodenameOneImplementationholds onecurrentForm,Display.edtLoopImplpaints one surface per tick,paintDirtyuses one global paint queue clipped togetDisplayWidth()/getDisplayHeight(), andhandleEventroutes every input event to one form. Everything that looks like a second window today —Sheet,InteractionDialog,ToastBar,Dialog— is an overlay inside the current form's layered panes.This adds real native windows, each rendering its own component tree, on all four desktop targets, without changing the single-form model mobile depends on.
API
TopLevelContaineris the shared contractFormandWindowboth implement. Its members were chosen by counting actualgetComponentForm().<method>()chains inCodenameOne/src, and every one of them was already public onFormwith an identical signature, soFormneeded nothing beyond theimplementsclause andasContainer()— a Java interface cannot extend a class, so without that bridge aTopLevelContainerreference cannot go anywhere aComponentis wanted.Window extends Container implements TopLevelContainer. Inside a windowgetComponentForm()returnsnull, by design;Component.getTopLevelContainer()is the new resolution API, and core now uses it internally.DesktopandMonitorare the public parallel toDisplayfor "what screens exist and what windows are open", including per-monitor DPI and backing scale;Displaykeeps meaning "the main app surface" exactly as before.Modality is enforced in core rather than per port, so it behaves identically everywhere:
Displaykeeps a modal stack andhandleEventdrops input to blocked windows.showModal()parks the caller throughinvokeAndBlockthe wayDialogalready does, which re-enters the event loop — so every other window stays live and repainting while a modal is up.Implementation
The impl SPI is a single
WindowManagerfacade returned fromCodenameOneImplementation.getWindowManager(). Returningnullis the capability query, so there is no separateisMultiWindowSupported()that could drift from it. Only genuinely universal operations are abstract; anything a port might not offer has a no-op default, so adding a capability later never breaks a port.Per-window paint state moves into a
PaintSurfacevalue object with the main window as instance zero;getCodenameOneGraphics(),repaint(Animation),cancelRepaintandhasPendingPaints()keep their signatures, so every existing port still compiles and behaves.paintDirty()'s body is parameterized rather than globally rebound — a global "active surface" was rejected becauseDisplay.getDisplayWidth()is public and callable off the EDT, so a live binding would change its answer re-entrantly across ~210 call sites.Events pack the window id into the type word (
type | (windowId << 8)). Window 0 is numerically identical to the previous wire format, so drag coalescing and the stack-swap logic are untouched. The port is handed the id at creation and echoes it back, so there is no peer-to-window map on the off-EDT input path.Ports: JavaSE (per-canvas graphics de-singletonization —
getNativeGraphicsused to return one shared instance, andisScreenGraphicswas an identity check against one buffer, so a second window would have drawn into the first window's pixels), native Windows (Direct2D per-window render targets,GWLP_USERDATAidentity,WM_DPICHANGED), native Linux (per-window cairo back buffer, GTK closure data), and Mac Catalyst (UIWindowSceneper window). Peer components and native text editing work in every window on every one of the four. iOS, Android and JavaScript need no port changes at all: they inherit the false capability and the throw lives in core.Latent bug fixed on the way
handleEventreturnedoffsetunchanged when the form was null, while the caller loopswhile (offset < actualTmpPointer)— an infinite EDT spin. It is unreachable today only because all nine entry points guard ongetCurrentForm() != null; window disposal with events in flight makes it reachable. It is now askipEventthat drains the packet so the rest of the batch still dispatches.Testing
Core unit tests drive a scriptable fake
WindowManageronTestCodenameOneImplementation— settable, defaulting to null, so the unsupported path is the default — covering lifecycle, paint isolation, event routing, modality including a modal window nested in a modal dialog, theTopLevelContainercontract, and a fake multi-monitor table at mixed DPI. JavaSE port tests cover the per-canvas graphics resolution, which is the riskiest edit here and had no coverage before.The centrepiece is a windowed screenshot family in
scripts/hellocodenameone: representative UI re-run inside a real window at several sizes and compared against its own goldens. A picture of a window proves nothing; layout, scrolling, graphics, layered overlays, native editing and modality rendering correctly on a non-primary surface is the actual claim. The three sizes, including a deliberately non-square one, are what prove content lays out to the window rather than toDisplay.getDisplayWidth(). This needed per-window capture on every port, since the existing pipeline can only see the main framebuffer.Mac Catalyst was built and run on real hardware for this branch rather than left to CI, because it is the hardest of the four. That found four defects compiling never would have:
capture()was unimplemented; the readiness probe was a false positive; captures were taken before the first paint; and the scene was never asked for the geometry the window was created with, so several captures came out at the main display size with the window's content in the corner.Known scope limits, documented
HTMLComponent, accessibility on secondary windows,Dialog.show()from inside a window and form transitions into or out of one are out of scope for v1 and called out in the guide.Display.getDisplayWidth()/getDisplayHeight()keep reporting the main window; components inside a window use their top level's size.🤖 Generated with Claude Code