Implement dialog windows for the win32 platform (#176309)
## What's new?
- Implemented `DialogWindowControllerWin32` for win32 dialogs :rocket:
- Refactored and updated the Win32 embedder to support dialogs
- Updated the `multiple_windows` example to demonstrate both modal and
modeless dialogs
- Added integration tests for the dialog windows
- Added tests for dialogs in the embedder
## How to test?
1. Run the `multiple_windows` example application with a local engine
built from this pull request
2. Click the `Modeless Dialog` creation button on the main window
3. Open a regular window and click the `Modal Dialog` creation button
4. Note the behavior of modal and modeless dialogs
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
diff --git a/dev/integration_tests/windowing_test/lib/main.dart b/dev/integration_tests/windowing_test/lib/main.dart
index 5f9416d..c40a456 100644
--- a/dev/integration_tests/windowing_test/lib/main.dart
+++ b/dev/integration_tests/windowing_test/lib/main.dart
@@ -23,6 +23,9 @@
}
late final RegularWindowController controller;
+final ValueNotifier<DialogWindowController?> dialogController = ValueNotifier(
+ null,
+);
void main() {
final Completer<void> windowCreated = Completer();
@@ -61,7 +64,9 @@
controller.removeListener(notificationHandler);
}
- if (jsonMap['type'] == 'get_size') {
+ if (jsonMap['type'] == 'ping') {
+ return jsonEncode({'type': 'pong'});
+ } else if (jsonMap['type'] == 'get_size') {
return jsonEncode({
'width': controller.contentSize.width,
'height': controller.contentSize.height,
@@ -129,6 +134,23 @@
}, () => controller.isActivated);
} else if (jsonMap['type'] == 'get_activated') {
return jsonEncode({'isActivated': controller.isActivated});
+ } else if (jsonMap['type'] == 'open_dialog') {
+ if (dialogController.value != null) {
+ return jsonEncode({'result': false});
+ }
+ dialogController.value = DialogWindowController(
+ preferredSize: const Size(200, 200),
+ parent: controller,
+ delegate: MyDialogWindowControllerDelegate(
+ onDestroyed: () {
+ dialogController.value = null;
+ },
+ ),
+ );
+ return jsonEncode({'result': true});
+ } else if (jsonMap['type'] == 'close_dialog') {
+ dialogController.value?.destroy();
+ return jsonEncode({'result': true});
} else {
throw ArgumentError('Unknown message type: ${jsonMap['type']}');
}
@@ -169,38 +191,82 @@
State<MyHomePage> createState() => _MyHomePageState();
}
-class _MyHomePageState extends State<MyHomePage> {
- int _counter = 0;
+class MyDialogWindowControllerDelegate extends DialogWindowControllerDelegate {
+ MyDialogWindowControllerDelegate({required this.onDestroyed});
- void _incrementCounter() {
- setState(() {
- _counter++;
- });
+ final VoidCallback onDestroyed;
+
+ @override
+ void onWindowDestroyed() {
+ onDestroyed();
+ super.onWindowDestroyed();
}
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+ @override
+ Widget build(BuildContext context) {
+ return ValueListenableBuilder(
+ valueListenable: dialogController,
+ builder:
+ (
+ BuildContext context,
+ DialogWindowController? dialogController,
+ Widget? child,
+ ) {
+ return ViewAnchor(
+ view: dialogController != null
+ ? DialogWindow(
+ controller: dialogController,
+ child: MyDialogPage(controller: dialogController),
+ )
+ : null,
+ child: Scaffold(
+ appBar: AppBar(
+ backgroundColor: Theme.of(context).colorScheme.inversePrimary,
+ title: Text(widget.title),
+ ),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[const Text('This is the main window.')],
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
+
+class MyDialogPage extends StatelessWidget {
+ const MyDialogPage({super.key, required this.controller});
+
+ final DialogWindowController controller;
@override
Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- backgroundColor: Theme.of(context).colorScheme.inversePrimary,
- title: Text(widget.title),
- ),
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- const Text('You have pushed the button this many times:'),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ],
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ backgroundColor: Theme.of(context).colorScheme.inversePrimary,
+ title: Text('Dialog'),
),
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: _incrementCounter,
- tooltip: 'Increment',
- child: const Icon(Icons.add),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ const Text('This is a dialog window.'),
+ ElevatedButton(
+ key: const ValueKey<String>('close_dialog'),
+ onPressed: () {
+ controller.destroy();
+ },
+ child: Text('Close Dialog'),
+ ),
+ ],
+ ),
+ ),
),
);
}
diff --git a/dev/integration_tests/windowing_test/test_driver/main_test.dart b/dev/integration_tests/windowing_test/test_driver/main_test.dart
index d6572d6..0a7ab93 100644
--- a/dev/integration_tests/windowing_test/test_driver/main_test.dart
+++ b/dev/integration_tests/windowing_test/test_driver/main_test.dart
@@ -13,6 +13,7 @@
setUpAll(() async {
driver = await FlutterDriver.connect();
+ await driver.requestData(jsonEncode({'type': 'ping'}));
});
tearDownAll(() async {
@@ -168,5 +169,16 @@
timeout: Timeout.none,
onPlatform: {'linux': Skip('isMinimized is not supported on Wayland')},
);
+
+ test(
+ 'Can open dialog',
+ () async {
+ await driver.requestData(jsonEncode({'type': 'open_dialog'}));
+ await driver.waitFor(find.byValueKey('close_dialog'));
+ await driver.requestData(jsonEncode({'type': 'close_dialog'}));
+ },
+ timeout: Timeout.none,
+ onPlatform: {'linux': Skip('Dialogs are not yet supported on Wayland')},
+ );
});
}
diff --git a/engine/src/flutter/shell/platform/common/windowing.h b/engine/src/flutter/shell/platform/common/windowing.h
index 5c37df3..beb2ca4 100644
--- a/engine/src/flutter/shell/platform/common/windowing.h
+++ b/engine/src/flutter/shell/platform/common/windowing.h
@@ -8,11 +8,12 @@
namespace flutter {
// Types of windows.
-// The value must match value from WindowType in the Dart code
-// in packages/flutter/lib/src/widgets/window.dart
enum class WindowArchetype {
// Regular top-level window.
kRegular,
+
+ // Dialog window.
+ kDialog,
};
} // namespace flutter
diff --git a/engine/src/flutter/shell/platform/windows/BUILD.gn b/engine/src/flutter/shell/platform/windows/BUILD.gn
index 62730cc..7616ccb 100644
--- a/engine/src/flutter/shell/platform/windows/BUILD.gn
+++ b/engine/src/flutter/shell/platform/windows/BUILD.gn
@@ -93,6 +93,10 @@
"flutter_windows_view_controller.h",
"host_window.cc",
"host_window.h",
+ "host_window_dialog.cc",
+ "host_window_dialog.h",
+ "host_window_regular.cc",
+ "host_window_regular.h",
"keyboard_handler_base.h",
"keyboard_key_channel_handler.cc",
"keyboard_key_channel_handler.h",
diff --git a/engine/src/flutter/shell/platform/windows/host_window.cc b/engine/src/flutter/shell/platform/windows/host_window.cc
index 3854b17..501d936 100644
--- a/engine/src/flutter/shell/platform/windows/host_window.cc
+++ b/engine/src/flutter/shell/platform/windows/host_window.cc
@@ -3,6 +3,8 @@
// found in the LICENSE file.
#include "flutter/shell/platform/windows/host_window.h"
+#include "flutter/shell/platform/windows/host_window_dialog.h"
+#include "flutter/shell/platform/windows/host_window_regular.h"
#include <dwmapi.h>
@@ -90,61 +92,6 @@
return oss.str();
}
-// Calculates the required window size, in physical coordinates, to
-// accommodate the given |client_size|, in logical coordinates, constrained by
-// optional |smallest| and |biggest|, for a window with the specified
-// |window_style| and |extended_window_style|. If |owner_hwnd| is not null, the
-// DPI of the display with the largest area of intersection with |owner_hwnd| is
-// used for the calculation; otherwise, the primary display's DPI is used. The
-// resulting size includes window borders, non-client areas, and drop shadows.
-// On error, returns std::nullopt and logs an error message.
-std::optional<flutter::Size> GetWindowSizeForClientSize(
- flutter::WindowsProcTable const& win32,
- flutter::Size const& client_size,
- std::optional<flutter::Size> smallest,
- std::optional<flutter::Size> biggest,
- DWORD window_style,
- DWORD extended_window_style,
- HWND owner_hwnd) {
- UINT const dpi = flutter::GetDpiForHWND(owner_hwnd);
- double const scale_factor =
- static_cast<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
- RECT rect = {
- .right = static_cast<LONG>(client_size.width() * scale_factor),
- .bottom = static_cast<LONG>(client_size.height() * scale_factor)};
-
- if (!win32.AdjustWindowRectExForDpi(&rect, window_style, FALSE,
- extended_window_style, dpi)) {
- FML_LOG(ERROR) << "Failed to run AdjustWindowRectExForDpi: "
- << GetLastErrorAsString();
- return std::nullopt;
- }
-
- double width = static_cast<double>(rect.right - rect.left);
- double height = static_cast<double>(rect.bottom - rect.top);
-
- // Apply size constraints
- double const non_client_width = width - (client_size.width() * scale_factor);
- double const non_client_height =
- height - (client_size.height() * scale_factor);
- if (smallest) {
- flutter::Size min_physical_size = ClampToVirtualScreen(
- flutter::Size(smallest->width() * scale_factor + non_client_width,
- smallest->height() * scale_factor + non_client_height));
- width = std::max(width, min_physical_size.width());
- height = std::max(height, min_physical_size.height());
- }
- if (biggest) {
- flutter::Size max_physical_size = ClampToVirtualScreen(
- flutter::Size(biggest->width() * scale_factor + non_client_width,
- biggest->height() * scale_factor + non_client_height));
- width = std::min(width, max_physical_size.width());
- height = std::min(height, max_physical_size.height());
- }
-
- return flutter::Size{width, height};
-}
-
// Checks whether the window class of name |class_name| is registered for the
// current application.
bool IsClassRegistered(LPCWSTR class_name) {
@@ -260,27 +207,37 @@
const WindowSizeRequest& preferred_size,
const WindowConstraints& preferred_constraints,
LPCWSTR title) {
- DWORD window_style = WS_OVERLAPPEDWINDOW;
- DWORD extended_window_style = 0;
- auto const box_constraints = FromWindowConstraints(preferred_constraints);
+ return std::unique_ptr<HostWindow>(new HostWindowRegular(
+ window_manager, engine, preferred_size,
+ FromWindowConstraints(preferred_constraints), title));
+}
- // TODO(knopp): What about windows sized to content?
- FML_CHECK(preferred_size.has_preferred_view_size);
+std::unique_ptr<HostWindow> HostWindow::CreateDialogWindow(
+ WindowManager* window_manager,
+ FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const WindowConstraints& preferred_constraints,
+ LPCWSTR title,
+ HWND parent) {
+ return std::unique_ptr<HostWindow>(
+ new HostWindowDialog(window_manager, engine, preferred_size,
+ FromWindowConstraints(preferred_constraints), title,
+ parent ? parent : std::optional<HWND>()));
+}
- // Calculate the screen space window rectangle for the new window.
- // Default positioning values (CW_USEDEFAULT) are used
- // if the window has no owner.
- Rect const initial_window_rect = [&]() -> Rect {
- std::optional<Size> const window_size = GetWindowSizeForClientSize(
- *engine->windows_proc_table(),
- Size(preferred_size.preferred_view_width,
- preferred_size.preferred_view_height),
- box_constraints.smallest(), box_constraints.biggest(), window_style,
- extended_window_style, nullptr);
- return {{CW_USEDEFAULT, CW_USEDEFAULT},
- window_size ? *window_size : Size{CW_USEDEFAULT, CW_USEDEFAULT}};
- }();
-
+HostWindow::HostWindow(WindowManager* window_manager,
+ FlutterWindowsEngine* engine,
+ WindowArchetype archetype,
+ DWORD window_style,
+ DWORD extended_window_style,
+ const BoxConstraints& box_constraints,
+ Rect const initial_window_rect,
+ LPCWSTR title,
+ std::optional<HWND> const& owner_window)
+ : window_manager_(window_manager),
+ engine_(engine),
+ archetype_(archetype),
+ box_constraints_(box_constraints) {
// Set up the view.
auto view_window = std::make_unique<FlutterWindow>(
initial_window_rect.width(), initial_window_rect.height(),
@@ -288,12 +245,9 @@
std::unique_ptr<FlutterWindowsView> view =
engine->CreateView(std::move(view_window));
- if (view == nullptr) {
- FML_LOG(ERROR) << "Failed to create view";
- return nullptr;
- }
+ FML_CHECK(view != nullptr);
- std::unique_ptr<FlutterWindowsViewController> view_controller =
+ view_controller_ =
std::make_unique<FlutterWindowsViewController>(nullptr, std::move(view));
FML_CHECK(engine->running());
// The Windows embedder listens to accessibility updates using the
@@ -317,69 +271,46 @@
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
- if (!RegisterClassEx(&window_class)) {
- FML_LOG(ERROR) << "Cannot register window class "
- << WCharBufferToString(kWindowClassName) << ": "
- << GetLastErrorAsString();
- return nullptr;
- }
+ FML_CHECK(RegisterClassEx(&window_class));
}
// Create the native window.
- HWND hwnd = CreateWindowEx(
+ window_handle_ = CreateWindowEx(
extended_window_style, kWindowClassName, title, window_style,
initial_window_rect.left(), initial_window_rect.top(),
- initial_window_rect.width(), initial_window_rect.height(), nullptr,
- nullptr, GetModuleHandle(nullptr), engine->windows_proc_table().get());
- if (!hwnd) {
- FML_LOG(ERROR) << "Cannot create window: " << GetLastErrorAsString();
- return nullptr;
- }
+ initial_window_rect.width(), initial_window_rect.height(),
+ owner_window ? *owner_window : nullptr, nullptr, GetModuleHandle(nullptr),
+ engine->windows_proc_table().get());
+ FML_CHECK(window_handle_ != nullptr);
// Adjust the window position so its origin aligns with the top-left corner
// of the window frame, not the window rectangle (which includes the
// drop-shadow). This adjustment must be done post-creation since the frame
// rectangle is only available after the window has been created.
RECT frame_rect;
- DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect,
- sizeof(frame_rect));
+ DwmGetWindowAttribute(window_handle_, DWMWA_EXTENDED_FRAME_BOUNDS,
+ &frame_rect, sizeof(frame_rect));
RECT window_rect;
- GetWindowRect(hwnd, &window_rect);
+ GetWindowRect(window_handle_, &window_rect);
LONG const left_dropshadow_width = frame_rect.left - window_rect.left;
LONG const top_dropshadow_height = window_rect.top - frame_rect.top;
- SetWindowPos(hwnd, nullptr, window_rect.left - left_dropshadow_width,
+ SetWindowPos(window_handle_, nullptr,
+ window_rect.left - left_dropshadow_width,
window_rect.top - top_dropshadow_height, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
- UpdateTheme(hwnd);
+ UpdateTheme(window_handle_);
- SetChildContent(view_controller->view()->GetWindowHandle(), hwnd);
+ SetChildContent(view_controller_->view()->GetWindowHandle(), window_handle_);
// TODO(loicsharma): Hide the window until the first frame is rendered.
// Single window apps use the engine's next frame callback to show the
// window. This doesn't work for multi window apps as the engine cannot have
// multiple next frame callbacks. If multiple windows are created, only the
// last one will be shown.
- ShowWindow(hwnd, SW_SHOWNORMAL);
- return std::unique_ptr<HostWindow>(
- new HostWindow(window_manager, engine, WindowArchetype::kRegular,
- std::move(view_controller), box_constraints, hwnd));
-}
-
-HostWindow::HostWindow(
- WindowManager* window_manager,
- FlutterWindowsEngine* engine,
- WindowArchetype archetype,
- std::unique_ptr<FlutterWindowsViewController> view_controller,
- const BoxConstraints& box_constraints,
- HWND hwnd)
- : window_manager_(window_manager),
- engine_(engine),
- archetype_(archetype),
- view_controller_(std::move(view_controller)),
- window_handle_(hwnd),
- box_constraints_(box_constraints) {
- SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
+ ShowWindow(window_handle_, SW_SHOWNORMAL);
+ SetWindowLongPtr(window_handle_, GWLP_USERDATA,
+ reinterpret_cast<LONG_PTR>(this));
}
HostWindow::~HostWindow() {
@@ -394,6 +325,17 @@
}
HostWindow* HostWindow::GetThisFromHandle(HWND hwnd) {
+ wchar_t class_name[256];
+ if (!GetClassName(hwnd, class_name, sizeof(class_name) / sizeof(wchar_t))) {
+ FML_LOG(ERROR) << "Failed to get class name for window handle " << hwnd
+ << ": " << GetLastErrorAsString();
+ return nullptr;
+ }
+ // Ignore window handles that do not match the expected class name.
+ if (wcscmp(class_name, kWindowClassName) != 0) {
+ return nullptr;
+ }
+
return reinterpret_cast<HostWindow*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
}
@@ -401,7 +343,7 @@
return window_handle_;
}
-void HostWindow::FocusViewOf(HostWindow* window) {
+void HostWindow::FocusRootViewOf(HostWindow* window) {
auto child_content = window->view_controller_->view()->GetWindowHandle();
if (window != nullptr && child_content != nullptr) {
SetFocus(child_content);
@@ -436,6 +378,10 @@
}
switch (message) {
+ case WM_DESTROY:
+ is_being_destroyed_ = true;
+ break;
+
case WM_DPICHANGED: {
auto* const new_scaled_window_rect = reinterpret_cast<RECT*>(lparam);
LONG const width =
@@ -494,13 +440,9 @@
}
case WM_ACTIVATE:
- FocusViewOf(this);
+ FocusRootViewOf(this);
return 0;
- case WM_MOUSEACTIVATE:
- FocusViewOf(this);
- return MA_ACTIVATE;
-
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
@@ -761,4 +703,140 @@
.height = rect.bottom / dpr,
};
}
+
+std::optional<Size> HostWindow::GetWindowSizeForClientSize(
+ WindowsProcTable const& win32,
+ Size const& client_size,
+ std::optional<Size> smallest,
+ std::optional<Size> biggest,
+ DWORD window_style,
+ DWORD extended_window_style,
+ std::optional<HWND> const& owner_hwnd) {
+ UINT const dpi = GetDpiForHWND(owner_hwnd ? *owner_hwnd : nullptr);
+ double const scale_factor =
+ static_cast<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
+ RECT rect = {
+ .right = static_cast<LONG>(client_size.width() * scale_factor),
+ .bottom = static_cast<LONG>(client_size.height() * scale_factor)};
+
+ if (!win32.AdjustWindowRectExForDpi(&rect, window_style, FALSE,
+ extended_window_style, dpi)) {
+ FML_LOG(ERROR) << "Failed to run AdjustWindowRectExForDpi: "
+ << GetLastErrorAsString();
+ return std::nullopt;
+ }
+
+ double width = static_cast<double>(rect.right - rect.left);
+ double height = static_cast<double>(rect.bottom - rect.top);
+
+ // Apply size constraints.
+ double const non_client_width = width - (client_size.width() * scale_factor);
+ double const non_client_height =
+ height - (client_size.height() * scale_factor);
+ if (smallest) {
+ flutter::Size min_physical_size = ClampToVirtualScreen(
+ flutter::Size(smallest->width() * scale_factor + non_client_width,
+ smallest->height() * scale_factor + non_client_height));
+ width = std::max(width, min_physical_size.width());
+ height = std::max(height, min_physical_size.height());
+ }
+ if (biggest) {
+ flutter::Size max_physical_size = ClampToVirtualScreen(
+ flutter::Size(biggest->width() * scale_factor + non_client_width,
+ biggest->height() * scale_factor + non_client_height));
+ width = std::min(width, max_physical_size.width());
+ height = std::min(height, max_physical_size.height());
+ }
+
+ return flutter::Size{width, height};
+}
+
+void HostWindow::EnableRecursively(bool enable) {
+ EnableWindow(window_handle_, enable);
+
+ for (HostWindow* const owned : GetOwnedWindows()) {
+ owned->EnableRecursively(enable);
+ }
+}
+
+HostWindow* HostWindow::FindFirstEnabledDescendant() const {
+ if (IsWindowEnabled(window_handle_)) {
+ return const_cast<HostWindow*>(this);
+ }
+
+ for (HostWindow* const owned : GetOwnedWindows()) {
+ if (HostWindow* const result = owned->FindFirstEnabledDescendant()) {
+ return result;
+ }
+ }
+
+ return nullptr;
+}
+
+std::vector<HostWindow*> HostWindow::GetOwnedWindows() const {
+ std::vector<HostWindow*> owned_windows;
+ struct EnumData {
+ HWND owner_window_handle;
+ std::vector<HostWindow*>* owned_windows;
+ } data{window_handle_, &owned_windows};
+
+ EnumWindows(
+ [](HWND hwnd, LPARAM lparam) -> BOOL {
+ auto* const data = reinterpret_cast<EnumData*>(lparam);
+ if (GetWindow(hwnd, GW_OWNER) == data->owner_window_handle) {
+ HostWindow* const window = GetThisFromHandle(hwnd);
+ if (window && !window->is_being_destroyed_) {
+ data->owned_windows->push_back(window);
+ }
+ }
+ return TRUE;
+ },
+ reinterpret_cast<LPARAM>(&data));
+
+ return owned_windows;
+}
+
+HostWindow* HostWindow::GetOwnerWindow() const {
+ if (HWND const owner_window_handle = GetWindow(GetWindowHandle(), GW_OWNER)) {
+ return GetThisFromHandle(owner_window_handle);
+ }
+ return nullptr;
+};
+
+void HostWindow::DisableRecursively() {
+ // Disable the window itself.
+ EnableWindow(window_handle_, false);
+
+ for (HostWindow* const owned : GetOwnedWindows()) {
+ owned->DisableRecursively();
+ }
+}
+
+void HostWindow::UpdateModalStateLayer() {
+ auto children = GetOwnedWindows();
+ if (children.empty()) {
+ // Leaf window in the active path, enable it.
+ EnableWindow(window_handle_, true);
+ } else {
+ // Non-leaf window in the active path, disable it and process children.
+ EnableWindow(window_handle_, false);
+
+ // On same level of window hierarchy the most recently created window
+ // will remain enabled.
+ auto latest_child = *std::max_element(
+ children.begin(), children.end(), [](HostWindow* a, HostWindow* b) {
+ return a->view_controller_->view()->view_id() <
+ b->view_controller_->view()->view_id();
+ });
+
+ for (HostWindow* const child : children) {
+ if (child == latest_child) {
+ child->UpdateModalStateLayer();
+ } else {
+ child->DisableRecursively();
+ }
+ }
+ }
+}
+
} // namespace flutter
diff --git a/engine/src/flutter/shell/platform/windows/host_window.h b/engine/src/flutter/shell/platform/windows/host_window.h
index 77e28aa..88639ae3 100644
--- a/engine/src/flutter/shell/platform/windows/host_window.h
+++ b/engine/src/flutter/shell/platform/windows/host_window.h
@@ -19,6 +19,7 @@
namespace flutter {
class WindowManager;
+class WindowsProcTable;
class FlutterWindowsView;
class FlutterWindowsViewController;
@@ -27,19 +28,42 @@
public:
virtual ~HostWindow();
- // Creates a native Win32 window with a child view confined to its client
- // area. |controller| is a pointer to the controller that manages the
+ // Creates a regular Win32 window with a child view confined to its client
+ // area. |window_manager| is a pointer to the window manager that manages the
// |HostWindow|. |engine| is a pointer to the engine that manages
- // the controller. On success, a valid window handle can be retrieved
+ // the window manager. |preferred_size| is the preferred size of the window.
+ // |preferred_constraints| are the constraints set on the window's size.
+ // |title| is the title of the window.
+ //
+ // On success, a valid window handle can be retrieved
// via |HostWindow::GetWindowHandle|. |nullptr| will be returned
// on failure.
static std::unique_ptr<HostWindow> CreateRegularWindow(
- WindowManager* controller,
+ WindowManager* window_manager,
FlutterWindowsEngine* engine,
const WindowSizeRequest& preferred_size,
const WindowConstraints& preferred_constraints,
LPCWSTR title);
+ // Creates a dialog Win32 window with a child view confined to its client
+ // area. |window_manager| is a pointer to the window manager that manages the
+ // |HostWindow|. |engine| is a pointer to the engine that manages
+ // the window manager. |preferred_size| is the preferred size of the window.
+ // |preferred_constraints| are the constraints set on the window's size.
+ // |title| is the title of the window. |parent| is the parent of this dialog,
+ // which can be `nullptr`.
+ //
+ // On success, a valid window handle can be retrieved
+ // via |HostWindow::GetWindowHandle|. `nullptr` will be returned
+ // on failure.
+ static std::unique_ptr<HostWindow> CreateDialogWindow(
+ WindowManager* window_manager,
+ FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const WindowConstraints& preferred_constraints,
+ LPCWSTR title,
+ HWND parent);
+
// Returns the instance pointer for |hwnd| or nullptr if invalid.
static HostWindow* GetThisFromHandle(HWND hwnd);
@@ -60,17 +84,25 @@
// Set the fullscreen state. |display_id| indicates the display where
// the window should be shown fullscreen; std::nullopt indicates
// that no display was specified, so the current display may be used.
- void SetFullscreen(bool fullscreen,
- std::optional<FlutterEngineDisplayId> display_id);
+ virtual void SetFullscreen(bool fullscreen,
+ std::optional<FlutterEngineDisplayId> display_id);
// Returns |true| if this window is fullscreen, otherwise |false|.
- bool GetFullscreen() const;
+ virtual bool GetFullscreen() const;
// Given a window identifier, returns the window content size of the
// window.
static ActualWindowSize GetWindowContentSize(HWND hwnd);
- private:
+ // Returns the owner window, or nullptr if none.
+ HostWindow* GetOwnerWindow() const;
+
+ // This method is called when a dialog is created or destroyed.
+ // It walks the path of child windows to make sure that the right
+ // windows are enabled or disabled.
+ void UpdateModalStateLayer();
+
+ protected:
friend WindowManager;
// Information saved before going into fullscreen mode, used to restore the
@@ -85,15 +117,63 @@
MONITORINFO monitor_info;
};
- HostWindow(WindowManager* controller,
+ // Construct a host window.
+ //
+ // See:
+ // - https://learn.microsoft.com/windows/win32/winmsg/window-styles
+ // - https://learn.microsoft.com/windows/win32/winmsg/extended-window-styles
+ HostWindow(WindowManager* window_manager,
FlutterWindowsEngine* engine,
WindowArchetype archetype,
- std::unique_ptr<FlutterWindowsViewController> view_controller,
- const BoxConstraints& constraints,
- HWND hwnd);
+ DWORD window_style,
+ DWORD extended_window_style,
+ const BoxConstraints& box_constraints,
+ Rect const initial_window_rect,
+ LPCWSTR title,
+ std::optional<HWND> const& owner_window);
+
+ // Calculates the required window size, in physical coordinates, to
+ // accommodate the given |client_size|, in logical coordinates, constrained by
+ // optional |smallest| and |biggest|, for a window with the specified
+ // |window_style| and |extended_window_style|. If |owner_hwnd| is not null,
+ // the DPI of the display with the largest area of intersection with
+ // |owner_hwnd| is used for the calculation; otherwise, the primary display's
+ // DPI is used. The resulting size includes window borders, non-client areas,
+ // and drop shadows. On error, returns std::nullopt and logs an error message.
+ static std::optional<Size> GetWindowSizeForClientSize(
+ WindowsProcTable const& win32,
+ Size const& client_size,
+ std::optional<Size> smallest,
+ std::optional<Size> biggest,
+ DWORD window_style,
+ DWORD extended_window_style,
+ std::optional<HWND> const& owner_hwnd);
+
+ // Processes and routes salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT HandleMessage(HWND hwnd,
+ UINT message,
+ WPARAM wparam,
+ LPARAM lparam);
// Sets the focus to the child view window of |window|.
- static void FocusViewOf(HostWindow* window);
+ static void FocusRootViewOf(HostWindow* window);
+
+ // Enables or disables mouse and keyboard input to this window and all its
+ // descendants.
+ void EnableRecursively(bool enable);
+
+ // Returns the first enabled descendant window. If the current window itself
+ // is enabled, returns the current window. If no window is enabled, returns
+ // `nullptr`.
+ HostWindow* FindFirstEnabledDescendant() const;
+
+ // Returns windows owned by this window.
+ std::vector<HostWindow*> GetOwnedWindows() const;
+
+ // Disables mouse and keyboard input to the window and all its descendants.
+ void DisableRecursively();
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
@@ -101,11 +181,6 @@
// responds to changes in DPI. Delegates other messages to the controller.
static LRESULT WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
- // Processes and routes salient window messages for mouse handling,
- // size change and DPI. Delegates handling of these to member overloads that
- // inheriting classes can handle.
- LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
-
// Controller for this window.
WindowManager* const window_manager_ = nullptr;
@@ -121,11 +196,14 @@
WindowArchetype archetype_ = WindowArchetype::kRegular;
// Backing handle for this window.
- HWND window_handle_ = nullptr;
+ HWND window_handle_;
// The constraints on the window's client area.
BoxConstraints box_constraints_;
+ // True while handling WM_DESTROY; used to detect in-progress destruction.
+ bool is_being_destroyed_ = false;
+
// Whether or not the window is currently in a fullscreen state.
bool is_fullscreen_ = false;
diff --git a/engine/src/flutter/shell/platform/windows/host_window_dialog.cc b/engine/src/flutter/shell/platform/windows/host_window_dialog.cc
new file mode 100644
index 0000000..de6518d
--- /dev/null
+++ b/engine/src/flutter/shell/platform/windows/host_window_dialog.cc
@@ -0,0 +1,142 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/windows/host_window_dialog.h"
+
+#include "flutter/shell/platform/windows/flutter_windows_engine.h"
+
+namespace {
+DWORD GetWindowStyleForDialog(std::optional<HWND> const& owner_window) {
+ DWORD window_style = WS_OVERLAPPED | WS_CAPTION | WS_THICKFRAME;
+ if (!owner_window) {
+ // If the dialog has no owner, add a minimize box and a system menu.
+ window_style |= WS_MINIMIZEBOX | WS_SYSMENU;
+ }
+
+ return window_style;
+}
+
+DWORD GetExtendedWindowStyleForDialog(std::optional<HWND> const& owner_window) {
+ DWORD extended_window_style = WS_EX_DLGMODALFRAME;
+ if (owner_window) {
+ // If the owner window has WS_EX_TOOLWINDOW style, apply the same
+ // style to the dialog.
+ if (GetWindowLongPtr(*owner_window, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) {
+ extended_window_style |= WS_EX_TOOLWINDOW;
+ }
+ }
+ return extended_window_style;
+}
+} // namespace
+
+namespace flutter {
+
+HostWindowDialog::HostWindowDialog(WindowManager* window_manager,
+ FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints,
+ LPCWSTR title,
+ std::optional<HWND> const& owner_window)
+ : HostWindow(
+ window_manager,
+ engine,
+ WindowArchetype::kDialog,
+ GetWindowStyleForDialog(owner_window),
+ GetExtendedWindowStyleForDialog(owner_window),
+ constraints,
+ GetInitialRect(engine, preferred_size, constraints, owner_window),
+ title,
+ owner_window) {
+ auto hwnd = window_handle_;
+ if (owner_window == nullptr) {
+ if (HMENU hMenu = GetSystemMenu(hwnd, FALSE)) {
+ EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_GRAYED);
+ }
+ }
+
+ if (owner_window != nullptr) {
+ UpdateModalState();
+ }
+}
+
+Rect HostWindowDialog::GetInitialRect(FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints,
+ std::optional<HWND> const& owner_window) {
+ auto const window_style = GetWindowStyleForDialog(owner_window);
+ auto const extended_window_style =
+ GetExtendedWindowStyleForDialog(owner_window);
+ std::optional<Size> const window_size =
+ HostWindow::GetWindowSizeForClientSize(
+ *engine->windows_proc_table(),
+ Size(preferred_size.preferred_view_width,
+ preferred_size.preferred_view_height),
+ constraints.smallest(), constraints.biggest(), window_style,
+ extended_window_style, owner_window);
+ Point window_origin = {CW_USEDEFAULT, CW_USEDEFAULT};
+ if (owner_window && window_size.has_value()) {
+ // Center dialog in the owner's frame.
+ RECT frame;
+ DwmGetWindowAttribute(*owner_window, DWMWA_EXTENDED_FRAME_BOUNDS, &frame,
+ sizeof(frame));
+ window_origin = {(frame.left + frame.right - window_size->width()) * 0.5,
+ (frame.top + frame.bottom - window_size->height()) * 0.5};
+ }
+
+ return {window_origin,
+ window_size ? *window_size : Size{CW_USEDEFAULT, CW_USEDEFAULT}};
+}
+
+LRESULT HostWindowDialog::HandleMessage(HWND hwnd,
+ UINT message,
+ WPARAM wparam,
+ LPARAM lparam) {
+ switch (message) {
+ case WM_DESTROY:
+ is_being_destroyed_ = true;
+ if (HostWindow* const owner_window = GetOwnerWindow()) {
+ UpdateModalState();
+ FocusRootViewOf(owner_window);
+ }
+ break;
+
+ case WM_ACTIVATE:
+ if (LOWORD(wparam) != WA_INACTIVE) {
+ // Prevent disabled window from being activated using the task
+ // switcher.
+ if (!IsWindowEnabled(hwnd)) {
+ // Redirect focus and activation to the first enabled descendant.
+ if (HostWindow* enabled_descendant = FindFirstEnabledDescendant()) {
+ SetActiveWindow(enabled_descendant->GetWindowHandle());
+ FocusRootViewOf(this);
+ }
+ return 0;
+ }
+ FocusRootViewOf(this);
+ }
+ return 0;
+ }
+
+ return HostWindow::HandleMessage(hwnd, message, wparam, lparam);
+}
+
+void HostWindowDialog::UpdateModalState() {
+ // Find the root window of the window hierarchy and process
+ // modal state update for the entire branch.
+ HostWindow* root = this;
+ while (HostWindow* const owner = root->GetOwnerWindow()) {
+ root = owner;
+ }
+ root->UpdateModalStateLayer();
+}
+
+void HostWindowDialog::SetFullscreen(
+ bool fullscreen,
+ std::optional<FlutterEngineDisplayId> display_id) {}
+
+bool HostWindowDialog::GetFullscreen() const {
+ return false;
+}
+
+} // namespace flutter
diff --git a/engine/src/flutter/shell/platform/windows/host_window_dialog.h b/engine/src/flutter/shell/platform/windows/host_window_dialog.h
new file mode 100644
index 0000000..0be455b
--- /dev/null
+++ b/engine/src/flutter/shell/platform/windows/host_window_dialog.h
@@ -0,0 +1,46 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_DIALOG_H_
+#define FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_DIALOG_H_
+
+#include "host_window.h"
+
+namespace flutter {
+class HostWindowDialog : public HostWindow {
+ public:
+ // Creates a dialog window.
+ //
+ // If |owner_window| is not null, the dialog will be modal to the owner.
+ // This also affects the dialog window's styling.
+ HostWindowDialog(WindowManager* window_manager,
+ FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints,
+ LPCWSTR title,
+ std::optional<HWND> const& owner_window);
+
+ void SetFullscreen(bool fullscreen,
+ std::optional<FlutterEngineDisplayId> display_id) override;
+ bool GetFullscreen() const override;
+
+ protected:
+ LRESULT HandleMessage(HWND hwnd,
+ UINT message,
+ WPARAM wparam,
+ LPARAM lparam) override;
+
+ private:
+ // Enforces modal behavior. This favors enabling most recently created
+ // modal window higest up in the window hierarchy.
+ void UpdateModalState();
+
+ static Rect GetInitialRect(FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints,
+ std::optional<HWND> const& owner_window);
+};
+} // namespace flutter
+
+#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_DIALOG_H_
diff --git a/engine/src/flutter/shell/platform/windows/host_window_regular.cc b/engine/src/flutter/shell/platform/windows/host_window_regular.cc
new file mode 100644
index 0000000..6f5458a
--- /dev/null
+++ b/engine/src/flutter/shell/platform/windows/host_window_regular.cc
@@ -0,0 +1,44 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/windows/host_window_regular.h"
+
+#include "flutter/shell/platform/windows/flutter_windows_engine.h"
+
+namespace flutter {
+HostWindowRegular::HostWindowRegular(WindowManager* window_manager,
+ FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints,
+ LPCWSTR title)
+
+ : HostWindow(window_manager,
+ engine,
+ WindowArchetype::kRegular,
+ WS_OVERLAPPEDWINDOW,
+ 0,
+ constraints,
+ GetInitialRect(engine, preferred_size, constraints),
+ title,
+ std::nullopt) {
+ // TODO(knopp): Investigate sizing the window to its content with the help of
+ // https://github.com/flutter/flutter/pull/173610.
+ FML_CHECK(preferred_size.has_preferred_view_size);
+}
+
+Rect HostWindowRegular::GetInitialRect(FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints) {
+ std::optional<Size> const window_size =
+ HostWindow::GetWindowSizeForClientSize(
+ *engine->windows_proc_table(),
+ Size(preferred_size.preferred_view_width,
+ preferred_size.preferred_view_height),
+ constraints.smallest(), constraints.biggest(), WS_OVERLAPPEDWINDOW, 0,
+ nullptr);
+ return {{CW_USEDEFAULT, CW_USEDEFAULT},
+ window_size ? *window_size : Size{CW_USEDEFAULT, CW_USEDEFAULT}};
+}
+
+} // namespace flutter
diff --git a/engine/src/flutter/shell/platform/windows/host_window_regular.h b/engine/src/flutter/shell/platform/windows/host_window_regular.h
new file mode 100644
index 0000000..4118283
--- /dev/null
+++ b/engine/src/flutter/shell/platform/windows/host_window_regular.h
@@ -0,0 +1,27 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_REGULAR_H_
+#define FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_REGULAR_H_
+
+#include "host_window.h"
+
+namespace flutter {
+class HostWindowRegular : public HostWindow {
+ public:
+ // Creates a regular window.
+ HostWindowRegular(WindowManager* window_manager,
+ FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints,
+ LPCWSTR title);
+
+ private:
+ static Rect GetInitialRect(FlutterWindowsEngine* engine,
+ const WindowSizeRequest& preferred_size,
+ const BoxConstraints& constraints);
+};
+} // namespace flutter
+
+#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_REGULAR_H_
diff --git a/engine/src/flutter/shell/platform/windows/window_manager.cc b/engine/src/flutter/shell/platform/windows/window_manager.cc
index 012e889..93fdccd 100644
--- a/engine/src/flutter/shell/platform/windows/window_manager.cc
+++ b/engine/src/flutter/shell/platform/windows/window_manager.cc
@@ -32,7 +32,7 @@
}
FlutterViewId WindowManager::CreateRegularWindow(
- const WindowCreationRequest* request) {
+ const RegularWindowCreationRequest* request) {
auto window = HostWindow::CreateRegularWindow(
this, engine_, request->preferred_size, request->preferred_constraints,
request->title);
@@ -45,6 +45,20 @@
return view_id;
}
+FlutterViewId WindowManager::CreateDialogWindow(
+ const DialogWindowCreationRequest* request) {
+ auto window = HostWindow::CreateDialogWindow(
+ this, engine_, request->preferred_size, request->preferred_constraints,
+ request->title, request->parent_or_null);
+ if (!window || !window->GetWindowHandle()) {
+ FML_LOG(ERROR) << "Failed to create host window";
+ return -1;
+ }
+ FlutterViewId const view_id = window->view_controller_->view()->view_id();
+ active_windows_[window->GetWindowHandle()] = std::move(window);
+ return view_id;
+}
+
void WindowManager::OnEngineShutdown() {
// Don't send any more messages to isolate.
on_message_ = nullptr;
@@ -116,12 +130,21 @@
FlutterViewId InternalFlutterWindows_WindowManager_CreateRegularWindow(
int64_t engine_id,
- const flutter::WindowCreationRequest* request) {
+ const flutter::RegularWindowCreationRequest* request) {
flutter::FlutterWindowsEngine* engine =
flutter::FlutterWindowsEngine::GetEngineForId(engine_id);
return engine->window_manager()->CreateRegularWindow(request);
}
+FLUTTER_EXPORT
+FlutterViewId InternalFlutterWindows_WindowManager_CreateDialogWindow(
+ int64_t engine_id,
+ const flutter::DialogWindowCreationRequest* request) {
+ flutter::FlutterWindowsEngine* engine =
+ flutter::FlutterWindowsEngine::GetEngineForId(engine_id);
+ return engine->window_manager()->CreateDialogWindow(request);
+}
+
HWND InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(
int64_t engine_id,
FlutterViewId view_id) {
diff --git a/engine/src/flutter/shell/platform/windows/window_manager.h b/engine/src/flutter/shell/platform/windows/window_manager.h
index 9cad1d5..c37ba56 100644
--- a/engine/src/flutter/shell/platform/windows/window_manager.h
+++ b/engine/src/flutter/shell/platform/windows/window_manager.h
@@ -39,12 +39,19 @@
};
// Sent by the framework to request a new window be created.
-struct WindowCreationRequest {
+struct RegularWindowCreationRequest {
WindowSizeRequest preferred_size;
WindowConstraints preferred_constraints;
LPCWSTR title;
};
+struct DialogWindowCreationRequest {
+ WindowSizeRequest preferred_size;
+ WindowConstraints preferred_constraints;
+ LPCWSTR title;
+ HWND parent_or_null;
+};
+
struct WindowsMessage {
FlutterViewId view_id;
HWND hwnd;
@@ -83,7 +90,10 @@
bool HasTopLevelWindows() const;
- FlutterViewId CreateRegularWindow(const WindowCreationRequest* request);
+ FlutterViewId CreateRegularWindow(
+ const RegularWindowCreationRequest* request);
+
+ FlutterViewId CreateDialogWindow(const DialogWindowCreationRequest* request);
// Message handler called by |HostWindow::WndProc| to process window
// messages before delegating them to the host window. This allows the
@@ -128,7 +138,12 @@
FLUTTER_EXPORT
FlutterViewId InternalFlutterWindows_WindowManager_CreateRegularWindow(
int64_t engine_id,
- const flutter::WindowCreationRequest* request);
+ const flutter::RegularWindowCreationRequest* request);
+
+FLUTTER_EXPORT
+FlutterViewId InternalFlutterWindows_WindowManager_CreateDialogWindow(
+ int64_t engine_id,
+ const flutter::DialogWindowCreationRequest* request);
// Retrives the HWND associated with this |engine_id| and |view_id|. Returns
// NULL if the HWND cannot be found
diff --git a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc
index cc0e0c9..72ecf3b 100644
--- a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc
+++ b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc
@@ -44,12 +44,14 @@
int64_t engine_id() { return reinterpret_cast<int64_t>(engine_.get()); }
flutter::Isolate& isolate() { return *isolate_; }
- WindowCreationRequest* creation_request() { return &creation_request_; }
+ RegularWindowCreationRequest* regular_creation_request() {
+ return ®ular_creation_request_;
+ }
private:
std::unique_ptr<FlutterWindowsEngine> engine_;
std::optional<flutter::Isolate> isolate_;
- WindowCreationRequest creation_request_{
+ RegularWindowCreationRequest regular_creation_request_{
.preferred_size =
{
.has_preferred_view_size = true,
@@ -73,7 +75,7 @@
InternalFlutterWindows_WindowManager_Initialize(engine_id(), &init_request);
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
DestroyWindow(InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(
engine_id(), view_id));
@@ -87,8 +89,8 @@
InternalFlutterWindows_WindowManager_HasTopLevelWindows(engine_id());
EXPECT_FALSE(has_top_level_windows);
- InternalFlutterWindows_WindowManager_CreateRegularWindow(engine_id(),
- creation_request());
+ InternalFlutterWindows_WindowManager_CreateRegularWindow(
+ engine_id(), regular_creation_request());
has_top_level_windows =
InternalFlutterWindows_WindowManager_HasTopLevelWindows(engine_id());
EXPECT_TRUE(has_top_level_windows);
@@ -99,7 +101,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
EXPECT_EQ(view_id, 0);
}
@@ -108,7 +110,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -120,7 +122,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -129,9 +131,9 @@
InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle);
EXPECT_EQ(size.width,
- creation_request()->preferred_size.preferred_view_width);
+ regular_creation_request()->preferred_size.preferred_view_width);
EXPECT_EQ(size.height,
- creation_request()->preferred_size.preferred_view_height);
+ regular_creation_request()->preferred_size.preferred_view_height);
}
TEST_F(WindowManagerTest, SetWindowSize) {
@@ -139,7 +141,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -164,7 +166,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -187,7 +189,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -210,7 +212,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -233,7 +235,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -257,7 +259,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -288,7 +290,7 @@
const int64_t view_id =
InternalFlutterWindows_WindowManager_CreateRegularWindow(
- engine_id(), creation_request());
+ engine_id(), regular_creation_request());
const HWND window_handle =
InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
view_id);
@@ -313,5 +315,79 @@
EXPECT_EQ(actual_size.height, 500);
}
+TEST_F(WindowManagerTest, CreateModelessDialogWindow) {
+ IsolateScope isolate_scope(isolate());
+ DialogWindowCreationRequest creation_request{
+ .preferred_size = {.has_preferred_view_size = true,
+ .preferred_view_width = 800,
+ .preferred_view_height = 600},
+ .preferred_constraints = {.has_view_constraints = false},
+ .title = L"Hello World",
+ .parent_or_null = nullptr};
+ const int64_t view_id =
+ InternalFlutterWindows_WindowManager_CreateDialogWindow(
+ engine_id(), &creation_request);
+ EXPECT_EQ(view_id, 0);
+}
+
+TEST_F(WindowManagerTest, CreateModalDialogWindow) {
+ IsolateScope isolate_scope(isolate());
+
+ const int64_t parent_view_id =
+ InternalFlutterWindows_WindowManager_CreateRegularWindow(
+ engine_id(), regular_creation_request());
+ const HWND parent_window_handle =
+ InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(
+ engine_id(), parent_view_id);
+
+ DialogWindowCreationRequest creation_request{
+ .preferred_size =
+ {
+ .has_preferred_view_size = true,
+ .preferred_view_width = 800,
+ .preferred_view_height = 600,
+ },
+ .preferred_constraints = {.has_view_constraints = false},
+ .title = L"Hello World",
+ .parent_or_null = parent_window_handle};
+
+ const int64_t view_id =
+ InternalFlutterWindows_WindowManager_CreateDialogWindow(
+ engine_id(), &creation_request);
+ EXPECT_EQ(view_id, 1);
+
+ const HWND window_handle =
+ InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
+ view_id);
+ HostWindow* host_window = HostWindow::GetThisFromHandle(window_handle);
+ EXPECT_EQ(host_window->GetOwnerWindow()->GetWindowHandle(),
+ parent_window_handle);
+}
+
+TEST_F(WindowManagerTest, DialogCanNeverBeFullscreen) {
+ IsolateScope isolate_scope(isolate());
+
+ DialogWindowCreationRequest creation_request{
+ .preferred_size =
+ {
+ .has_preferred_view_size = true,
+ .preferred_view_width = 800,
+ .preferred_view_height = 600,
+ },
+ };
+
+ const int64_t view_id =
+ InternalFlutterWindows_WindowManager_CreateDialogWindow(
+ engine_id(), &creation_request);
+ const HWND window_handle =
+ InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(),
+ view_id);
+
+ FullscreenRequest request{.fullscreen = true, .has_display_id = false};
+ InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request);
+ EXPECT_FALSE(
+ InternalFlutterWindows_WindowManager_GetFullscreen(window_handle));
+}
+
} // namespace testing
} // namespace flutter
diff --git a/examples/multiple_windows/lib/app/dialog_window_content.dart b/examples/multiple_windows/lib/app/dialog_window_content.dart
new file mode 100644
index 0000000..ae60166
--- /dev/null
+++ b/examples/multiple_windows/lib/app/dialog_window_content.dart
@@ -0,0 +1,83 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/material.dart';
+import 'models.dart';
+import 'window_content.dart';
+import 'package:flutter/src/widgets/_window.dart';
+
+class DialogWindowContent extends StatelessWidget {
+ const DialogWindowContent({super.key, required this.window});
+
+ final DialogWindowController window;
+
+ @override
+ Widget build(BuildContext context) {
+ final WindowManager windowManager = WindowManagerAccessor.of(context);
+
+ final child = FocusScope(
+ autofocus: true,
+ child: Scaffold(
+ appBar: AppBar(title: Text('Dialog')),
+ body: SingleChildScrollView(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ ListenableBuilder(
+ listenable: window,
+ builder: (BuildContext context, Widget? _) {
+ final dpr = MediaQuery.of(context).devicePixelRatio;
+ final windowSize = WindowScope.contentSizeOf(context);
+ return Text(
+ 'View ID: ${window.rootView.viewId}\n'
+ 'Parent View ID: ${window.parent?.rootView.viewId ?? "None"}\n'
+ 'Size: ${(windowSize.width).toStringAsFixed(1)}\u00D7${(windowSize.height).toStringAsFixed(1)}\n'
+ 'Device Pixel Ratio: $dpr',
+ textAlign: TextAlign.center,
+ );
+ },
+ ),
+ const SizedBox(height: 16.0),
+ ElevatedButton(
+ onPressed: () {
+ window.destroy();
+ },
+ child: const Text('Close'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ return ViewAnchor(
+ view: ListenableBuilder(
+ listenable: windowManager,
+ builder: (BuildContext context, Widget? child) {
+ final List<Widget> childViews = <Widget>[];
+ for (final KeyedWindow window in windowManager.windows) {
+ if (window.parent == window.controller) {
+ childViews.add(
+ WindowContent(
+ controller: window.controller,
+ windowKey: window.key,
+ onDestroyed: () => windowManager.remove(window.key),
+ onError: () => windowManager.remove(window.key),
+ ),
+ );
+ }
+ }
+
+ return ViewCollection(views: childViews);
+ },
+ ),
+ child: child,
+ );
+ }
+}
diff --git a/examples/multiple_windows/lib/app/dialog_window_edit_dialog.dart b/examples/multiple_windows/lib/app/dialog_window_edit_dialog.dart
new file mode 100644
index 0000000..52e1c25
--- /dev/null
+++ b/examples/multiple_windows/lib/app/dialog_window_edit_dialog.dart
@@ -0,0 +1,160 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/_window.dart';
+
+void showDialogWindowEditDialog({
+ required BuildContext context,
+ required DialogWindowController controller,
+}) {
+ showDialog(
+ context: context,
+ builder: (context) => _DialogWindowEditDialog(
+ controller: controller,
+ onClose: () => Navigator.pop(context),
+ ),
+ );
+}
+
+class _DialogWindowEditDialog extends StatefulWidget {
+ const _DialogWindowEditDialog({
+ required this.controller,
+ required this.onClose,
+ });
+
+ final DialogWindowController controller;
+ final VoidCallback onClose;
+
+ @override
+ State<StatefulWidget> createState() => _DialogWindowEditDialogState();
+}
+
+class _DialogWindowEditDialogState extends State<_DialogWindowEditDialog> {
+ late Size initialSize;
+ late String initialTitle;
+ late bool initialMinimized;
+
+ late final TextEditingController widthController;
+ late final TextEditingController heightController;
+ late final TextEditingController titleController;
+
+ bool? nextIsMinimized;
+
+ void _init() {
+ widget.controller.addListener(_onNotification);
+ initialSize = widget.controller.contentSize;
+ initialTitle = widget.controller.title;
+ initialMinimized = widget.controller.isMinimized;
+
+ widthController = TextEditingController(text: initialSize.width.toString());
+ heightController = TextEditingController(
+ text: initialSize.height.toString(),
+ );
+ titleController = TextEditingController(text: initialTitle);
+ nextIsMinimized = null;
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _init();
+ }
+
+ @override
+ void didUpdateWidget(covariant _DialogWindowEditDialog oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.controller != widget.controller) {
+ oldWidget.controller.removeListener(_onNotification);
+ _init();
+ }
+ }
+
+ void _onNotification() {
+ // We listen on the state of the controller. If a value that the user
+ // can edit changes from what it was initially set to, we invalidate
+ // their current change and store the new "initial" value.
+ if (widget.controller.contentSize != initialSize) {
+ initialSize = widget.controller.contentSize;
+ widthController.text = widget.controller.contentSize.width.toString();
+ heightController.text = widget.controller.contentSize.height.toString();
+ }
+ if (widget.controller.isMinimized != initialMinimized) {
+ setState(() {
+ initialMinimized = widget.controller.isMinimized;
+ nextIsMinimized = null;
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.controller.removeListener(_onNotification);
+ widthController.dispose();
+ heightController.dispose();
+ titleController.dispose();
+ super.dispose();
+ }
+
+ void _onSave() {
+ double? width = double.tryParse(widthController.text);
+ double? height = double.tryParse(heightController.text);
+ String? title = titleController.text.isEmpty ? null : titleController.text;
+ if (width != null &&
+ height != null &&
+ (width != initialSize.width || height != initialSize.height)) {
+ widget.controller.setSize(Size(width, height));
+ }
+ if (title != null && title != initialTitle) {
+ widget.controller.setTitle(title);
+ }
+ if (nextIsMinimized != null && nextIsMinimized != initialMinimized) {
+ widget.controller.setMinimized(nextIsMinimized!);
+ }
+
+ widget.onClose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text('Edit Window Properties'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ controller: widthController,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(labelText: 'Width'),
+ ),
+ TextField(
+ controller: heightController,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(labelText: 'Height'),
+ ),
+ TextField(
+ controller: titleController,
+ decoration: InputDecoration(labelText: 'Title'),
+ ),
+ CheckboxListTile(
+ title: const Text('Minimized'),
+ value: nextIsMinimized ?? initialMinimized,
+ onChanged: (bool? value) {
+ if (value != null) {
+ setState(() => nextIsMinimized = value);
+ }
+ },
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(onPressed: widget.onClose, child: Text('Cancel')),
+ TextButton(onPressed: _onSave, child: Text('Save')),
+ ],
+ );
+ }
+}
diff --git a/examples/multiple_windows/lib/app/main_window.dart b/examples/multiple_windows/lib/app/main_window.dart
index 9d42ab2..cbcd19f 100644
--- a/examples/multiple_windows/lib/app/main_window.dart
+++ b/examples/multiple_windows/lib/app/main_window.dart
@@ -12,6 +12,7 @@
import 'window_settings_dialog.dart';
import 'models.dart';
import 'regular_window_edit_dialog.dart';
+import 'dialog_window_edit_dialog.dart';
class MainWindow extends StatelessWidget {
const MainWindow({super.key});
@@ -96,7 +97,10 @@
context: context,
controller: regular,
),
- DialogWindowController() => throw UnimplementedError(),
+ final DialogWindowController dialog => showDialogWindowEditDialog(
+ context: context,
+ controller: dialog,
+ ),
};
}
@@ -173,6 +177,25 @@
child: const Text('Regular'),
),
const SizedBox(height: 8),
+ OutlinedButton(
+ onPressed: () async {
+ final UniqueKey key = UniqueKey();
+ windowManager.add(
+ KeyedWindow(
+ key: key,
+ controller: DialogWindowController(
+ delegate: CallbackDialogWindowControllerDelegate(
+ onDestroyed: () => windowManager.remove(key),
+ ),
+ title: 'Modeless Dialog',
+ preferredSize: windowSettings.dialogSize,
+ ),
+ ),
+ );
+ },
+ child: const Text('Modeless Dialog'),
+ ),
+ const SizedBox(height: 8),
Container(
alignment: Alignment.bottomRight,
child: TextButton(
diff --git a/examples/multiple_windows/lib/app/models.dart b/examples/multiple_windows/lib/app/models.dart
index 2465c21..5800338 100644
--- a/examples/multiple_windows/lib/app/models.dart
+++ b/examples/multiple_windows/lib/app/models.dart
@@ -63,10 +63,16 @@
/// Settings that control the behavior of newly created windows.
class WindowSettings {
- WindowSettings({this.regularSize = const Size(400, 300)});
+ WindowSettings({
+ this.regularSize = const Size(400, 300),
+ this.dialogSize = const Size(200, 200),
+ });
/// The initial size for newly created regular windows.
Size regularSize;
+
+ /// The initial size of the dialog window.
+ Size dialogSize;
}
/// Provides access to the [WindowSettings] from the widget tree.
diff --git a/examples/multiple_windows/lib/app/regular_window_content.dart b/examples/multiple_windows/lib/app/regular_window_content.dart
index 150c1cc..0324087 100644
--- a/examples/multiple_windows/lib/app/regular_window_content.dart
+++ b/examples/multiple_windows/lib/app/regular_window_content.dart
@@ -69,6 +69,26 @@
child: const Text('Create Regular Window'),
),
const SizedBox(height: 20),
+ ElevatedButton(
+ onPressed: () {
+ final UniqueKey key = UniqueKey();
+ windowManager.add(
+ KeyedWindow(
+ key: key,
+ controller: DialogWindowController(
+ preferredSize: windowSettings.dialogSize,
+ delegate: CallbackDialogWindowControllerDelegate(
+ onDestroyed: () => windowManager.remove(key),
+ ),
+ parent: window,
+ title: 'Dialog',
+ ),
+ ),
+ );
+ },
+ child: const Text('Create Modal Dialog'),
+ ),
+ const SizedBox(height: 20),
Text(
'View #${window.rootView.viewId}\n'
'Size: ${(windowSize.width).toStringAsFixed(1)}\u00D7${(windowSize.height).toStringAsFixed(1)}\n'
@@ -120,3 +140,16 @@
final VoidCallback onDestroyed;
}
+
+class CallbackDialogWindowControllerDelegate
+ with DialogWindowControllerDelegate {
+ CallbackDialogWindowControllerDelegate({required this.onDestroyed});
+
+ @override
+ void onWindowDestroyed() {
+ onDestroyed();
+ super.onWindowDestroyed();
+ }
+
+ final VoidCallback onDestroyed;
+}
diff --git a/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart b/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart
index 7a5a16f..63e0a9e 100644
--- a/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart
+++ b/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart
@@ -47,7 +47,7 @@
bool? nextIsFullscreen;
bool? nextIsMaximized;
- bool? nextIsMinized;
+ bool? nextIsMinimized;
void _init() {
widget.controller.addListener(_onNotification);
@@ -64,7 +64,7 @@
titleController = TextEditingController(text: initialTitle);
nextIsFullscreen = null;
nextIsMaximized = null;
- nextIsMinized = null;
+ nextIsMinimized = null;
}
@override
@@ -106,7 +106,7 @@
if (widget.controller.isMinimized != initialMinimized) {
setState(() {
initialMinimized = widget.controller.isMinimized;
- nextIsMinized = null;
+ nextIsMinimized = null;
});
}
}
@@ -114,6 +114,9 @@
@override
void dispose() {
widget.controller.removeListener(_onNotification);
+ widthController.dispose();
+ heightController.dispose();
+ titleController.dispose();
super.dispose();
}
@@ -135,8 +138,8 @@
if (nextIsMaximized != null && nextIsMaximized != initialMaximized) {
widget.controller.setMaximized(nextIsMaximized!);
}
- if (nextIsMinized != null && nextIsMinized != initialMinimized) {
- widget.controller.setMinimized(nextIsMinized!);
+ if (nextIsMinimized != null && nextIsMinimized != initialMinimized) {
+ widget.controller.setMinimized(nextIsMinimized!);
}
widget.onClose();
@@ -183,10 +186,10 @@
),
CheckboxListTile(
title: const Text('Minimized'),
- value: nextIsMinized ?? initialMinimized,
+ value: nextIsMinimized ?? initialMinimized,
onChanged: (bool? value) {
if (value != null) {
- setState(() => nextIsMinized = value);
+ setState(() => nextIsMinimized = value);
}
},
),
diff --git a/examples/multiple_windows/lib/app/window_content.dart b/examples/multiple_windows/lib/app/window_content.dart
index 880b552..cc30e03 100644
--- a/examples/multiple_windows/lib/app/window_content.dart
+++ b/examples/multiple_windows/lib/app/window_content.dart
@@ -6,6 +6,7 @@
// ignore_for_file: implementation_imports
import 'package:flutter/material.dart';
+import 'dialog_window_content.dart';
import 'regular_window_content.dart';
import 'package:flutter/src/widgets/_window.dart';
@@ -33,7 +34,11 @@
controller: regular,
child: MaterialApp(home: RegularWindowContent(window: regular)),
),
- DialogWindowController() => throw UnimplementedError(),
+ final DialogWindowController dialog => DialogWindow(
+ key: windowKey,
+ controller: dialog,
+ child: MaterialApp(home: DialogWindowContent(window: dialog)),
+ ),
};
}
}
diff --git a/examples/multiple_windows/lib/app/window_settings_dialog.dart b/examples/multiple_windows/lib/app/window_settings_dialog.dart
index 4e31dfc..f6774d8 100644
--- a/examples/multiple_windows/lib/app/window_settings_dialog.dart
+++ b/examples/multiple_windows/lib/app/window_settings_dialog.dart
@@ -35,6 +35,8 @@
final TextEditingController _regularWidthController = TextEditingController();
final TextEditingController _regularHeightController =
TextEditingController();
+ final TextEditingController _dialogWidthController = TextEditingController();
+ final TextEditingController _dialogHeightController = TextEditingController();
@override
void initState() {
@@ -42,6 +44,8 @@
_regularWidthController.text = widget.settings.regularSize.width.toString();
_regularHeightController.text = widget.settings.regularSize.height
.toString();
+ _dialogWidthController.text = widget.settings.dialogSize.width.toString();
+ _dialogHeightController.text = widget.settings.dialogSize.height.toString();
}
@override
@@ -73,6 +77,28 @@
],
),
),
+ ListTile(
+ title: const Text('Dialog'),
+ subtitle: Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ controller: _dialogWidthController,
+ decoration: const InputDecoration(labelText: 'Initial width'),
+ ),
+ ),
+ const SizedBox(width: 20),
+ Expanded(
+ child: TextFormField(
+ controller: _dialogHeightController,
+ decoration: const InputDecoration(
+ labelText: 'Initial height',
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextButton(
@@ -83,6 +109,12 @@
double.tryParse(_regularHeightController.text) ??
widget.settings.regularSize.height,
);
+ widget.settings.dialogSize = Size(
+ double.tryParse(_dialogWidthController.text) ??
+ widget.settings.dialogSize.width,
+ double.tryParse(_dialogHeightController.text) ??
+ widget.settings.dialogSize.height,
+ );
widget.onClose();
},
child: const Text('Apply'),
diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart
index 46647a6..505b1c2 100644
--- a/packages/flutter/lib/src/widgets/_window_win32.dart
+++ b/packages/flutter/lib/src/widgets/_window_win32.dart
@@ -30,6 +30,7 @@
@internal
typedef HWND = ffi.Pointer<ffi.Void>;
+const int _WM_DESTROY = 0x0002;
const int _WM_SIZE = 0x0005;
const int _WM_ACTIVATE = 0x0006;
const int _WM_CLOSE = 0x0010;
@@ -157,7 +158,14 @@
BaseWindowController? parent,
String? title,
}) {
- throw UnimplementedError('Dialog windows are not yet implemented on Windows.');
+ return DialogWindowControllerWin32(
+ owner: this,
+ delegate: delegate,
+ preferredSize: preferredSize,
+ preferredConstraints: preferredConstraints,
+ title: title,
+ parent: parent,
+ );
}
/// Register a new [WindowsMessageHandler].
@@ -270,13 +278,17 @@
_handler = _RegularWindowMesageHandler(controller: this);
owner._addMessageHandler(_handler);
- final int viewId = _Win32PlatformInterface.createWindow(
+ final int viewId = _Win32PlatformInterface.createRegularWindow(
_owner.allocator,
PlatformDispatcher.instance.engineId!,
preferredSize,
preferredConstraints,
title,
);
+ if (viewId < 0) {
+ throw Exception('Windows failed to create a regular window with a valid view id.');
+ }
+
final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere(
(FlutterView view) => view.viewId == viewId,
);
@@ -418,8 +430,6 @@
}
_Win32PlatformInterface.destroyWindow(getWindowHandle());
_destroyed = true;
- _owner._removeMessageHandler(_handler);
- _delegate.onWindowDestroyed();
}
int? _handleWindowsMessage(
@@ -436,6 +446,225 @@
if (message == _WM_CLOSE) {
_delegate.onWindowCloseRequested(this);
return 0;
+ } else if (message == _WM_DESTROY) {
+ _destroyed = true;
+ _owner._removeMessageHandler(_handler);
+ _delegate.onWindowDestroyed();
+ return 0;
+ } else if (message == _WM_SIZE || message == _WM_ACTIVATE) {
+ notifyListeners();
+ }
+ return null;
+ }
+}
+
+class _DialogWindowMesageHandler implements _WindowsMessageHandler {
+ _DialogWindowMesageHandler({required this.controller});
+
+ final DialogWindowControllerWin32 controller;
+
+ @override
+ int? handleWindowsMessage(
+ FlutterView view,
+ HWND windowHandle,
+ int message,
+ int wParam,
+ int lParam,
+ ) {
+ return controller._handleWindowsMessage(view, windowHandle, message, wParam, lParam);
+ }
+}
+
+/// Implementation of [DialogWindowController] for the Windows platform.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+///
+/// See also:
+///
+/// * [DialogWindowController], the base class for dialog windows.
+class DialogWindowControllerWin32 extends DialogWindowController {
+ /// Creates a new dialog window controller for Win32.
+ ///
+ /// When this constructor completes the native window has been created and
+ /// has a view associated with it.
+ ///
+ /// {@macro flutter.widgets.windowing.experimental}
+ ///
+ /// See also:
+ ///
+ /// * [DialogWindowController], the base class for dialog windows.
+ @internal
+ DialogWindowControllerWin32({
+ required WindowingOwnerWin32 owner,
+ required DialogWindowControllerDelegate delegate,
+ Size? preferredSize,
+ BoxConstraints? preferredConstraints,
+ String? title,
+ BaseWindowController? parent,
+ }) : _owner = owner,
+ _delegate = delegate,
+ _parent = parent,
+ super.empty() {
+ if (!isWindowingEnabled) {
+ throw UnsupportedError(_kWindowingDisabledErrorMessage);
+ }
+
+ _handler = _DialogWindowMesageHandler(controller: this);
+ owner._addMessageHandler(_handler);
+ final int viewId = _Win32PlatformInterface.createDialogWindow(
+ _owner.allocator,
+ PlatformDispatcher.instance.engineId!,
+ preferredSize,
+ preferredConstraints,
+ title,
+ parent != null
+ ? _Win32PlatformInterface.getWindowHandle(
+ PlatformDispatcher.instance.engineId!,
+ parent.rootView.viewId,
+ )
+ : null,
+ );
+ if (viewId < 0) {
+ throw Exception('Windows failed to create a dialog window with a valid view id.');
+ }
+
+ final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere(
+ (FlutterView view) => view.viewId == viewId,
+ );
+ rootView = flutterView;
+ }
+
+ final WindowingOwnerWin32 _owner;
+ final DialogWindowControllerDelegate _delegate;
+ final BaseWindowController? _parent;
+ late final _DialogWindowMesageHandler _handler;
+ bool _destroyed = false;
+
+ @override
+ @internal
+ Size get contentSize {
+ _ensureNotDestroyed();
+ final _ActualContentSize size = _Win32PlatformInterface.getWindowContentSize(getWindowHandle());
+ final Size result = Size(size.width, size.height);
+ return result;
+ }
+
+ @override
+ @internal
+ String get title {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.getWindowTitle(_owner.allocator, getWindowHandle());
+ }
+
+ @override
+ @internal
+ bool get isActivated {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.getForegroundWindow() == getWindowHandle();
+ }
+
+ @override
+ @internal
+ bool get isMinimized {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.isIconic(getWindowHandle()) != 0;
+ }
+
+ @override
+ @internal
+ void setSize(Size? size) {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.setWindowContentSize(_owner.allocator, getWindowHandle(), size);
+ // Note that we do not notify the listener when setting the size,
+ // as that will happen when the WM_SIZE message is received in
+ // _handleWindowsMessage.
+ }
+
+ @override
+ @internal
+ void setConstraints(BoxConstraints constraints) {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.setWindowConstraints(_owner.allocator, getWindowHandle(), constraints);
+ notifyListeners();
+ }
+
+ @override
+ @internal
+ void setTitle(String title) {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.setWindowTitle(_owner.allocator, getWindowHandle(), title);
+ notifyListeners();
+ }
+
+ @override
+ @internal
+ void activate() {
+ _ensureNotDestroyed();
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE);
+ }
+
+ @override
+ @internal
+ void setMinimized(bool minimized) {
+ if (parent != null) {
+ return;
+ }
+
+ _ensureNotDestroyed();
+ if (minimized) {
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_MINIMIZE);
+ } else {
+ _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE);
+ }
+ }
+
+ @override
+ @internal
+ BaseWindowController? get parent => _parent;
+
+ /// Returns HWND pointer to the top level window.
+ @internal
+ HWND getWindowHandle() {
+ _ensureNotDestroyed();
+ return _Win32PlatformInterface.getWindowHandle(
+ PlatformDispatcher.instance.engineId!,
+ rootView.viewId,
+ );
+ }
+
+ void _ensureNotDestroyed() {
+ if (_destroyed) {
+ throw StateError('Window has been destroyed.');
+ }
+ }
+
+ @override
+ void destroy() {
+ if (_destroyed) {
+ return;
+ }
+ _Win32PlatformInterface.destroyWindow(getWindowHandle());
+ }
+
+ int? _handleWindowsMessage(
+ FlutterView view,
+ HWND windowHandle,
+ int message,
+ int wParam,
+ int lParam,
+ ) {
+ if (view.viewId != rootView.viewId) {
+ return null;
+ }
+
+ if (message == _WM_CLOSE) {
+ _delegate.onWindowCloseRequested(this);
+ return 0;
+ } else if (message == _WM_DESTROY) {
+ _destroyed = true;
+ _owner._removeMessageHandler(_handler);
+ _delegate.onWindowDestroyed();
+ return 0;
} else if (message == _WM_SIZE || message == _WM_ACTIVATE) {
notifyListeners();
}
@@ -474,28 +703,61 @@
ffi.Pointer<_WindowingInitRequest> request,
);
- static int createWindow(
+ static int createRegularWindow(
ffi.Allocator allocator,
int engineId,
Size? preferredSize,
BoxConstraints? preferredConstraints,
String? title,
) {
- final ffi.Pointer<_WindowCreationRequest> request = allocator<_WindowCreationRequest>();
+ final ffi.Pointer<_RegularWindowCreationRequest> request =
+ allocator<_RegularWindowCreationRequest>();
try {
request.ref.preferredSize.from(preferredSize);
request.ref.preferredConstraints.from(preferredConstraints);
request.ref.title = (title ?? 'Regular window').toNativeUtf16(allocator: allocator);
- return _createWindow(engineId, request);
+ return _createRegularWindow(engineId, request);
} finally {
allocator.free(request);
}
}
- @ffi.Native<ffi.Int64 Function(ffi.Int64, ffi.Pointer<_WindowCreationRequest>)>(
+ @ffi.Native<ffi.Int64 Function(ffi.Int64, ffi.Pointer<_RegularWindowCreationRequest>)>(
symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow',
)
- external static int _createWindow(int engineId, ffi.Pointer<_WindowCreationRequest> request);
+ external static int _createRegularWindow(
+ int engineId,
+ ffi.Pointer<_RegularWindowCreationRequest> request,
+ );
+
+ static int createDialogWindow(
+ ffi.Allocator allocator,
+ int engineId,
+ Size? preferredSize,
+ BoxConstraints? preferredConstraints,
+ String? title,
+ HWND? parent,
+ ) {
+ final ffi.Pointer<_DialogWindowCreationRequest> request =
+ allocator<_DialogWindowCreationRequest>();
+ try {
+ request.ref.preferredSize.from(preferredSize);
+ request.ref.preferredConstraints.from(preferredConstraints);
+ request.ref.title = (title ?? 'Dialog window').toNativeUtf16(allocator: allocator);
+ request.ref.parentOrNull = parent ?? ffi.Pointer<ffi.Void>.fromAddress(0);
+ return _createDialogWindow(engineId, request);
+ } finally {
+ allocator.free(request);
+ }
+ }
+
+ @ffi.Native<ffi.Int64 Function(ffi.Int64, ffi.Pointer<_DialogWindowCreationRequest>)>(
+ symbol: 'InternalFlutterWindows_WindowManager_CreateDialogWindow',
+ )
+ external static int _createDialogWindow(
+ int engineId,
+ ffi.Pointer<_DialogWindowCreationRequest> request,
+ );
@ffi.Native<HWND Function(ffi.Int64, ffi.Int64)>(
symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle',
@@ -629,13 +891,21 @@
external static HWND getForegroundWindow();
}
-/// Payload for the creation method used by [_Win32PlatformInterface.createWindow].
-final class _WindowCreationRequest extends ffi.Struct {
+/// Payload for the creation method used by [_Win32PlatformInterface.createRegularWindow].
+final class _RegularWindowCreationRequest extends ffi.Struct {
external _WindowSizeRequest preferredSize;
external _WindowConstraintsRequest preferredConstraints;
external ffi.Pointer<_Utf16> title;
}
+/// Payload for the creation method used by [_Win32PlatformInterface.createDialogWindow].
+final class _DialogWindowCreationRequest extends ffi.Struct {
+ external _WindowSizeRequest preferredSize;
+ external _WindowConstraintsRequest preferredConstraints;
+ external ffi.Pointer<_Utf16> title;
+ external HWND parentOrNull;
+}
+
/// Payload for the initialization request for the windowing subsystem used
/// by the constructor for [WindowingOwnerWin32].
final class _WindowingInitRequest extends ffi.Struct {
@@ -643,7 +913,7 @@
onMessage;
}
-/// Payload for the size of a window used by [_WindowCreationRequest] and
+/// Payload for the size of a window used by [_RegularWindowCreationRequest] and
/// [_Win32PlatformInterface.setWindowContentSize].
final class _WindowSizeRequest extends ffi.Struct {
@ffi.Bool()
@@ -662,7 +932,7 @@
}
}
-/// Payload for the constraints of a window used by [_WindowCreationRequest] and
+/// Payload for the constraints of a window used by [_RegularWindowCreationRequest] and
/// [_Win32PlatformInterface.setWindowConstraints].
final class _WindowConstraintsRequest extends ffi.Struct {
@ffi.Bool()