Skip to content

Cancellable window close request (and app quit request) event #65

Open

Description

@lijy91

There's no way to intercept a window being closed. Only WindowClosedEvent fires after the fact. window_manager 0.6.0 currently can't implement setPreventClose / onWindowClose for the system close button, and documents that as a gap.

Proposal: a cancellable close-request event per window, raised by the close button, Alt+F4, the taskbar/Dock menu and delete-event, plus Window::Close(). Requirements from the reports below:

  • system shutdown, logoff and installer-initiated closes (WM_QUERYENDSESSION/WM_ENDSESSION, or a WM_CLOSE from another process) must not be silently swallowed
  • Cmd+Q / app termination on macOS should surface as an application-level quit request (cancellable), distinct from window close
  • it must not break Flutter's own AppLifecycleListener.onExitRequested when nobody listens
  • possibly the same pattern for minimize/maximize requests

Background:

Activity

  1. lijy91 commented on Oct 6, 2026

    @lijy91
    MemberAuthor

    Implemented in 3cc8b3d (all five bindings: libnativeapi/nativeapi@1630105).

    • Shared EventRequest / EventDecision for cancellation and deferred decisions, with retained async delivery and immediate unsubscription.
    • Application::Quit() and Window::Close() go through confirmation requests; native window close (macOS, Windows, GTK) and native macOS app quit are routed there, keeping host delegates and their decisions.
    • Binding-owned votes are fenced by their event loop's lifetime. JS keeps deliveries alive through Promise completion and cancels its votes when its environment ends; Python decides from asyncio.

    Verified: core CTest 26/26 on macOS. Real Flutter 3.47.6: multi-window ownership (19 checks) and Application/AppLifecycleListener exit (31 checks). Real Node regressions on macOS, Windows and Linux (18 each). The window fixture still prints "Communicating on a dead channel." at engine shutdown, with exit 0.

    Still open: checking the real desktop entry points, i.e. the OS title-bar close button, the system-menu close, and app quit from the Dock/menu/taskbar.

  2. lijy91 commented on Oct 7, 2026

    @lijy91
    MemberAuthor

    Windows desktop entry points verified in a real Flutter app (libnativeapi/nativeapi@e0ca21a).

    flutter_window_title_bar_example gets a Closing option: Keep open cancels every WindowCloseRequestedEvent through event.request.cancel() and counts it; Allow lets it through. tools/gui/flutter_window_title_bar_close_test.ps1 drives it on Windows 11 with owner-guarded real clicks on the example's own window and menu, no keyboard. 3/3 runs, all checks passing:

    • Keep open, a real click on the window's own close button (located through DWM's caption button bounds): the window stays, one request cancelled.
    • Keep open, the system menu's Close (from the DragToMoveArea strip): the window stays, a second request cancelled.
    • Allow, the close button again: the window closes and the app exits with code 0.

    The #66 system-menu and #70 snap-layout end-to-end checks still pass on the same build.

    Not covered on Windows: Alt+F4 (keyboard) and the taskbar's Close (a click on explorer's window) — both arrive as the same SC_CLOSE / WM_CLOSE path as the close button, but were not driven here.

    Still open for this issue: the real close button on macOS and on Linux (GNOME), and quitting from the Dock / app menu on macOS.

  3. lijy91 commented on Oct 7, 2026

    @lijy91
    MemberAuthor

    GNOME verified, and a Linux core bug found and fixed on the way.

    Bug (fixed in e791090): on Linux, WindowManager stored a window's ID on its GdkWindow only, and a Window wrapper made afterwards found it there but left the GtkWindow without one. The close gate and the per-window event subscription check the ID on the GtkWindow, so a window WindowManager saw first — a Flutter app's main window, taken with GetCurrent() — received no events of its own and its close requests could not be cancelled: GNOME's close button closed it regardless of the listener. The widget's ID now wins wherever one is found or allocated and goes on both objects. tests/window_close_manager_first_linux_test reproduces it (no close request, no property event before; all pass after); Linux CTest 26/26.

    Real GNOME check (tools/gui/flutter_window_title_bar_close_test_linux.py, libnativeapi/nativeapi@90180e4; Ubuntu 24.04, GNOME 46, Flutter 3.47.6, real input through Mutter RemoteDesktop with the harness's owner check, no keyboard), 3/3 runs:

    • Keep open: two real clicks on the header bar's close button (found by its accessible name through AT-SPI) leave the window open, two requests cancelled.
    • Allow: the close button closes the window and the app exits with code 0.

    The window menu on GNOME is gnome-shell's, outside the app, so its Close was not driven.

    Remaining: the close button and quitting from the Dock / app menu on macOS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions