Skip to content

Commit fe409cf

Browse files
authored
Implement dialog windows for the win32 platform (#176309)
## What's new? - Implemented `DialogWindowControllerWin32` for win32 dialogs 🚀 - 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.
1 parent 666634b commit fe409cf

22 files changed

Lines changed: 1444 additions & 217 deletions

dev/integration_tests/windowing_test/lib/main.dart

Lines changed: 93 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class _MainRegularWindowControllerDelegate
2323
}
2424

2525
late final RegularWindowController controller;
26+
final ValueNotifier<DialogWindowController?> dialogController = ValueNotifier(
27+
null,
28+
);
2629

2730
void main() {
2831
final Completer<void> windowCreated = Completer();
@@ -61,7 +64,9 @@ void main() {
6164
controller.removeListener(notificationHandler);
6265
}
6366

64-
if (jsonMap['type'] == 'get_size') {
67+
if (jsonMap['type'] == 'ping') {
68+
return jsonEncode({'type': 'pong'});
69+
} else if (jsonMap['type'] == 'get_size') {
6570
return jsonEncode({
6671
'width': controller.contentSize.width,
6772
'height': controller.contentSize.height,
@@ -129,6 +134,23 @@ void main() {
129134
}, () => controller.isActivated);
130135
} else if (jsonMap['type'] == 'get_activated') {
131136
return jsonEncode({'isActivated': controller.isActivated});
137+
} else if (jsonMap['type'] == 'open_dialog') {
138+
if (dialogController.value != null) {
139+
return jsonEncode({'result': false});
140+
}
141+
dialogController.value = DialogWindowController(
142+
preferredSize: const Size(200, 200),
143+
parent: controller,
144+
delegate: MyDialogWindowControllerDelegate(
145+
onDestroyed: () {
146+
dialogController.value = null;
147+
},
148+
),
149+
);
150+
return jsonEncode({'result': true});
151+
} else if (jsonMap['type'] == 'close_dialog') {
152+
dialogController.value?.destroy();
153+
return jsonEncode({'result': true});
132154
} else {
133155
throw ArgumentError('Unknown message type: ${jsonMap['type']}');
134156
}
@@ -169,38 +191,82 @@ class MyHomePage extends StatefulWidget {
169191
State<MyHomePage> createState() => _MyHomePageState();
170192
}
171193

172-
class _MyHomePageState extends State<MyHomePage> {
173-
int _counter = 0;
194+
class MyDialogWindowControllerDelegate extends DialogWindowControllerDelegate {
195+
MyDialogWindowControllerDelegate({required this.onDestroyed});
174196

175-
void _incrementCounter() {
176-
setState(() {
177-
_counter++;
178-
});
197+
final VoidCallback onDestroyed;
198+
199+
@override
200+
void onWindowDestroyed() {
201+
onDestroyed();
202+
super.onWindowDestroyed();
179203
}
204+
}
180205

206+
class _MyHomePageState extends State<MyHomePage> {
181207
@override
182208
Widget build(BuildContext context) {
183-
return Scaffold(
184-
appBar: AppBar(
185-
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
186-
title: Text(widget.title),
187-
),
188-
body: Center(
189-
child: Column(
190-
mainAxisAlignment: MainAxisAlignment.center,
191-
children: <Widget>[
192-
const Text('You have pushed the button this many times:'),
193-
Text(
194-
'$_counter',
195-
style: Theme.of(context).textTheme.headlineMedium,
196-
),
197-
],
209+
return ValueListenableBuilder(
210+
valueListenable: dialogController,
211+
builder:
212+
(
213+
BuildContext context,
214+
DialogWindowController? dialogController,
215+
Widget? child,
216+
) {
217+
return ViewAnchor(
218+
view: dialogController != null
219+
? DialogWindow(
220+
controller: dialogController,
221+
child: MyDialogPage(controller: dialogController),
222+
)
223+
: null,
224+
child: Scaffold(
225+
appBar: AppBar(
226+
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
227+
title: Text(widget.title),
228+
),
229+
body: Center(
230+
child: Column(
231+
mainAxisAlignment: MainAxisAlignment.center,
232+
children: <Widget>[const Text('This is the main window.')],
233+
),
234+
),
235+
),
236+
);
237+
},
238+
);
239+
}
240+
}
241+
242+
class MyDialogPage extends StatelessWidget {
243+
const MyDialogPage({super.key, required this.controller});
244+
245+
final DialogWindowController controller;
246+
247+
@override
248+
Widget build(BuildContext context) {
249+
return MaterialApp(
250+
home: Scaffold(
251+
appBar: AppBar(
252+
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
253+
title: Text('Dialog'),
254+
),
255+
body: Center(
256+
child: Column(
257+
mainAxisAlignment: MainAxisAlignment.center,
258+
children: <Widget>[
259+
const Text('This is a dialog window.'),
260+
ElevatedButton(
261+
key: const ValueKey<String>('close_dialog'),
262+
onPressed: () {
263+
controller.destroy();
264+
},
265+
child: Text('Close Dialog'),
266+
),
267+
],
268+
),
198269
),
199-
),
200-
floatingActionButton: FloatingActionButton(
201-
onPressed: _incrementCounter,
202-
tooltip: 'Increment',
203-
child: const Icon(Icons.add),
204270
),
205271
);
206272
}

dev/integration_tests/windowing_test/test_driver/main_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ void main() {
1313

1414
setUpAll(() async {
1515
driver = await FlutterDriver.connect();
16+
await driver.requestData(jsonEncode({'type': 'ping'}));
1617
});
1718

1819
tearDownAll(() async {
@@ -168,5 +169,16 @@ void main() {
168169
timeout: Timeout.none,
169170
onPlatform: {'linux': Skip('isMinimized is not supported on Wayland')},
170171
);
172+
173+
test(
174+
'Can open dialog',
175+
() async {
176+
await driver.requestData(jsonEncode({'type': 'open_dialog'}));
177+
await driver.waitFor(find.byValueKey('close_dialog'));
178+
await driver.requestData(jsonEncode({'type': 'close_dialog'}));
179+
},
180+
timeout: Timeout.none,
181+
onPlatform: {'linux': Skip('Dialogs are not yet supported on Wayland')},
182+
);
171183
});
172184
}

engine/src/flutter/shell/platform/common/windowing.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
namespace flutter {
99

1010
// Types of windows.
11-
// The value must match value from WindowType in the Dart code
12-
// in packages/flutter/lib/src/widgets/window.dart
1311
enum class WindowArchetype {
1412
// Regular top-level window.
1513
kRegular,
14+
15+
// Dialog window.
16+
kDialog,
1617
};
1718

1819
} // namespace flutter

engine/src/flutter/shell/platform/windows/BUILD.gn

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ source_set("flutter_windows_source") {
9393
"flutter_windows_view_controller.h",
9494
"host_window.cc",
9595
"host_window.h",
96+
"host_window_dialog.cc",
97+
"host_window_dialog.h",
98+
"host_window_regular.cc",
99+
"host_window_regular.h",
96100
"keyboard_handler_base.h",
97101
"keyboard_key_channel_handler.cc",
98102
"keyboard_key_channel_handler.h",

0 commit comments

Comments
 (0)