Skip to content

Commit 227016d

Browse files
feat: add SetWillCloseHook to WindowManager
Adds a WindowWillCloseHook following the same pattern as the existing WindowWillShowHook and WindowWillHideHook. This allows FFI consumers (notably Flutter apps via nativeapi-flutter) to intercept window close and decide whether to proceed — needed for "unsaved changes" dialogs. Changes per layer: - window_manager.h: WindowWillCloseHook typedef, SetWillCloseHook, HasWillCloseHook, HandleWillClose, CallOriginalClose declarations. - macOS: swizzles performClose: on NSWindow (same pattern as makeKeyAndOrderFront: and orderOut:). CallOriginalClose calls the swizzled original. - Linux: adds a delete-event emission hook alongside show/hide. CallOriginalClose destroys the toplevel GtkWidget. - Windows: stores the hook; CallOriginalClose posts WM_CLOSE. Full WM_CLOSE interception (WH_CBT or window subclassing) is left as a follow-up — noted with a ponytail: comment. - C API: native_window_manager_set_will_close_hook, has_will_close_hook, handle_will_close, call_original_close. - Tests: window_manager_hook_test — verifies set/clear, dispatch, no-hook safety, and coexistence with show/hide hooks. All 6 tests pass on macOS. Build verified on macOS (CMake + make).
1 parent fb69854 commit 227016d

8 files changed

Lines changed: 413 additions & 6 deletions

File tree

src/capi/window_manager_c.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ void native_window_manager_set_will_hide_hook(native_window_manager_set_will_hid
8686
}
8787
}
8888

89+
void native_window_manager_set_will_close_hook(native_window_manager_set_will_close_hook_callback_t hook, void* hook_user_data) {
90+
try {
91+
std::optional<std::function<void(unsigned int)>> hook_cpp;
92+
if (hook) {
93+
hook_cpp = [hook, hook_user_data](unsigned int arg0) { hook(arg0, hook_user_data); };
94+
}
95+
nativeapi::WindowManager::GetInstance().SetWillCloseHook(hook_cpp);
96+
return;
97+
} catch (...) {
98+
fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_set_will_close_hook");
99+
return;
100+
}
101+
}
102+
89103
bool native_window_manager_has_will_show_hook(void) {
90104
try {
91105
return nativeapi::WindowManager::GetInstance().HasWillShowHook();
@@ -104,6 +118,15 @@ bool native_window_manager_has_will_hide_hook(void) {
104118
}
105119
}
106120

121+
bool native_window_manager_has_will_close_hook(void) {
122+
try {
123+
return nativeapi::WindowManager::GetInstance().HasWillCloseHook();
124+
} catch (...) {
125+
fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_has_will_close_hook");
126+
return false;
127+
}
128+
}
129+
107130
void native_window_manager_handle_will_show(native_window_id_t id) {
108131
try {
109132
nativeapi::WindowManager::GetInstance().HandleWillShow(id);
@@ -124,6 +147,16 @@ void native_window_manager_handle_will_hide(native_window_id_t id) {
124147
}
125148
}
126149

150+
void native_window_manager_handle_will_close(native_window_id_t id) {
151+
try {
152+
nativeapi::WindowManager::GetInstance().HandleWillClose(id);
153+
return;
154+
} catch (...) {
155+
fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_handle_will_close");
156+
return;
157+
}
158+
}
159+
127160
bool native_window_manager_call_original_show(native_window_id_t id) {
128161
try {
129162
return nativeapi::WindowManager::GetInstance().CallOriginalShow(id);
@@ -142,6 +175,15 @@ bool native_window_manager_call_original_hide(native_window_id_t id) {
142175
}
143176
}
144177

178+
bool native_window_manager_call_original_close(native_window_id_t id) {
179+
try {
180+
return nativeapi::WindowManager::GetInstance().CallOriginalClose(id);
181+
} catch (...) {
182+
fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_call_original_close");
183+
return false;
184+
}
185+
}
186+
145187
native_listener_id_t native_window_manager_add_listener(native_window_event_callback_t callback, void* user_data) {
146188
if (!callback) {
147189
return 0;

src/capi/window_manager_c.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ typedef void (*native_window_manager_set_will_show_hook_callback_t)(unsigned int
2323

2424
typedef void (*native_window_manager_set_will_hide_hook_callback_t)(unsigned int arg0, void* user_data);
2525

26+
typedef void (*native_window_manager_set_will_close_hook_callback_t)(unsigned int arg0, void* user_data);
27+
2628
/// Caller owns the returned handle; release it with native_window_free().
2729
FFI_PLUGIN_EXPORT
2830
native_window_t native_window_manager_get(native_window_id_t id);
@@ -40,24 +42,36 @@ void native_window_manager_set_will_show_hook(native_window_manager_set_will_sho
4042
FFI_PLUGIN_EXPORT
4143
void native_window_manager_set_will_hide_hook(native_window_manager_set_will_hide_hook_callback_t hook, void* hook_user_data);
4244

45+
FFI_PLUGIN_EXPORT
46+
void native_window_manager_set_will_close_hook(native_window_manager_set_will_close_hook_callback_t hook, void* hook_user_data);
47+
4348
FFI_PLUGIN_EXPORT
4449
bool native_window_manager_has_will_show_hook(void);
4550

4651
FFI_PLUGIN_EXPORT
4752
bool native_window_manager_has_will_hide_hook(void);
4853

54+
FFI_PLUGIN_EXPORT
55+
bool native_window_manager_has_will_close_hook(void);
56+
4957
FFI_PLUGIN_EXPORT
5058
void native_window_manager_handle_will_show(native_window_id_t id);
5159

5260
FFI_PLUGIN_EXPORT
5361
void native_window_manager_handle_will_hide(native_window_id_t id);
5462

63+
FFI_PLUGIN_EXPORT
64+
void native_window_manager_handle_will_close(native_window_id_t id);
65+
5566
FFI_PLUGIN_EXPORT
5667
bool native_window_manager_call_original_show(native_window_id_t id);
5768

5869
FFI_PLUGIN_EXPORT
5970
bool native_window_manager_call_original_hide(native_window_id_t id);
6071

72+
FFI_PLUGIN_EXPORT
73+
bool native_window_manager_call_original_close(native_window_id_t id);
74+
6175
/// Registers @p callback for every WindowEvent this WindowManager emits.
6276
/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure.
6377
FFI_PLUGIN_EXPORT

src/platform/linux/window_manager_linux.cpp

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,27 @@ static gboolean on_hide_emission_hook(GSignalInvocationHint* ihint,
122122
return TRUE; // Continue emission
123123
}
124124

125+
// Signal emission hook for delete-event signal
126+
static gboolean on_delete_event_emission_hook(GSignalInvocationHint* ihint,
127+
guint n_param_values,
128+
const GValue* param_values,
129+
gpointer data) {
130+
(void)ihint;
131+
(void)n_param_values;
132+
(void)data;
133+
134+
GtkWidget* widget = GTK_WIDGET(g_value_get_object(&param_values[0]));
135+
if (widget && GTK_IS_WINDOW(widget)) {
136+
GdkWindow* gdk_window = gtk_widget_get_window(widget);
137+
if (gdk_window) {
138+
WindowId id = GetOrCreateWindowId(gdk_window);
139+
WindowManager::GetInstance().HandleWillClose(id);
140+
}
141+
}
142+
143+
return TRUE; // Continue emission
144+
}
145+
125146
// GTK signal callbacks to invoke hooks (used as fallback)
126147
static gboolean OnGtkMapEvent(GtkWidget* widget, GdkEvent* event, gpointer user_data) {
127148
(void)event;
@@ -181,9 +202,10 @@ static void InstallGlobalSwizzling() {
181202
return;
182203
}
183204

184-
// Get the show and hide signal IDs for GtkWidget
205+
// Get the show, hide, and delete-event signal IDs for GtkWidget
185206
guint show_signal_id = g_signal_lookup("show", GTK_TYPE_WIDGET);
186207
guint hide_signal_id = g_signal_lookup("hide", GTK_TYPE_WIDGET);
208+
guint delete_event_signal_id = g_signal_lookup("delete-event", GTK_TYPE_WIDGET);
187209

188210
if (show_signal_id != 0) {
189211
// Add emission hook for show signal
@@ -195,6 +217,11 @@ static void InstallGlobalSwizzling() {
195217
g_signal_add_emission_hook(hide_signal_id, 0, on_hide_emission_hook, nullptr, nullptr);
196218
}
197219

220+
if (delete_event_signal_id != 0) {
221+
// Add emission hook for delete-event signal
222+
g_signal_add_emission_hook(delete_event_signal_id, 0, on_delete_event_emission_hook, nullptr, nullptr);
223+
}
224+
198225
g_swizzle_installed = true;
199226
}
200227

@@ -228,9 +255,10 @@ class WindowManager::Impl {
228255

229256
private:
230257
WindowManager* manager_;
231-
// Optional pre-show/hide hooks
258+
// Optional pre-show/hide/close hooks
232259
std::optional<WindowManager::WindowWillShowHook> will_show_hook_;
233260
std::optional<WindowManager::WindowWillHideHook> will_hide_hook_;
261+
std::optional<WindowManager::WindowWillCloseHook> will_close_hook_;
234262

235263
friend class WindowManager;
236264
};
@@ -372,6 +400,14 @@ void WindowManager::SetWillHideHook(std::optional<WindowWillHideHook> hook) {
372400
}
373401
}
374402

403+
void WindowManager::SetWillCloseHook(std::optional<WindowWillCloseHook> hook) {
404+
pimpl_->will_close_hook_ = std::move(hook);
405+
if (pimpl_->will_close_hook_) {
406+
// Ensure global swizzling is installed when hook is set
407+
InstallGlobalSwizzling();
408+
}
409+
}
410+
375411
bool WindowManager::HasWillShowHook() const {
376412
return pimpl_->will_show_hook_.has_value();
377413
}
@@ -380,6 +416,10 @@ bool WindowManager::HasWillHideHook() const {
380416
return pimpl_->will_hide_hook_.has_value();
381417
}
382418

419+
bool WindowManager::HasWillCloseHook() const {
420+
return pimpl_->will_close_hook_.has_value();
421+
}
422+
383423
void WindowManager::HandleWillShow(WindowId id) {
384424
if (pimpl_->will_show_hook_) {
385425
(*pimpl_->will_show_hook_)(id);
@@ -392,6 +432,12 @@ void WindowManager::HandleWillHide(WindowId id) {
392432
}
393433
}
394434

435+
void WindowManager::HandleWillClose(WindowId id) {
436+
if (pimpl_->will_close_hook_) {
437+
(*pimpl_->will_close_hook_)(id);
438+
}
439+
}
440+
395441
bool WindowManager::CallOriginalShow(WindowId id) {
396442
GdkWindow* gdk_window = FindGdkWindowById(id);
397443
if (!gdk_window) {
@@ -414,6 +460,21 @@ bool WindowManager::CallOriginalHide(WindowId id) {
414460
return true;
415461
}
416462

463+
bool WindowManager::CallOriginalClose(WindowId id) {
464+
GdkWindow* gdk_window = FindGdkWindowById(id);
465+
if (!gdk_window) {
466+
return false;
467+
}
468+
469+
// On Linux, destroy the underlying GtkWidget to close the window
470+
GtkWidget* widget = gtk_widget_get_toplevel(GTK_WIDGET(gdk_window));
471+
if (widget && GTK_IS_WINDOW(widget)) {
472+
gtk_widget_destroy(widget);
473+
return true;
474+
}
475+
return false;
476+
}
477+
417478
void WindowManager::StartEventListening() {
418479
pimpl_->StartEventListening();
419480
}

src/platform/macos/window_manager_macos.mm

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@
2929
WindowManager* manager_;
3030
NativeAPIWindowManagerDelegate* delegate_;
3131

32-
// Optional pre-show/hide hooks
32+
// Optional pre-show/hide/close hooks
3333
std::optional<WindowManager::WindowWillShowHook> will_show_hook_;
3434
std::optional<WindowManager::WindowWillHideHook> will_hide_hook_;
35+
std::optional<WindowManager::WindowWillCloseHook> will_close_hook_;
3536

3637
friend class WindowManager;
3738
};
@@ -44,6 +45,7 @@
4445
@interface NSWindow (NativeAPISwizzle)
4546
- (void)na_swizzled_makeKeyAndOrderFront:(id)sender;
4647
- (void)na_swizzled_orderOut:(id)sender;
48+
- (void)na_swizzled_performClose:(id)sender;
4749
@end
4850

4951
@implementation NSWindow (NativeAPISwizzle)
@@ -79,6 +81,22 @@ - (void)na_swizzled_orderOut:(id)sender {
7981
[self na_swizzled_orderOut:sender];
8082
}
8183

84+
- (void)na_swizzled_performClose:(id)sender {
85+
// Resolve window id and handle hook if present
86+
if (nativeapi::WindowManager::GetInstance().HasWillCloseHook()) {
87+
auto windows = nativeapi::WindowManager::GetInstance().GetAll();
88+
for (const auto& window : windows) {
89+
if (window->GetNativeObject() == (__bridge void*)self) {
90+
nativeapi::WindowManager::GetInstance().HandleWillClose(window->GetId());
91+
// Hook handles all logic; never call original here
92+
return;
93+
}
94+
}
95+
}
96+
// No window found in registry, call original implementation (swapped)
97+
[self na_swizzled_performClose:sender];
98+
}
99+
82100
@end
83101

84102
static void NativeAPIInstallNSWindowWillShowSwizzleOnce() {
@@ -109,6 +127,20 @@ static void NativeAPIInstallNSWindowWillHideSwizzleOnce() {
109127
});
110128
}
111129

130+
static void NativeAPIInstallNSWindowWillCloseSwizzleOnce() {
131+
static dispatch_once_t onceTokenClose;
132+
dispatch_once(&onceTokenClose, ^{
133+
Class cls = [NSWindow class];
134+
SEL originalSel = @selector(performClose:);
135+
SEL swizzledSel = @selector(na_swizzled_performClose:);
136+
Method original = class_getInstanceMethod(cls, originalSel);
137+
Method swizzled = class_getInstanceMethod(cls, swizzledSel);
138+
if (original && swizzled) {
139+
method_exchangeImplementations(original, swizzled);
140+
}
141+
});
142+
}
143+
112144
// Objective-C delegate class to handle NSWindow notifications
113145
@interface NativeAPIWindowManagerDelegate : NSObject
114146
@property(nonatomic, assign) void* impl; // Use void* instead of private class
@@ -341,6 +373,13 @@ - (void)windowWillClose:(NSNotification*)notification {
341373
}
342374
}
343375

376+
void WindowManager::SetWillCloseHook(std::optional<WindowWillCloseHook> hook) {
377+
pimpl_->will_close_hook_ = std::move(hook);
378+
if (pimpl_->will_close_hook_) {
379+
NativeAPIInstallNSWindowWillCloseSwizzleOnce();
380+
}
381+
}
382+
344383
bool WindowManager::HasWillShowHook() const {
345384
return pimpl_->will_show_hook_.has_value();
346385
}
@@ -349,6 +388,10 @@ - (void)windowWillClose:(NSNotification*)notification {
349388
return pimpl_->will_hide_hook_.has_value();
350389
}
351390

391+
bool WindowManager::HasWillCloseHook() const {
392+
return pimpl_->will_close_hook_.has_value();
393+
}
394+
352395
void WindowManager::HandleWillShow(WindowId id) {
353396
if (pimpl_->will_show_hook_) {
354397
(*pimpl_->will_show_hook_)(id);
@@ -361,6 +404,12 @@ - (void)windowWillClose:(NSNotification*)notification {
361404
}
362405
}
363406

407+
void WindowManager::HandleWillClose(WindowId id) {
408+
if (pimpl_->will_close_hook_) {
409+
(*pimpl_->will_close_hook_)(id);
410+
}
411+
}
412+
364413
bool WindowManager::CallOriginalShow(WindowId id) {
365414
auto window = Get(id);
366415
if (!window) {
@@ -389,6 +438,20 @@ - (void)windowWillClose:(NSNotification*)notification {
389438
return true;
390439
}
391440

441+
bool WindowManager::CallOriginalClose(WindowId id) {
442+
auto window = Get(id);
443+
if (!window) {
444+
return false;
445+
}
446+
void* native = window->GetNativeObject();
447+
if (!native) {
448+
return false;
449+
}
450+
NSWindow* ns_window = (__bridge NSWindow*)native;
451+
[ns_window na_swizzled_performClose:nil];
452+
return true;
453+
}
454+
392455
void WindowManager::StartEventListening() {
393456
pimpl_->StartEventListening();
394457
}

0 commit comments

Comments
 (0)