Visitar URL original
fix(legacy): resolve the window late and add bindWindow by palmoni5 · Pull Request #606 · leanflutter/window_manager · GitHub
Skip to content

fix(legacy): resolve the window late and add bindWindow - #606

Open
palmoni5 wants to merge 1 commit into
leanflutter:mainfrom
palmoni5:upstream/legacy-late-window-resolution
Open

palmoni5 wants to merge 1 commit into
leanflutter:mainfrom
palmoni5:upstream/legacy-late-window-resolution

Conversation

@palmoni5

@palmoni5 palmoni5 commented Nov 10, 2026 •

Copy link
Copy Markdown

Problem

package:window_manager/legacy.dart addresses nativeapi.WindowManager.instance.getCurrent() and caches the first answer. On Windows, GetCurrent is implemented with GetActiveWindow(), which returns no window while the app's window is still hidden (Linux and macOS fall back to a realized/first window). Apps that keep their window hidden until ready and call addListener, setPreventClose(true) and show() during startup hit this:

  • the native listener is never wired (_wireNativeEvents returns early and is never retried), so close requests are not intercepted and onWindowClose never fires;
  • isVisible() answers false, early calls do nothing, and show() returns without showing anything.

Change

  • Late resolution. A missing window is never cached: every call that needs the window looks again. When a window is first found while listeners exist or prevent-close is on, native events are wired then.
  • Windows hidden-window fallback. With no current window on Windows, the bridge takes the application's primary window if one is set, else this process's only unowned top-level window that hosts a Flutter view (a child of window class FLUTTERVIEW, which the runner parents to its top-level window), hidden or not, wrapped with nativeapi.Window.createWithNativeWindow. Top-level windows are walked with EnumWindows. With several candidates it answers nothing rather than guess; bindWindow is the way to choose. Linux and macOS are unchanged.
  • Pending show(). A show() with no window yet is kept and carried out on the first window found or bound; hide() cancels it. A one-time debug-mode log points to bindWindow.
  • WindowManager.bindWindow(nativeapi.Window? window) (public): addresses the given window and moves listeners and prevent-close to it; null goes back to the current window.
  • package:ffi becomes a direct dependency (already transitive through nativeapi).
  • README: new section "Choosing the window legacy.dart addresses".

Tests

  • New test/legacy_late_window_test.dart (fake windows, a fake close request and @visibleForTesting debugCurrentWindowOverride): a window found after the listener is wired then and its close request is cancelled under prevent-close; a missing window is looked up again on every call while a found one is kept; ensureInitialized wires a window found late; a show() without a window is carried out on the window found or bound; hide() cancels it; bindWindow moves listeners and prevent-close, and bindWindow(null) goes back.
  • New test/flutter_host_window_win32_test.dart (Windows only): real hidden top-level windows with a FLUTTERVIEW child; the sole host is found, owned windows are skipped, several candidates answer nothing, and a bare test process has none.

dart format --set-exit-if-changed ., flutter analyze (package and example) and flutter test (package and example) pass locally (Flutter 3.47.7, Windows), and the full CI workflow passes on the identical commit in my fork.

Related

Part of a set of independent PRs, each based on main and mergeable on its own: widgets #598, #599, #600, #601; legacy bridge #602, #603, #604, #605, #606 (this).

Expected conflict with #602 (show/hide events) in show()/hide(): keep the pending-show handling here and move #602's visibility check and show/hide emits into _showWindow/hide(). Merges cleanly with #603, #604 and #605; once #602/#604/#605 land, their debugBindWindow seam can delegate to bindWindow, and #605's debugApplicationOverride could also serve the primary-window lookup here.

legacy.dart addressed nativeapi.WindowManager.instance.getCurrent() and
cached the first answer. On Windows that is GetActiveWindow(), which
returns nothing while the app's window is still hidden. An app that keeps
its window hidden until ready and calls addListener, setPreventClose(true)
and show() during startup therefore lost all of them: native events were
never wired, so close requests were not intercepted and onWindowClose
never fired, and show() did nothing.

- A missing window is never remembered: every call looks again, and the
  first window found gets the listeners and prevent-close set up before
  it existed.
- On Windows, when there is no current window, the bridge takes the
  application's primary window if one is set, else this process's only
  unowned top-level window hosting a Flutter view (a child of window class
  FLUTTERVIEW), hidden or not, found with EnumWindows. With several
  candidates it answers nothing rather than guess. Linux and macOS are
  unchanged: their getCurrent() already falls back to a hidden window.
- A show() without a window is kept and carried out on the first window
  found or bound; hide() cancels it. A one-time debug message points to
  bindWindow.
- New public WindowManager.bindWindow(window) addresses a given
  nativeapi.Window and moves listeners and prevent-close to it;
  bindWindow(null) goes back to the current window.

package:ffi becomes a direct dependency (it was already transitive
through nativeapi).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant