mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
## 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.
274 lines
8.8 KiB
Dart
274 lines
8.8 KiB
Dart
// 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
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/src/widgets/_window.dart';
|
|
import 'package:flutter_driver/driver_extension.dart';
|
|
|
|
class _MainRegularWindowControllerDelegate
|
|
extends RegularWindowControllerDelegate {
|
|
@override
|
|
void onWindowDestroyed() {
|
|
super.onWindowDestroyed();
|
|
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
late final RegularWindowController controller;
|
|
final ValueNotifier<DialogWindowController?> dialogController = ValueNotifier(
|
|
null,
|
|
);
|
|
|
|
void main() {
|
|
final Completer<void> windowCreated = Completer();
|
|
enableFlutterDriverExtension(
|
|
handler: (String? message) async {
|
|
await windowCreated.future;
|
|
if (message == null) {
|
|
return '';
|
|
}
|
|
|
|
final jsonMap = jsonDecode(message);
|
|
if (!jsonMap.containsKey('type')) {
|
|
throw ArgumentError('Message must contain a "type" field.');
|
|
}
|
|
|
|
/// This helper method registers a listener on the controller,
|
|
/// calls [act] to perform some action on the controller, waits for
|
|
/// the [predicate] to be satisified, and finally cleans up the listener.
|
|
Future<void> awaitNotification(
|
|
VoidCallback act,
|
|
bool Function() predicate,
|
|
) async {
|
|
final StreamController<bool> streamController = StreamController();
|
|
void notificationHandler() {
|
|
streamController.add(true);
|
|
}
|
|
|
|
controller.addListener(notificationHandler);
|
|
|
|
act();
|
|
await for (final _ in streamController.stream) {
|
|
if (predicate()) {
|
|
break;
|
|
}
|
|
}
|
|
controller.removeListener(notificationHandler);
|
|
}
|
|
|
|
if (jsonMap['type'] == 'ping') {
|
|
return jsonEncode({'type': 'pong'});
|
|
} else if (jsonMap['type'] == 'get_size') {
|
|
return jsonEncode({
|
|
'width': controller.contentSize.width,
|
|
'height': controller.contentSize.height,
|
|
});
|
|
} else if (jsonMap['type'] == 'set_size') {
|
|
final Size size = Size(
|
|
jsonMap['width'].toDouble(),
|
|
jsonMap['height'].toDouble(),
|
|
);
|
|
await awaitNotification(() {
|
|
controller.setSize(size);
|
|
}, () => controller.contentSize == size);
|
|
} else if (jsonMap['type'] == 'set_constraints') {
|
|
final BoxConstraints constraints = BoxConstraints(
|
|
minWidth: jsonMap['min_width'].toDouble(),
|
|
minHeight: jsonMap['min_height'].toDouble(),
|
|
maxWidth: jsonMap['max_width'].toDouble(),
|
|
maxHeight: jsonMap['max_height'].toDouble(),
|
|
);
|
|
// We assume that this will cause a resize, which the current tests do.
|
|
final initialSize = controller.contentSize;
|
|
await awaitNotification(() {
|
|
controller.setConstraints(constraints);
|
|
}, () => controller.contentSize != initialSize);
|
|
} else if (jsonMap['type'] == 'set_fullscreen') {
|
|
await awaitNotification(() {
|
|
controller.setFullscreen(true);
|
|
}, () => controller.isFullscreen);
|
|
} else if (jsonMap['type'] == 'unset_fullscreen') {
|
|
await awaitNotification(() {
|
|
controller.setFullscreen(false);
|
|
}, () => !controller.isFullscreen);
|
|
} else if (jsonMap['type'] == 'get_fullscreen') {
|
|
return jsonEncode({'isFullscreen': controller.isFullscreen});
|
|
} else if (jsonMap['type'] == 'set_maximized') {
|
|
await awaitNotification(() {
|
|
controller.setMaximized(true);
|
|
}, () => controller.isMaximized);
|
|
} else if (jsonMap['type'] == 'unset_maximized') {
|
|
await awaitNotification(() {
|
|
controller.setMaximized(false);
|
|
}, () => !controller.isMaximized);
|
|
} else if (jsonMap['type'] == 'get_maximized') {
|
|
return jsonEncode({'isMaximized': controller.isMaximized});
|
|
} else if (jsonMap['type'] == 'set_minimized') {
|
|
await awaitNotification(() {
|
|
controller.setMinimized(true);
|
|
}, () => controller.isMinimized);
|
|
} else if (jsonMap['type'] == 'unset_minimized') {
|
|
await awaitNotification(() {
|
|
controller.setMinimized(false);
|
|
}, () => !controller.isMinimized);
|
|
} else if (jsonMap['type'] == 'get_minimized') {
|
|
return jsonEncode({'isMinimized': controller.isMinimized});
|
|
} else if (jsonMap['type'] == 'set_title') {
|
|
final String title = jsonMap['title'];
|
|
await awaitNotification(() {
|
|
controller.setTitle(title);
|
|
}, () => controller.title == title);
|
|
} else if (jsonMap['type'] == 'get_title') {
|
|
return jsonEncode({'title': controller.title});
|
|
} else if (jsonMap['type'] == 'set_activated') {
|
|
await awaitNotification(() {
|
|
controller.activate();
|
|
}, () => 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']}');
|
|
}
|
|
return '';
|
|
},
|
|
);
|
|
controller = RegularWindowController(
|
|
preferredSize: Size(640, 480),
|
|
title: 'Integration Test',
|
|
delegate: _MainRegularWindowControllerDelegate(),
|
|
);
|
|
windowCreated.complete();
|
|
|
|
runWidget(RegularWindow(controller: controller, child: MyApp()));
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Flutter Demo',
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
|
),
|
|
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MyHomePage extends StatefulWidget {
|
|
const MyHomePage({super.key, required this.title});
|
|
|
|
final String title;
|
|
|
|
@override
|
|
State<MyHomePage> createState() => _MyHomePageState();
|
|
}
|
|
|
|
class MyDialogWindowControllerDelegate extends DialogWindowControllerDelegate {
|
|
MyDialogWindowControllerDelegate({required this.onDestroyed});
|
|
|
|
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 MaterialApp(
|
|
home: Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
|
title: Text('Dialog'),
|
|
),
|
|
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'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|