Add dual-screen presenter mode (slide on beamer, notes on laptop)
When a second display is connected (macOS), presenting now opens a
borderless audience window on the beamer showing the slide, while the
main window shows the presenter view (current/next slide, speaker notes,
clock, controls) on the laptop. The two windows stay in sync over method
channels: navigation, blank screen, audio-complete and beamer clicks are
forwarded between them, and media plays only on the beamer to avoid
double audio. Falls back to the existing single-window presenter when
there is one display or the second window can't be created.
- Vendors a fork of desktop_multi_window in third_party/ that re-adds the
native macOS window geometry/fullscreen calls (coverScreen, setFrame,
close) the published 0.3.0 dropped; wired via a path dependency.
- Registers the app's plugins for sub-windows in MainFlutterWindow so
video/image rendering works on the beamer.
- Routes the multi_window dart entrypoint to a minimal AudienceWindowApp.
Compiles (flutter analyze + macOS debug build) and all tests pass;
runtime two-screen behaviour still needs verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:25:34 +02:00
|
|
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_WRAPPER_H_
|
|
|
|
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_WRAPPER_H_
|
|
|
|
|
|
|
|
|
|
#include <Windows.h>
|
|
|
|
|
#include <flutter/encodable_value.h>
|
|
|
|
|
#include <flutter/method_channel.h>
|
|
|
|
|
#include <flutter/method_result.h>
|
|
|
|
|
#include <memory>
|
|
|
|
|
#include <string>
|
|
|
|
|
|
2026-06-06 22:03:56 +02:00
|
|
|
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<MonitorSearch*>(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<bool>(&it->second);
|
|
|
|
|
return external ? *external : true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
Add dual-screen presenter mode (slide on beamer, notes on laptop)
When a second display is connected (macOS), presenting now opens a
borderless audience window on the beamer showing the slide, while the
main window shows the presenter view (current/next slide, speaker notes,
clock, controls) on the laptop. The two windows stay in sync over method
channels: navigation, blank screen, audio-complete and beamer clicks are
forwarded between them, and media plays only on the beamer to avoid
double audio. Falls back to the existing single-window presenter when
there is one display or the second window can't be created.
- Vendors a fork of desktop_multi_window in third_party/ that re-adds the
native macOS window geometry/fullscreen calls (coverScreen, setFrame,
close) the published 0.3.0 dropped; wired via a path dependency.
- Registers the app's plugins for sub-windows in MainFlutterWindow so
video/image rendering works on the beamer.
- Routes the multi_window dart entrypoint to a minimal AudienceWindowApp.
Compiles (flutter analyze + macOS debug build) and all tests pass;
runtime two-screen behaviour still needs verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:25:34 +02:00
|
|
|
class FlutterWindowWrapper {
|
|
|
|
|
public:
|
|
|
|
|
FlutterWindowWrapper(const std::string& window_id,
|
|
|
|
|
HWND hwnd,
|
|
|
|
|
const std::string& window_argument = "")
|
|
|
|
|
: window_id_(window_id), hwnd_(hwnd), window_argument_(window_argument) {}
|
|
|
|
|
|
|
|
|
|
~FlutterWindowWrapper() = default;
|
|
|
|
|
|
|
|
|
|
std::string GetWindowId() const { return window_id_; }
|
|
|
|
|
|
|
|
|
|
std::string GetWindowArgument() const { return window_argument_; }
|
|
|
|
|
|
|
|
|
|
HWND GetWindowHandle() { return hwnd_; }
|
|
|
|
|
|
|
|
|
|
void SetChannel(
|
|
|
|
|
std::shared_ptr<flutter::MethodChannel<flutter::EncodableValue>>
|
|
|
|
|
channel) {
|
|
|
|
|
channel_ = channel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void NotifyWindowEvent(const std::string& event,
|
|
|
|
|
const flutter::EncodableMap& data) {
|
|
|
|
|
if (channel_) {
|
|
|
|
|
channel_->InvokeMethod(event,
|
|
|
|
|
std::make_unique<flutter::EncodableValue>(data));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void HandleWindowMethod(
|
|
|
|
|
const std::string& method,
|
|
|
|
|
const flutter::EncodableMap* arguments,
|
|
|
|
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
|
|
|
|
if (method == "window_show") {
|
|
|
|
|
if (hwnd_) {
|
|
|
|
|
::ShowWindow(hwnd_, SW_SHOW);
|
|
|
|
|
}
|
|
|
|
|
result->Success();
|
|
|
|
|
} else if (method == "window_hide") {
|
|
|
|
|
if (hwnd_) {
|
|
|
|
|
::ShowWindow(hwnd_, SW_HIDE);
|
|
|
|
|
}
|
|
|
|
|
result->Success();
|
2026-06-06 22:03:56 +02:00
|
|
|
} 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<LPARAM>(&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();
|
Add dual-screen presenter mode (slide on beamer, notes on laptop)
When a second display is connected (macOS), presenting now opens a
borderless audience window on the beamer showing the slide, while the
main window shows the presenter view (current/next slide, speaker notes,
clock, controls) on the laptop. The two windows stay in sync over method
channels: navigation, blank screen, audio-complete and beamer clicks are
forwarded between them, and media plays only on the beamer to avoid
double audio. Falls back to the existing single-window presenter when
there is one display or the second window can't be created.
- Vendors a fork of desktop_multi_window in third_party/ that re-adds the
native macOS window geometry/fullscreen calls (coverScreen, setFrame,
close) the published 0.3.0 dropped; wired via a path dependency.
- Registers the app's plugins for sub-windows in MainFlutterWindow so
video/image rendering works on the beamer.
- Routes the multi_window dart entrypoint to a minimal AudienceWindowApp.
Compiles (flutter analyze + macOS debug build) and all tests pass;
runtime two-screen behaviour still needs verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:25:34 +02:00
|
|
|
} else {
|
|
|
|
|
result->Error("-1", "unknown method: " + method);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
void SetWindowHandle(HWND hwnd) { hwnd_ = hwnd; }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
std::string window_id_;
|
|
|
|
|
HWND hwnd_;
|
|
|
|
|
std::string window_argument_;
|
|
|
|
|
std::shared_ptr<flutter::MethodChannel<flutter::EncodableValue>> channel_;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_WRAPPER_H_
|