314 lines
11 KiB
C++
314 lines
11 KiB
C++
#include "flutter_window.h"
|
|
|
|
#include <optional>
|
|
|
|
#include "flutter/generated_plugin_registrant.h"
|
|
#include "mpv/mpv_plugin.h"
|
|
|
|
// Registry key for window placement persistence
|
|
static constexpr wchar_t kWindowPlacementKey[] = L"Software\\Plezy";
|
|
static constexpr wchar_t kWindowPlacementValue[] = L"WindowPlacement";
|
|
|
|
// Debounce timer for saving window placement
|
|
static UINT_PTR g_saveTimerId = 0;
|
|
static HWND g_mainHwnd = nullptr;
|
|
// When true, WM_WINDOWPOSCHANGED should not persist placement. Used while a
|
|
// native fullscreen toggle is in flux so we don't record the fullscreen rect
|
|
// as the user's last "normal" placement.
|
|
static bool g_suppressPlacementSave = false;
|
|
|
|
// Forward declaration
|
|
static void SaveWindowPlacement(HWND hwnd);
|
|
|
|
// Timer callback for debounced save
|
|
static void CALLBACK SaveTimerProc(HWND, UINT, UINT_PTR, DWORD) {
|
|
if (g_mainHwnd) SaveWindowPlacement(g_mainHwnd);
|
|
KillTimer(nullptr, g_saveTimerId);
|
|
g_saveTimerId = 0;
|
|
}
|
|
|
|
// Write a WINDOWPLACEMENT struct directly to the registry.
|
|
static void WriteWindowPlacement(const WINDOWPLACEMENT& wp) {
|
|
HKEY hKey;
|
|
if (RegCreateKeyExW(HKEY_CURRENT_USER, kWindowPlacementKey, 0, nullptr,
|
|
REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey,
|
|
nullptr) == ERROR_SUCCESS) {
|
|
RegSetValueExW(hKey, kWindowPlacementValue, 0, REG_BINARY,
|
|
reinterpret_cast<const BYTE*>(&wp), sizeof(wp));
|
|
RegCloseKey(hKey);
|
|
}
|
|
}
|
|
|
|
// Save the window's current WINDOWPLACEMENT to registry.
|
|
static void SaveWindowPlacement(HWND hwnd) {
|
|
WINDOWPLACEMENT wp{};
|
|
wp.length = sizeof(wp);
|
|
if (!GetWindowPlacement(hwnd, &wp)) return;
|
|
WriteWindowPlacement(wp);
|
|
}
|
|
|
|
// Load and apply WINDOWPLACEMENT from registry
|
|
// Returns whether the window should be maximized
|
|
static bool LoadWindowPlacement(HWND hwnd) {
|
|
HKEY hKey;
|
|
if (RegOpenKeyExW(HKEY_CURRENT_USER, kWindowPlacementKey, 0, KEY_READ,
|
|
&hKey) != ERROR_SUCCESS)
|
|
return false;
|
|
|
|
WINDOWPLACEMENT wp{};
|
|
wp.length = sizeof(wp);
|
|
DWORD size = sizeof(wp);
|
|
bool wasMaximized = false;
|
|
|
|
if (RegQueryValueExW(hKey, kWindowPlacementValue, nullptr, nullptr,
|
|
reinterpret_cast<BYTE*>(&wp), &size) == ERROR_SUCCESS &&
|
|
size == sizeof(wp)) {
|
|
// Prevent restoring as minimized
|
|
if (wp.showCmd == SW_SHOWMINIMIZED) wp.showCmd = SW_SHOWNORMAL;
|
|
SetWindowPlacement(hwnd, &wp);
|
|
wasMaximized = (wp.showCmd == SW_SHOWMAXIMIZED);
|
|
}
|
|
|
|
RegCloseKey(hKey);
|
|
return wasMaximized;
|
|
}
|
|
|
|
// Debounce save to avoid excessive registry writes during resize/move
|
|
static void DebounceSaveWindowPlacement(HWND hwnd) {
|
|
g_mainHwnd = hwnd;
|
|
if (g_saveTimerId) KillTimer(nullptr, g_saveTimerId);
|
|
g_saveTimerId = SetTimer(nullptr, 0, 500, SaveTimerProc); // 500ms debounce
|
|
}
|
|
|
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
|
: project_(project) {}
|
|
|
|
FlutterWindow::~FlutterWindow() {}
|
|
|
|
bool FlutterWindow::OnCreate() {
|
|
if (!Win32Window::OnCreate()) {
|
|
return false;
|
|
}
|
|
|
|
RECT frame = GetClientArea();
|
|
|
|
// The size here must match the window dimensions to avoid unnecessary surface
|
|
// creation / destruction in the startup path.
|
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
|
frame.right - frame.left, frame.bottom - frame.top, project_);
|
|
// Ensure that basic setup of the controller was successful.
|
|
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
|
return false;
|
|
}
|
|
RegisterPlugins(flutter_controller_->engine());
|
|
|
|
// Register mpv player plugin.
|
|
OutputDebugStringA("FlutterWindow: About to register MpvPlayerPlugin\n");
|
|
MpvPlayerPluginRegisterWithRegistrar(
|
|
flutter_controller_->engine()->GetRegistrarForPlugin("MpvPlayerPlugin"));
|
|
OutputDebugStringA("FlutterWindow: MpvPlayerPlugin registered\n");
|
|
|
|
RegisterWindowChannel();
|
|
|
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
|
|
|
// Load saved window placement before showing
|
|
HWND hwnd = GetHandle();
|
|
bool maximized = LoadWindowPlacement(hwnd);
|
|
|
|
flutter_controller_->engine()->SetNextFrameCallback([this, maximized]() {
|
|
::ShowWindow(this->GetHandle(), maximized ? SW_SHOWMAXIMIZED : SW_SHOWNORMAL);
|
|
});
|
|
|
|
// Flutter can complete the first frame before the "show window" callback is
|
|
// registered. The following call ensures a frame is pending to ensure the
|
|
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
|
flutter_controller_->ForceRedraw();
|
|
|
|
return true;
|
|
}
|
|
|
|
void FlutterWindow::OnDestroy() {
|
|
// Cancel any pending save timer and save immediately
|
|
if (g_saveTimerId) {
|
|
KillTimer(nullptr, g_saveTimerId);
|
|
g_saveTimerId = 0;
|
|
}
|
|
// If still fullscreen at shutdown, persist the pre-fullscreen placement
|
|
// rather than the fullscreen rect so the next launch restores correctly.
|
|
if (is_fullscreen_ && placement_before_fullscreen_.length != 0) {
|
|
WriteWindowPlacement(placement_before_fullscreen_);
|
|
} else {
|
|
SaveWindowPlacement(GetHandle());
|
|
}
|
|
|
|
window_channel_ = nullptr;
|
|
|
|
if (flutter_controller_) {
|
|
flutter_controller_ = nullptr;
|
|
}
|
|
|
|
Win32Window::OnDestroy();
|
|
}
|
|
|
|
LRESULT
|
|
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
|
WPARAM const wparam,
|
|
LPARAM const lparam) noexcept {
|
|
// Give Flutter, including plugins, an opportunity to handle window messages.
|
|
if (flutter_controller_) {
|
|
std::optional<LRESULT> result =
|
|
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
|
lparam);
|
|
if (result) {
|
|
return *result;
|
|
}
|
|
}
|
|
|
|
switch (message) {
|
|
case WM_FONTCHANGE:
|
|
flutter_controller_->engine()->ReloadSystemFonts();
|
|
break;
|
|
case WM_WINDOWPOSCHANGED:
|
|
// Don't persist placement while in fullscreen or mid-toggle — the rect
|
|
// would overwrite the user's real window position.
|
|
if (!is_fullscreen_ && !g_suppressPlacementSave) {
|
|
DebounceSaveWindowPlacement(hwnd);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Native fullscreen (plezy/window method channel)
|
|
//
|
|
// Works around window_manager 0.5.1's multi-monitor fullscreen bug where the
|
|
// window ends up on the wrong monitor and renders only a fragment of the
|
|
// Flutter surface. We:
|
|
// 1) Resolve the target monitor by the current window's center point
|
|
// (robust across DPI and restore transitions).
|
|
// 2) Save WINDOWPLACEMENT + styles once.
|
|
// 3) Strip WS_OVERLAPPEDWINDOW and move+size in a single SetWindowPos so
|
|
// Flutter only relayouts once, at the final DPI/size.
|
|
// 4) On exit, restore styles and WINDOWPLACEMENT, re-maximizing if needed.
|
|
// ---------------------------------------------------------------------------
|
|
void FlutterWindow::RegisterWindowChannel() {
|
|
auto messenger = flutter_controller_->engine()->messenger();
|
|
window_channel_ =
|
|
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
|
messenger, "plezy/window",
|
|
&flutter::StandardMethodCodec::GetInstance());
|
|
|
|
window_channel_->SetMethodCallHandler(
|
|
[this](const flutter::MethodCall<flutter::EncodableValue>& call,
|
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>
|
|
result) {
|
|
const std::string& name = call.method_name();
|
|
if (name == "setFullScreen") {
|
|
bool value = false;
|
|
if (const auto* args =
|
|
std::get_if<flutter::EncodableMap>(call.arguments())) {
|
|
auto it = args->find(flutter::EncodableValue("isFullScreen"));
|
|
if (it != args->end()) {
|
|
if (const bool* b = std::get_if<bool>(&it->second)) value = *b;
|
|
}
|
|
}
|
|
SetNativeFullScreen(value);
|
|
result->Success();
|
|
} else if (name == "isFullScreen") {
|
|
result->Success(flutter::EncodableValue(is_fullscreen_));
|
|
} else {
|
|
result->NotImplemented();
|
|
}
|
|
});
|
|
}
|
|
|
|
void FlutterWindow::NotifyFullScreenChanged() {
|
|
if (!window_channel_) return;
|
|
window_channel_->InvokeMethod(
|
|
"onFullScreenChanged",
|
|
std::make_unique<flutter::EncodableValue>(is_fullscreen_));
|
|
}
|
|
|
|
void FlutterWindow::SetNativeFullScreen(bool fullscreen) {
|
|
HWND hwnd = GetHandle();
|
|
if (!hwnd) return;
|
|
if (fullscreen == is_fullscreen_) return;
|
|
|
|
g_suppressPlacementSave = true;
|
|
// Also cancel any pending save so a WM_WINDOWPOSCHANGED just before the
|
|
// toggle doesn't fire SaveTimerProc mid-transition.
|
|
if (g_saveTimerId) {
|
|
KillTimer(nullptr, g_saveTimerId);
|
|
g_saveTimerId = 0;
|
|
}
|
|
|
|
if (fullscreen) {
|
|
// Resolve target monitor first — if this fails we bail before mutating
|
|
// any saved state, so a subsequent exit call has nothing stale to act on.
|
|
RECT wr{};
|
|
::GetWindowRect(hwnd, &wr);
|
|
// Center point is stable across maximize/straddle cases where
|
|
// MonitorFromWindow would resolve to the neighbor monitor.
|
|
POINT center{(wr.left + wr.right) / 2, (wr.top + wr.bottom) / 2};
|
|
MONITORINFO mi{};
|
|
mi.cbSize = sizeof(mi);
|
|
if (!::GetMonitorInfoW(::MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST),
|
|
&mi)) {
|
|
g_suppressPlacementSave = false;
|
|
return;
|
|
}
|
|
|
|
// Save pre-fullscreen state (showCmd inside the placement carries the
|
|
// maximize bit, so no separate flag is needed).
|
|
placement_before_fullscreen_.length = sizeof(WINDOWPLACEMENT);
|
|
::GetWindowPlacement(hwnd, &placement_before_fullscreen_);
|
|
style_before_fullscreen_ = ::GetWindowLongPtr(hwnd, GWL_STYLE);
|
|
ex_style_before_fullscreen_ = ::GetWindowLongPtr(hwnd, GWL_EXSTYLE);
|
|
|
|
// Strip frame/caption. Stripping WS_OVERLAPPEDWINDOW alone is enough to
|
|
// make the following SetWindowPos use the given rect exactly — no need
|
|
// to ShowWindow(SW_SHOWNORMAL) first (would cause a second relayout).
|
|
::SetWindowLongPtr(
|
|
hwnd, GWL_STYLE, style_before_fullscreen_ & ~WS_OVERLAPPEDWINDOW);
|
|
::SetWindowLongPtr(
|
|
hwnd, GWL_EXSTYLE,
|
|
ex_style_before_fullscreen_ &
|
|
~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE |
|
|
WS_EX_STATICEDGE));
|
|
|
|
const RECT& r = mi.rcMonitor;
|
|
::SetWindowPos(hwnd, HWND_TOP, r.left, r.top, r.right - r.left,
|
|
r.bottom - r.top,
|
|
SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
|
|
is_fullscreen_ = true;
|
|
} else {
|
|
if (style_before_fullscreen_ != 0) {
|
|
::SetWindowLongPtr(hwnd, GWL_STYLE, style_before_fullscreen_);
|
|
::SetWindowLongPtr(hwnd, GWL_EXSTYLE, ex_style_before_fullscreen_);
|
|
}
|
|
|
|
if (placement_before_fullscreen_.length == sizeof(WINDOWPLACEMENT)) {
|
|
WINDOWPLACEMENT wp = placement_before_fullscreen_;
|
|
if (wp.showCmd == SW_SHOWMINIMIZED) wp.showCmd = SW_SHOWNORMAL;
|
|
::SetWindowPlacement(hwnd, &wp);
|
|
}
|
|
|
|
// Force a frame refresh so restored chrome paints.
|
|
::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE |
|
|
SWP_FRAMECHANGED);
|
|
|
|
is_fullscreen_ = false;
|
|
placement_before_fullscreen_ = {};
|
|
style_before_fullscreen_ = 0;
|
|
ex_style_before_fullscreen_ = 0;
|
|
}
|
|
|
|
g_suppressPlacementSave = false;
|
|
NotifyFullScreenChanged();
|
|
}
|