Repository: godotengine/godot (language: cpp)

Issue:
Closing game running in editor no longer sends NotificationWMCloseRequest - v4.4Beta1
### Tested versions

- Reproducible in 4.4Beta1 .NET
- Not reproducible in 4.3 .NET

### System information

Windows 11 - Godot_v4.4-beta1_mono_win64

### Issue description

When closing the game while running it via the editor, a NotificationWMCloseRequest is no longer sent in 4.4.

### Steps to reproduce

See minimal reproduction project

### Minimal reproduction project (MRP)

Attached is a simple 'save on exit' project that will demonstrate the issue.  When running the game in the editor interface in 4.3, the entire game state will be saved upon closing (and loaded when you next restart the game).  When running the game in the editor interface in 4.4, the game will no longer save because a NotificationWMCloseRequest is no longer sent when the game is closing.

Note: The `StageManager` class is responsible for receiving the NotificationWMCloseRequest in this project.

[BrackeysFirstSavedGame.zip](https://github.com/user-attachments/files/18483606/BrackeysFirstSavedGame.zip)


Discussion / hints:
Seems like another issue with embedded mode.

It is still work if you run the project in non-embedded mode. `NOTIFICATION_WM_CLOSE_REQUEST` is sent when `close` button in the window title is pressed, not when the game exits. It's not emitted when closing by any other means (like `get_tree().quit()` for example).

When running in an embedded / floating mode, the actual window title bar is not visible, and closing the floating window stops the game instantly (same as pressing the stop button in the editor).
I disabled embedded-mode and, you are correct, everything appears to be working again.

I think it would be preferable as a future enhancement to cause quitting the game while in embedded mode or via clicking the `Stop Running Project` button to trigger a `NOTIFICATION_WM_CLOSE_REQUEST`, but it can maybe be argued this is not a regression.

Fix patch:
diff --git a/editor/plugins/game_view_plugin.cpp b/editor/plugins/game_view_plugin.cpp
index b1f7a8498a3a..f0f1a915f011 100644
--- a/editor/plugins/game_view_plugin.cpp
+++ b/editor/plugins/game_view_plugin.cpp
@@ -652,14 +652,20 @@ void GameView::_update_arguments_for_instance(int p_idx, List<String> &r_argumen
 	r_arguments.insert_after(N, itos(rect.size.x) + "x" + itos(rect.size.y));
 }
 
-void GameView::_window_before_closing() {
+void GameView::_window_close_request() {
 	// Before the parent window closed, we close the embedded game. That prevents
 	// the embedded game to be seen without a parent window for a fraction of second.
 	if (EditorRunBar::get_singleton()->is_playing() && (embedded_process->is_embedding_completed() || embedded_process->is_embedding_in_progress())) {
+		// Try to gracefully close the window. That way, the NOTIFICATION_WM_CLOSE_REQUEST
+		// notification should be propagated in the game process.
 		embedded_process->reset();
-		// Call deferred to prevent the _stop_pressed callback to be executed before the wrapper window
-		// actually closes.
-		callable_mp(EditorRunBar::get_singleton(), &EditorRunBar::stop_playing).call_deferred();
+
+		// When the embedding is not complete, we need to kill the process.
+		if (embedded_process->is_embedding_in_progress()) {
+			// Call deferred to prevent the _stop_pressed callback to be executed before the wrapper window
+			// actually closes.
+			callable_mp(EditorRunBar::get_singleton(), &EditorRunBar::stop_playing).call_deferred();
+		}
 	}
 }
 
@@ -829,7 +835,8 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	p_debugger->connect("session_started", callable_mp(this, &GameView::_sessions_changed));
 	p_debugger->connect("session_stopped", callable_mp(this, &GameView::_sessions_changed));
 
-	p_wrapper->connect("window_before_closing", callable_mp(this, &GameView::_window_before_closing));
+	p_wrapper->set_override_close_request(true);
+	p_wrapper->connect("window_close_requested", callable_mp(this, &GameView::_window_close_request));
 	p_wrapper->connect("window_size_changed", callable_mp(this, &GameView::_update_floating_window_settings));
 }
 
diff --git a/editor/plugins/game_view_plugin.h b/editor/plugins/game_view_plugin.h
index 959da73dcde8..273652dae821 100644
--- a/editor/plugins/game_view_plugin.h
+++ b/editor/plugins/game_view_plugin.h
@@ -162,7 +162,7 @@ class GameView : public VBoxContainer {
 	void _camera_override_button_toggled(bool p_pressed);
 	void _camera_override_menu_id_pressed(int p_id);
 
-	void _window_before_closing();
+	void _window_close_request();
 	void _update_floating_window_settings();
 	void _attach_script_debugger();
 	void _detach_script_debugger();
diff --git a/editor/window_wrapper.cpp b/editor/window_wrapper.cpp
index 37a3efc91e90..22e2fda68567 100644
--- a/editor/window_wrapper.cpp
+++ b/editor/window_wrapper.cpp
@@ -101,12 +101,6 @@ void WindowWrapper::_set_window_enabled_with_rect(bool p_visible, const Rect2 p_
 
 	Node *parent = _get_wrapped_control_parent();
 
-	// In the GameView plugin, we need to the the signal before the window is actually closed
-	// to prevent the embedded game to be seen the parent window for a fraction of a second.
-	if (!p_visible) {
-		emit_signal("window_before_closing");
-	}
-
 	if (wrapped_control->get_parent() != parent) {
 		// Move the control to the window.
 		wrapped_control->reparent(parent, false);
@@ -120,7 +114,7 @@ void WindowWrapper::_set_window_enabled_with_rect(bool p_visible, const Rect2 p_
 	}
 
 	window->set_visible(p_visible);
-	if (!p_visible) {
+	if (!p_visible && !override_close_request) {
 		emit_signal("window_close_requested");
 	}
 	emit_signal("window_visibility_changed", p_visible);
@@ -141,10 +135,17 @@ void WindowWrapper::_window_size_changed() {
 	emit_signal(SNAME("window_size_changed"));
 }
 
+void WindowWrapper::_window_close_request() {
+	if (override_close_request) {
+		emit_signal("window_close_requested");
+	} else {
+		set_window_enabled(false);
+	}
+}
+
 void WindowWrapper::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("window_visibility_changed", PropertyInfo(Variant::BOOL, "visible")));
 	ADD_SIGNAL(MethodInfo("window_close_requested"));
-	ADD_SIGNAL(MethodInfo("window_before_closing"));
 	ADD_SIGNAL(MethodInfo("window_size_changed"));
 }
 
@@ -330,6 +331,10 @@ void WindowWrapper::grab_window_focus() {
 	}
 }
 
+void WindowWrapper::set_override_close_request(bool p_enabled) {
+	override_close_request = p_enabled;
+}
+
 WindowWrapper::WindowWrapper() {
 	if (!EditorNode::get_singleton()->is_multi_window_enabled()) {
 		return;
@@ -342,7 +347,7 @@ WindowWrapper::WindowWrapper() {
 	add_child(window);
 	window->hide();
 
-	window->connect("close_requested", callable_mp(this, &WindowWrapper::set_window_enabled).bind(false));
+	window->connect("close_requested", callable_mp(this, &WindowWrapper::_window_close_request));
 	window->connect("size_changed", callable_mp(this, &WindowWrapper::_window_size_changed));
 
 	ShortcutBin *capturer = memnew(ShortcutBin);
diff --git a/editor/window_wrapper.h b/editor/window_wrapper.h
index 3f4e9385861c..e33c8c179d53 100644
--- a/editor/window_wrapper.h
+++ b/editor/window_wrapper.h
@@ -50,12 +50,15 @@ class WindowWrapper : public MarginContainer {
 
 	Ref<Shortcut> enable_shortcut;
 
+	bool override_close_request = false;
+
 	Rect2 _get_default_window_rect() const;
 	Node *_get_wrapped_control_parent() const;
 
 	void _set_window_enabled_with_rect(bool p_visible, const Rect2 p_rect);
 	void _set_window_rect(const Rect2 p_rect);
 	void _window_size_changed();
+	void _window_close_request();
 
 protected:
 	static void _bind_methods();
@@ -84,6 +87,8 @@ class WindowWrapper : public MarginContainer {
 	void set_margins_enabled(bool p_enabled);
 	void grab_window_focus();
 
+	void set_override_close_request(bool p_enabled);
+
 	WindowWrapper();
 	~WindowWrapper();
 };
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index b17f72dc5c04..b1945eb47a2e 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -5849,6 +5849,24 @@ Error DisplayServerX11::remove_embedded_process(OS::ProcessID p_pid) {
 	}
 
 	EmbeddedProcessData *ep = embedded_processes.get(p_pid);
+
+	// Handle bad window errors silently because just in case the embedded window was closed.
+	int (*oldHandler)(Display *, XErrorEvent *) = XSetErrorHandler(&bad_window_error_handler);
+
+	// Send the message to gracefully close the window.
+	XEvent ev;
+	memset(&ev, 0, sizeof(ev));
+	ev.xclient.type = ClientMessage;
+	ev.xclient.window = ep->process_window;
+	ev.xclient.message_type = XInternAtom(x11_display, "WM_PROTOCOLS", True);
+	ev.xclient.format = 32;
+	ev.xclient.data.l[0] = XInternAtom(x11_display, "WM_DELETE_WINDOW", False);
+	ev.xclient.data.l[1] = CurrentTime;
+	XSendEvent(x11_display, ep->process_window, False, NoEventMask, &ev);
+
+	// Restore default error handler.
+	XSetErrorHandler(oldHandler);
+
 	embedded_processes.erase(p_pid);
 	memdelete(ep);
 
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index ac7fa8b3c69e..1d3a6f0ca4f2 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -2975,6 +2975,9 @@ Error DisplayServerWindows::remove_embedded_process(OS::ProcessID p_pid) {
 
 	EmbeddedProcessData *ep = embedded_processes.get(p_pid);
 
+	// Send a close message to gracefully close the process.
+	PostMessage(ep->window_handle, WM_CLOSE, 0, 0);
+
 	// This is a workaround to ensure the parent window correctly regains focus after the
 	// embedded window is closed. When the embedded window is closed while it has focus,
 	// the parent window (the editor) does not become active. It appears focused but is not truly activated.


Test patch: