Skip to content

Commit 3cc8b3d

Browse files
committed
Add cancellable window close and application quit requests (#65)
- Shared EventRequest / EventDecision for cancellation and deferred decisions; retained asynchronous delivery with immediate unsubscription. - Application.Quit() and Window.Close() go through confirmation requests; native window close and macOS application quit are routed to them, preserving host delegates and their decisions. - Binding-owned votes are fenced by their event loop's lifetime. - Real Flutter lifecycle and multi-window ownership tests.
1 parent 144fca5 commit 3cc8b3d

88 files changed

Lines changed: 5856 additions & 189 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎include/nativeapi.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "../src/drop_target.h"
1717
#include "../src/file_dialog.h"
1818
#include "../src/foundation/color.h"
19+
#include "../src/foundation/event_request.h"
1920
#include "../src/foundation/geometry.h"
2021
#include "../src/foundation/keyboard.h"
2122
#include "../src/image.h"
@@ -53,6 +54,7 @@
5354
#include "../src/capi/display_manager_c.h"
5455
#include "../src/capi/drag_source_c.h"
5556
#include "../src/capi/drop_target_c.h"
57+
#include "../src/capi/event_request_c.h"
5658
#include "../src/capi/file_dialog_c.h"
5759
#include "../src/capi/geometry_c.h"
5860
#include "../src/capi/image_c.h"

‎src/application.cpp‎

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,200 @@
1+
#include <atomic>
2+
13
#include "application.h"
4+
#include "application_quit_dispatch.h"
5+
#include "foundation/dispatcher.h"
6+
#include "foundation/event_request_dispatch.h"
27

38
namespace nativeapi {
49

10+
struct detail::ApplicationQuitState {
11+
std::shared_ptr<EventRequest> request;
12+
std::atomic<bool> dispatch_failed{false};
13+
bool retain_for_host = false;
14+
bool has_loop_owner = false;
15+
std::weak_ptr<void> loop_owner;
16+
};
17+
518
Application& Application::GetInstance() {
619
static Application instance;
720
return instance;
821
}
922

23+
void Application::RequestQuit(int exit_code,
24+
std::function<void(int)> stop_loop,
25+
std::shared_ptr<void> loop_owner,
26+
std::function<void(bool)> native_confirmation) try {
27+
const bool has_loop_owner = !!loop_owner;
28+
auto dispatch = CreateGuardedCallback<>(
29+
std::function<void()>([this, exit_code, stop_loop = std::move(stop_loop), has_loop_owner,
30+
weak_owner = std::weak_ptr<void>(loop_owner),
31+
native_confirmation = std::move(native_confirmation)] {
32+
try {
33+
if (has_loop_owner && weak_owner.expired())
34+
return;
35+
exit_code_ = exit_code;
36+
if (quit_state_) {
37+
if (!quit_state_->dispatch_failed.load() &&
38+
!(quit_state_->has_loop_owner && quit_state_->loop_owner.expired()))
39+
return;
40+
InvalidateQuitRequest();
41+
}
42+
43+
auto state = std::make_shared<detail::ApplicationQuitState>();
44+
state->retain_for_host = !!native_confirmation;
45+
state->has_loop_owner = has_loop_owner;
46+
state->loop_owner = weak_owner;
47+
std::weak_ptr<detail::ApplicationQuitState> weak_state = state;
48+
49+
auto finish = CreateGuardedCallback<bool>(std::function<void(bool)>(
50+
[this, stop_loop, weak_state, native_confirmation](bool accepted) {
51+
// Keep the request installed until its completion reaches the UI thread.
52+
// Reentrant and worker requests cannot start a second confirmation.
53+
auto state = weak_state.lock();
54+
if (!state || quit_state_ != state)
55+
return;
56+
if (!accepted || (state->has_loop_owner && state->loop_owner.expired())) {
57+
quit_state_.reset();
58+
if (native_confirmation)
59+
native_confirmation(false);
60+
return;
61+
}
62+
// Keep this attempt installed through its action too. An exiting
63+
// listener can call Quit(), but must not start recursive termination.
64+
const int code = exit_code_;
65+
try {
66+
if (native_confirmation) {
67+
native_confirmation(true);
68+
} else if (stop_loop) {
69+
// A notification failure cannot undo an already approved quit.
70+
try {
71+
Emit<ApplicationExitingEvent>(code);
72+
} catch (...) {
73+
}
74+
stop_loop(code);
75+
} else {
76+
PerformQuit(code);
77+
}
78+
} catch (...) {
79+
}
80+
if (quit_state_ == state && !state->retain_for_host)
81+
quit_state_.reset();
82+
}));
83+
std::shared_ptr<EventRequest> request;
84+
try {
85+
request =
86+
detail::EventRequestDispatch::Create(true, [finish, weak_state](bool accepted) {
87+
auto complete = [finish, accepted] { finish(accepted); };
88+
if (IsMainThread())
89+
complete();
90+
else {
91+
try {
92+
if (RunOnMainThread(std::move(complete)))
93+
return;
94+
} catch (...) {
95+
}
96+
if (auto state = weak_state.lock())
97+
state->dispatch_failed = true;
98+
}
99+
});
100+
} catch (...) {
101+
// Allocation failure must not silently approve termination.
102+
return;
103+
}
104+
state->request = request;
105+
quit_state_ = state;
106+
try {
107+
Emit<ApplicationQuitRequestedEvent>(request);
108+
} catch (...) {
109+
request->Cancel();
110+
}
111+
detail::EventRequestDispatch::Finish(request);
112+
} catch (...) {
113+
// The UI scheduler invokes this after a worker Quit() has returned.
114+
// Failure here must also leave the application running.
115+
}
116+
}));
117+
if (IsMainThread())
118+
dispatch();
119+
else
120+
(void)RunOnMainThread(std::move(dispatch));
121+
} catch (...) {
122+
// Public Quit() never throws, including failures to allocate/schedule work.
123+
}
124+
125+
void Application::InvalidateQuitRequest() {
126+
if (quit_state_)
127+
detail::EventRequestDispatch::Invalidate(quit_state_->request);
128+
quit_state_.reset();
129+
}
130+
131+
void detail::ApplicationQuitDispatch::RequestForLoop(int exit_code,
132+
std::function<void(int)> stop_loop,
133+
std::shared_ptr<void> loop_owner) {
134+
if (stop_loop)
135+
Application::GetInstance().RequestQuit(exit_code, std::move(stop_loop), std::move(loop_owner));
136+
}
137+
138+
void detail::ApplicationQuitDispatch::CancelForLoop(std::weak_ptr<void> loop_owner) try {
139+
auto& app = Application::GetInstance();
140+
auto cancel = app.CreateGuardedCallback<>(std::function<void()>([&app, loop_owner] {
141+
const auto& state = app.quit_state_;
142+
if (state && state->has_loop_owner && !state->loop_owner.owner_before(loop_owner) &&
143+
!loop_owner.owner_before(state->loop_owner))
144+
app.InvalidateQuitRequest();
145+
}));
146+
if (IsMainThread())
147+
cancel();
148+
else
149+
(void)RunOnMainThread(std::move(cancel));
150+
} catch (...) {
151+
// Expiring the binding's owner also fences later requests and completions,
152+
// including when scheduling this eager invalidation fails.
153+
}
154+
155+
std::shared_ptr<EventRequest> detail::ApplicationQuitDispatch::CurrentRequest() {
156+
auto& app = Application::GetInstance();
157+
if (!IsMainThread())
158+
return nullptr;
159+
const auto& state = app.quit_state_;
160+
if (state &&
161+
(state->dispatch_failed.load() || (state->has_loop_owner && state->loop_owner.expired())))
162+
app.InvalidateQuitRequest();
163+
return app.quit_state_ ? app.quit_state_->request : nullptr;
164+
}
165+
void detail::ApplicationQuitDispatch::RequestFromNative(std::function<void(bool)> confirmation) {
166+
if (confirmation && IsMainThread())
167+
Application::GetInstance().RequestQuit(0, nullptr, nullptr, std::move(confirmation));
168+
}
169+
void detail::ApplicationQuitDispatch::HoldForHost(const std::shared_ptr<EventRequest>& request) {
170+
auto& app = Application::GetInstance();
171+
if (IsMainThread() && app.quit_state_ && app.quit_state_->request == request)
172+
app.quit_state_->retain_for_host = true;
173+
}
174+
void detail::ApplicationQuitDispatch::ReleaseFromHost(
175+
const std::shared_ptr<EventRequest>& request) {
176+
auto& app = Application::GetInstance();
177+
if (IsMainThread() && app.quit_state_ && app.quit_state_->request == request)
178+
app.InvalidateQuitRequest();
179+
}
180+
int detail::ApplicationQuitDispatch::ExitCode() {
181+
return Application::GetInstance().exit_code_;
182+
}
183+
void detail::ApplicationQuitDispatch::NotifyRequired() try {
184+
auto& app = Application::GetInstance();
185+
if (!IsMainThread())
186+
return;
187+
app.InvalidateQuitRequest();
188+
auto request = EventRequestDispatch::Create(false, nullptr);
189+
try {
190+
app.Emit<ApplicationQuitRequestedEvent>(request);
191+
} catch (...) {
192+
}
193+
EventRequestDispatch::Finish(request);
194+
} catch (...) {
195+
// Mandatory native termination cannot be vetoed by a failed notification.
196+
}
197+
10198
int RunApp(std::shared_ptr<Window> window) {
11199
return Application::GetInstance().Run(window);
12200
}

‎src/application.h‎

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <functional>
44
#include <memory>
55
#include <string>
6+
#include <utility>
67
#include <vector>
78

89
#include "foundation/event.h"
@@ -13,6 +14,12 @@
1314

1415
namespace nativeapi {
1516

17+
class EventRequest;
18+
namespace detail {
19+
class ApplicationQuitDispatch;
20+
struct ApplicationQuitState;
21+
} // namespace detail
22+
1623
// ---------------------------------------------------------------------------
1724
// Events
1825
// ---------------------------------------------------------------------------
@@ -69,11 +76,24 @@ class ApplicationDeactivatedEvent : public ApplicationEvent {
6976

7077
/**
7178
* @brief Event emitted when the application receives a quit request
79+
*
80+
* Programmatic Quit() and macOS native quit queries supply a shared,
81+
* cancelable request. Native termination that must continue supplies a
82+
* noncancelable request. An existing host retains its final termination decision.
7283
*/
7384
class ApplicationQuitRequestedEvent : public ApplicationEvent {
7485
public:
75-
ApplicationQuitRequestedEvent() = default;
86+
explicit ApplicationQuitRequestedEvent(std::shared_ptr<EventRequest> request = nullptr)
87+
: request_(std::move(request)) {}
88+
/**
89+
* @brief Gets the shared quit confirmation request.
90+
* @return The shared request supplied by the producer.
91+
*/
92+
std::shared_ptr<EventRequest> GetRequest() const { return request_; }
7693
std::string GetTypeName() const override { return "ApplicationQuitRequestedEvent"; }
94+
95+
private:
96+
std::shared_ptr<EventRequest> request_;
7797
};
7898

7999
/**
@@ -177,26 +197,46 @@ class Application : public EventEmitter<ApplicationEvent> {
177197
/**
178198
* @brief Request the application to quit
179199
*
180-
* Emits ApplicationQuitRequestedEvent, then stops the event loop: Run()
200+
* Emits ApplicationQuitRequestedEvent with a cancelable EventRequest. A
201+
* listener can Cancel() or Defer() it for asynchronous confirmation. Only
202+
* after every listener has finished and every deferred decision has accepted
203+
* does the event loop stop: Run()
181204
* returns @p exit_code after emitting ApplicationExitingEvent, and the
182205
* process keeps going. Windows stay open until released. Callable from any
183206
* thread: the request is carried over to the main thread, where the events
184207
* are emitted.
208+
* Repeated Quit() calls during confirmation share the pending request and
209+
* update its exit code. A cancelled request leaves the application running;
210+
* a later Quit() starts a fresh request. Exceptions during request dispatch
211+
* cancel it. A failed main-thread dispatch does not quit from a worker.
185212
*
186213
* When the loop is not Run()'s own (a host such as a Flutter runner owns
187214
* it), there is nothing to return to and the process ends instead, after
188215
* ApplicationExitingEvent: on macOS through -[NSApplication terminate:],
189-
* which exits with status 0 whatever @p exit_code is; on Linux with
216+
* which exits with status 0 whatever @p exit_code is after the host delegate
217+
* allows termination (the host may cancel or defer); on Linux with
190218
* @p exit_code; on Windows the main thread's windows are destroyed while
191219
* the host's loop still runs (a Flutter view shuts its engine down then),
192220
* and the loop gets WM_QUIT and ends it.
193221
*
194-
* A quit the user or the system starts outside Quit() (Cmd+Q, logout) also
195-
* emits ApplicationQuitRequestedEvent. On macOS it cannot be vetoed and
196-
* ends the process without Run() returning.
222+
* On macOS, a native AppKit quit query (Cmd+Q, Dock Quit, or an eligible
223+
* system request) also emits a cancelable ApplicationQuitRequestedEvent.
224+
* Native approval resumes the existing host's decision; the host may still
225+
* refuse or defer. Duplicate attempts share the pending confirmation through
226+
* the host's asynchronous reply. Mandatory termination only emits a
227+
* noncancelable request, invalidates pending votes, and continues. Allowed
228+
* native termination ends the process without Run() returning.
197229
*
198230
* @param exit_code The exit code to use when quitting (default: 0)
199231
*
232+
* @note Platform availability:
233+
* - macOS: Supported - cancelable programmatic and native quit queries, then the host decision
234+
* - Windows: Supported - cancelable Quit(), then the native loop stops
235+
* - Linux: Supported - cancelable Quit(), then the native loop stops
236+
* - Android: Not applicable - ignored; the Activity owns its lifecycle
237+
* - iOS: Not applicable - ignored; UIApplication owns its lifecycle
238+
* - OpenHarmony: Not applicable - ignored; the Ability owns its lifecycle
239+
*
200240
* @code
201241
* auto& app = Application::GetInstance();
202242
* app.Quit(0); // Quit with success code
@@ -404,6 +444,10 @@ class Application : public EventEmitter<ApplicationEvent> {
404444
*/
405445
std::vector<std::shared_ptr<Window>> GetAllWindows() const;
406446

447+
protected:
448+
void StartEventListening() override;
449+
void StopEventListening() override;
450+
407451
private:
408452
/**
409453
* @brief Private constructor to enforce singleton pattern
@@ -413,6 +457,15 @@ class Application : public EventEmitter<ApplicationEvent> {
413457
*/
414458
Application();
415459

460+
friend class detail::ApplicationQuitDispatch;
461+
void RequestQuit(int exit_code,
462+
std::function<void(int)> stop_loop = nullptr,
463+
std::shared_ptr<void> loop_owner = nullptr,
464+
std::function<void(bool)> native_confirmation = nullptr);
465+
void PerformQuit(int exit_code);
466+
void InvalidateQuitRequest();
467+
std::shared_ptr<detail::ApplicationQuitState> quit_state_;
468+
416469
// Prevent copy construction and assignment to maintain singleton property
417470
Application(const Application&) = delete;
418471
Application& operator=(const Application&) = delete;
@@ -439,8 +492,6 @@ class Application : public EventEmitter<ApplicationEvent> {
439492
* @brief Primary application window
440493
*/
441494
std::shared_ptr<Window> primary_window_;
442-
443-
private:
444495
};
445496

446497
/**

‎src/application_quit_dispatch.h‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#pragma once
2+
3+
// Private binding/host protocol; not part of API_HEADERS or the umbrella.
4+
#include <functional>
5+
#include <memory>
6+
7+
namespace nativeapi {
8+
class EventRequest;
9+
}
10+
11+
namespace nativeapi::detail {
12+
13+
class ApplicationQuitDispatch {
14+
public:
15+
// A binding owns a non-blocking loop. Confirm using the same application
16+
// request as Quit(), then finish that loop rather than the host's process.
17+
static void RequestForLoop(int exit_code,
18+
std::function<void(int)> stop_loop,
19+
std::shared_ptr<void> loop_owner = nullptr);
20+
21+
// Optional ownership fence for a cancellable binding loop. The binding
22+
// alone retains loop_owner; queued requests/completions retain it weakly.
23+
// Cancel only the matching attempt, even if a new loop has already started.
24+
static void CancelForLoop(std::weak_ptr<void> loop_owner);
25+
26+
// Native UI producer and host confirmation continuation. These calls must
27+
// run on UI; the host retains the slot until its async decision completes.
28+
static std::shared_ptr<EventRequest> CurrentRequest();
29+
static void RequestFromNative(std::function<void(bool)> confirmation);
30+
static void HoldForHost(const std::shared_ptr<EventRequest>& request);
31+
static void ReleaseFromHost(const std::shared_ptr<EventRequest>& request);
32+
static void NotifyRequired();
33+
static int ExitCode();
34+
};
35+
36+
} // namespace nativeapi::detail

0 commit comments

Comments
 (0)