diff --git a/lib/widgets/presentation/fullscreen_presenter.dart b/lib/widgets/presentation/fullscreen_presenter.dart index 2fdcbb2..5f8220f 100644 --- a/lib/widgets/presentation/fullscreen_presenter.dart +++ b/lib/widgets/presentation/fullscreen_presenter.dart @@ -42,8 +42,8 @@ class FullscreenPresenter extends StatefulWidget { }); /// Entry point used by the app: pick dual-screen mode when a second display is - /// available (macOS), otherwise the single-window presenter. Any failure to - /// open the second window falls back to single-window mode. + /// available on desktop, otherwise the single-window presenter. Any failure + /// to open the second window falls back to single-window mode. static Future present( BuildContext context, { required List slides, @@ -52,15 +52,21 @@ class FullscreenPresenter extends StatefulWidget { required int initialIndex, TlpLevel tlp = TlpLevel.none, }) async { - var dual = false; - if (Platform.isMacOS) { + var displayCount = 0; + if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { try { final displays = await screenRetriever.getAllDisplays(); - dual = displays.length >= 2; + displayCount = displays.length; } catch (_) { - dual = false; + displayCount = 0; } } + final dual = shouldUseDualScreen( + isMacOS: Platform.isMacOS, + isWindows: Platform.isWindows, + isLinux: Platform.isLinux, + displayCount: displayCount, + ); if (!context.mounted) return; if (dual) { await showDualScreen( @@ -204,6 +210,16 @@ class FullscreenPresenter extends StatefulWidget { State createState() => _FullscreenPresenterState(); } +@visibleForTesting +bool shouldUseDualScreen({ + required bool isMacOS, + required bool isWindows, + required bool isLinux, + required int displayCount, +}) { + return (isMacOS || isWindows || isLinux) && displayCount >= 2; +} + Future _wakeLockEnabled() async { try { return await WakelockPlus.enabled; diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 5283728..72a93b3 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -5,6 +5,7 @@ #include #endif +#include "desktop_multi_window/desktop_multi_window_plugin.h" #include "flutter/generated_plugin_registrant.h" struct _MyApplication { @@ -89,6 +90,8 @@ static void my_application_activate(GApplication* application) { gtk_widget_realize(GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + desktop_multi_window_plugin_set_window_created_callback( + [](FlPluginRegistry* registry) { fl_register_plugins(registry); }); gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/test/fullscreen_presenter_test.dart b/test/fullscreen_presenter_test.dart index f164670..bb039f3 100644 --- a/test/fullscreen_presenter_test.dart +++ b/test/fullscreen_presenter_test.dart @@ -24,6 +24,57 @@ void main() { Slide.create(SlideType.bullets).copyWith(title: 'Tweede', bullets: ['b']), ]; + test('dual-screen mode is available on every desktop platform', () { + expect( + shouldUseDualScreen( + isMacOS: true, + isWindows: false, + isLinux: false, + displayCount: 2, + ), + isTrue, + ); + expect( + shouldUseDualScreen( + isMacOS: false, + isWindows: true, + isLinux: false, + displayCount: 2, + ), + isTrue, + ); + expect( + shouldUseDualScreen( + isMacOS: false, + isWindows: false, + isLinux: true, + displayCount: 2, + ), + isTrue, + ); + }); + + test('dual-screen mode requires a desktop platform and two displays', () { + expect( + shouldUseDualScreen( + isMacOS: true, + isWindows: false, + isLinux: false, + displayCount: 1, + ), + isFalse, + ); + expect( + shouldUseDualScreen( + isMacOS: false, + isWindows: false, + isLinux: false, + displayCount: 2, + ), + isFalse, + ); + }); + testWidgets('starts in audience view without presenter chrome', ( tester, ) async { diff --git a/third_party/desktop_multi_window/linux/flutter_window.cc b/third_party/desktop_multi_window/linux/flutter_window.cc index 8a7a209..6755b29 100755 --- a/third_party/desktop_multi_window/linux/flutter_window.cc +++ b/third_party/desktop_multi_window/linux/flutter_window.cc @@ -1,7 +1,32 @@ #include "flutter_window.h" +#include #include +namespace { + +bool ReadExternalArgument(FlValue* arguments) { + if (arguments == nullptr || + fl_value_get_type(arguments) != FL_VALUE_TYPE_MAP) { + return true; + } + FlValue* external = fl_value_lookup_string(arguments, "external"); + if (external == nullptr || + fl_value_get_type(external) != FL_VALUE_TYPE_BOOL) { + return true; + } + return fl_value_get_bool(external); +} + +gboolean CloseWindowOnIdle(gpointer data) { + GtkWidget* window = GTK_WIDGET(data); + gtk_widget_destroy(window); + g_object_unref(window); + return G_SOURCE_REMOVE; +} + +} // namespace + FlutterWindow::FlutterWindow(const std::string& id, const std::string& argument, GtkWidget* window) @@ -42,6 +67,49 @@ void FlutterWindow::HandleWindowMethod(const gchar* method, } else if (strcmp(method, "window_hide") == 0) { Hide(); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); + } else if (strcmp(method, "window_close") == 0) { + response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); + if (window_) { + g_idle_add(CloseWindowOnIdle, g_object_ref(window_)); + } + } else if (strcmp(method, "window_coverScreen") == 0) { + if (!window_) { + response = FL_METHOD_RESPONSE( + fl_method_error_response_new("-1", "window is not available", + nullptr)); + } else { + GtkWindow* window = GTK_WINDOW(window_); + GdkScreen* screen = gtk_window_get_screen(window); + GdkWindow* gdk_window = gtk_widget_get_window(window_); + const gint monitor_count = gdk_screen_get_n_monitors(screen); + gint current_monitor = gdk_window + ? gdk_screen_get_monitor_at_window(screen, + gdk_window) + : gdk_screen_get_primary_monitor(screen); + if (current_monitor < 0) { + current_monitor = 0; + } + + gint target_monitor = current_monitor; + if (ReadExternalArgument(arguments) && monitor_count > 1) { + for (gint i = 0; i < monitor_count; ++i) { + if (i != current_monitor) { + target_monitor = i; + break; + } + } + } + + GdkRectangle bounds; + gdk_screen_get_monitor_geometry(screen, target_monitor, &bounds); + gtk_window_unfullscreen(window); + gtk_window_set_decorated(window, FALSE); + gtk_window_move(window, bounds.x, bounds.y); + gtk_window_resize(window, bounds.width, bounds.height); + gtk_window_fullscreen_on_monitor(window, screen, target_monitor); + gtk_widget_show(window_); + response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); + } } else { g_autofree gchar* error_msg = g_strdup_printf("unknown method: %s", method); response = FL_METHOD_RESPONSE( diff --git a/third_party/desktop_multi_window/windows/flutter_window_wrapper.h b/third_party/desktop_multi_window/windows/flutter_window_wrapper.h index a4281a9..1c21ccf 100644 --- a/third_party/desktop_multi_window/windows/flutter_window_wrapper.h +++ b/third_party/desktop_multi_window/windows/flutter_window_wrapper.h @@ -8,6 +8,42 @@ #include #include +namespace { + +struct MonitorSearch { + HMONITOR current = nullptr; + HMONITOR external = nullptr; + HMONITOR fallback = nullptr; +}; + +inline BOOL CALLBACK FindPresentationMonitor(HMONITOR monitor, + HDC, + LPRECT, + LPARAM data) { + auto* search = reinterpret_cast(data); + if (!search->fallback) { + search->fallback = monitor; + } + if (monitor != search->current && !search->external) { + search->external = monitor; + } + return TRUE; +} + +inline bool ReadExternalArgument(const flutter::EncodableMap* arguments) { + if (!arguments) { + return true; + } + const auto it = arguments->find(flutter::EncodableValue("external")); + if (it == arguments->end()) { + return true; + } + const auto* external = std::get_if(&it->second); + return external ? *external : true; +} + +} // namespace + class FlutterWindowWrapper { public: FlutterWindowWrapper(const std::string& window_id, @@ -51,6 +87,45 @@ class FlutterWindowWrapper { ::ShowWindow(hwnd_, SW_HIDE); } result->Success(); + } else if (method == "window_close") { + result->Success(); + if (hwnd_) { + ::PostMessage(hwnd_, WM_CLOSE, 0, 0); + } + } else if (method == "window_coverScreen") { + if (!hwnd_) { + result->Error("-1", "window is not available"); + return; + } + + MonitorSearch search; + search.current = ::MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONEAREST); + ::EnumDisplayMonitors( + nullptr, nullptr, FindPresentationMonitor, + reinterpret_cast(&search)); + + HMONITOR target = search.current; + if (ReadExternalArgument(arguments) && search.external) { + target = search.external; + } else if (!target) { + target = search.fallback; + } + + MONITORINFO monitor_info{sizeof(MONITORINFO)}; + if (!target || !::GetMonitorInfo(target, &monitor_info)) { + result->Error("-1", "unable to find a display"); + return; + } + + const RECT bounds = monitor_info.rcMonitor; + ::SetWindowLongPtr(hwnd_, GWL_STYLE, WS_POPUP | WS_VISIBLE); + ::SetWindowLongPtr(hwnd_, GWL_EXSTYLE, + ::GetWindowLongPtr(hwnd_, GWL_EXSTYLE) & + ~WS_EX_WINDOWEDGE); + ::SetWindowPos(hwnd_, HWND_TOP, bounds.left, bounds.top, + bounds.right - bounds.left, bounds.bottom - bounds.top, + SWP_FRAMECHANGED | SWP_SHOWWINDOW); + result->Success(); } else { result->Error("-1", "unknown method: " + method); } diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee30..0b4eecb 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -2,6 +2,7 @@ #include +#include "desktop_multi_window/desktop_multi_window_plugin.h" #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) @@ -25,6 +26,11 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + DesktopMultiWindowSetWindowCreatedCallback([](void* controller) { + auto* flutter_view_controller = + reinterpret_cast(controller); + RegisterPlugins(flutter_view_controller->engine()); + }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() {