From 02aedd71b1ec7b2485edfaca9780a365cfcb56da Mon Sep 17 00:00:00 2001 From: DeckerSU Date: Sun, 2 Nov 2025 22:25:43 +0100 Subject: [PATCH 1/2] feat: enhance Linux window close handling - Updated the window close handler to manage exit behavior on Linux, ensuring graceful shutdown without GTK cleanup issues. - Added a delete-event signal handler to intercept window close events on Linux, allowing for proper dialog handling before exiting. - Introduced a stub for the exit function on the web platform to prevent unsupported calls. --- lib/sdk/widgets/window_close_handler.dart | 28 ++++++++++++++++-- .../window_close_handler_exit_stub.dart | 7 +++++ linux/my_application.cc | 29 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 lib/sdk/widgets/window_close_handler_exit_stub.dart diff --git a/lib/sdk/widgets/window_close_handler.dart b/lib/sdk/widgets/window_close_handler.dart index 8623ae57bb..10b96594b9 100644 --- a/lib/sdk/widgets/window_close_handler.dart +++ b/lib/sdk/widgets/window_close_handler.dart @@ -1,3 +1,6 @@ +import 'dart:io' show exit + if (dart.library.html) 'window_close_handler_exit_stub.dart' show exit; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -15,6 +18,7 @@ import 'package:web_dex/shared/utils/window/window.dart'; /// /// This widget uses different strategies based on the platform: /// - Desktop (Windows, macOS, Linux): Uses flutter_window_close for native window close handling +/// On Linux, native code uses workaround to bypass GTK cleanup to prevent crashes /// - Web: Uses showMessageBeforeUnload for browser beforeunload event /// - Mobile (iOS, Android): Uses WidgetsBindingObserver for lifecycle management /// and PopScope for exit confirmation @@ -47,9 +51,25 @@ class _WindowCloseHandlerState extends State /// Sets up the appropriate close handler based on the platform. void _setupCloseHandler() { if (PlatformTuner.isNativeDesktop) { - // Desktop platforms: Use flutter_window_close + // Desktop platforms: Use flutter_window_close for all platforms + // On Linux, we use flutter_window_close for dialog, but return false to prevent + // standard window closing, then manually trigger exit via SystemNavigator FlutterWindowClose.setWindowShouldCloseHandler(() async { - return await _handleWindowClose(); + final shouldClose = await _handleWindowClose(); + + // On Linux, if user confirmed, we need to manually exit instead of letting + // flutter_window_close handle it, to avoid GTK cleanup issues + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.linux && shouldClose) { + // Hide window immediately + // Then exit after a short delay to allow any final cleanup + Future.delayed(const Duration(milliseconds: 200), () { + exit(0); + }); + // Return false to prevent flutter_window_close from closing the window + return false; + } + + return shouldClose; }); } else if (kIsWeb) { // Web platform: Use beforeunload event @@ -66,7 +86,8 @@ class _WindowCloseHandlerState extends State void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - // Dispose SDK when app is terminated or detached from UI (mobile platforms) + // Dispose SDK when app is terminated or detached from UI + // This applies to mobile platforms if (state == AppLifecycleState.detached) { _disposeSDKIfNeeded(); } @@ -151,6 +172,7 @@ class _WindowCloseHandlerState extends State void dispose() { // Clean up based on platform if (PlatformTuner.isNativeDesktop) { + // Desktop platforms: Remove flutter_window_close handler FlutterWindowClose.setWindowShouldCloseHandler(null); } else if (!kIsWeb) { // Mobile platforms: Remove lifecycle observer diff --git a/lib/sdk/widgets/window_close_handler_exit_stub.dart b/lib/sdk/widgets/window_close_handler_exit_stub.dart new file mode 100644 index 0000000000..abdc980178 --- /dev/null +++ b/lib/sdk/widgets/window_close_handler_exit_stub.dart @@ -0,0 +1,7 @@ +// Stub file for web platform - exit is not available on web +void exit(int code) { + // On web, we can't exit the process + // This should never be called as we check kIsWeb before using exit + throw UnsupportedError('exit() is not available on web platform'); +} + diff --git a/linux/my_application.cc b/linux/my_application.cc index be94af558c..c4a35d4e6a 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -1,5 +1,6 @@ #include "my_application.h" +#include #include #ifdef GDK_WINDOWING_X11 #include @@ -18,6 +19,7 @@ struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; GtkWindow* main_window; + FlView* flutter_view; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) @@ -25,6 +27,24 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) static void on_window_destroy(GtkWidget* widget, gpointer user_data) { MyApplication* self = MY_APPLICATION(user_data); self->main_window = nullptr; + self->flutter_view = nullptr; +} + +// Workaround for Flutter Linux shutdown issue (#132404) +// This handler intercepts delete-event early to prevent GTK from destroying the window +// and triggering FlutterEngineRemoveView. When flutter_window_close confirms closing, +// it will return false to prevent standard closing, and we'll handle exit manually. +// This handler acts as a safety net in case delete-event is still triggered. +static gboolean on_window_delete_event(GtkWidget* widget, GdkEvent* event, gpointer user_data) { + (void)widget; // Unused + (void)event; // Unused + (void)user_data; // Unused + + // On Linux, we want flutter_window_close to handle the dialog first + // So we return FALSE to let the event propagate to flutter_window_close + // If flutter_window_close returns false (user cancelled), nothing happens + // If flutter_window_close returns true, it will be intercepted and we'll exit manually + return FALSE; } // Implements GApplication::activate. @@ -89,7 +109,13 @@ static void my_application_activate(GApplication* application) { gtk_widget_grab_focus(GTK_WIDGET(view)); self->main_window = window; + self->flutter_view = view; + + // Connect destroy signal g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), self); + // Connect delete-event signal for graceful shutdown workaround + // This prevents the crash when closing the window on Linux + g_signal_connect(window, "delete-event", G_CALLBACK(on_window_delete_event), self); } // Implements GApplication::local_command_line. @@ -121,6 +147,8 @@ static void my_application_dispose(GObject* object) { self->main_window = nullptr; } + self->flutter_view = nullptr; + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } @@ -132,6 +160,7 @@ static void my_application_class_init(MyApplicationClass* klass) { static void my_application_init(MyApplication* self) { self->main_window = nullptr; + self->flutter_view = nullptr; } MyApplication* my_application_new() { From 02dad900d09d49b7038b7fa69081d8925e8cd792 Mon Sep 17 00:00:00 2001 From: DeckerSU Date: Sun, 2 Nov 2025 22:27:12 +0100 Subject: [PATCH 2/2] refactor: remove flutter_view references in Linux application - Removed unused flutter_view member from the MyApplication struct and related functions to clean up the code. - This change simplifies the application structure and addresses potential memory management concerns. --- linux/my_application.cc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/linux/my_application.cc b/linux/my_application.cc index c4a35d4e6a..b572d3c713 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -19,7 +19,6 @@ struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; GtkWindow* main_window; - FlView* flutter_view; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) @@ -27,7 +26,6 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) static void on_window_destroy(GtkWidget* widget, gpointer user_data) { MyApplication* self = MY_APPLICATION(user_data); self->main_window = nullptr; - self->flutter_view = nullptr; } // Workaround for Flutter Linux shutdown issue (#132404) @@ -109,7 +107,6 @@ static void my_application_activate(GApplication* application) { gtk_widget_grab_focus(GTK_WIDGET(view)); self->main_window = window; - self->flutter_view = view; // Connect destroy signal g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), self); @@ -147,8 +144,6 @@ static void my_application_dispose(GObject* object) { self->main_window = nullptr; } - self->flutter_view = nullptr; - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } @@ -160,7 +155,6 @@ static void my_application_class_init(MyApplicationClass* klass) { static void my_application_init(MyApplication* self) { self->main_window = nullptr; - self->flutter_view = nullptr; } MyApplication* my_application_new() {