mirror of
https://github.com/flutter/flutter.git
synced 2026-02-11 21:33:08 +08:00
* Can show context menus anywhere in the app, not just on text. * Unifies all desktop/mobile context menus to go through one class (ContextMenuController). * All context menus are now just plain widgets that can be fully customized. * Existing default context menus can be customized and reused.
264 lines
7.5 KiB
Dart
264 lines
7.5 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.
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'clipboard_utils.dart';
|
|
import 'editable_text_utils.dart';
|
|
|
|
void main() {
|
|
final MockClipboard mockClipboard = MockClipboard();
|
|
TestWidgetsFlutterBinding.ensureInitialized()
|
|
.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
|
|
|
|
setUp(() async {
|
|
// Fill the clipboard so that the Paste option is available in the text
|
|
// selection menu.
|
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
|
});
|
|
|
|
testWidgets('Hides and shows only a single menu', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey();
|
|
final GlobalKey key2 = GlobalKey();
|
|
late final BuildContext context;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (BuildContext localContext) {
|
|
context = localContext;
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byKey(key1), findsNothing);
|
|
expect(find.byKey(key2), findsNothing);
|
|
|
|
final ContextMenuController controller1 = ContextMenuController();
|
|
await tester.pump();
|
|
expect(find.byKey(key1), findsNothing);
|
|
expect(find.byKey(key2), findsNothing);
|
|
|
|
controller1.show(
|
|
context: context,
|
|
contextMenuBuilder: (BuildContext context) {
|
|
return Placeholder(key: key1);
|
|
},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(key1), findsOneWidget);
|
|
expect(find.byKey(key2), findsNothing);
|
|
|
|
// Showing the same thing again does nothing and is not an error.
|
|
controller1.show(
|
|
context: context,
|
|
contextMenuBuilder: (BuildContext context) {
|
|
return Placeholder(key: key1);
|
|
},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(tester.takeException(), null);
|
|
expect(find.byKey(key1), findsOneWidget);
|
|
expect(find.byKey(key2), findsNothing);
|
|
|
|
// Showing a new menu hides the first.
|
|
final ContextMenuController controller2 = ContextMenuController();
|
|
controller2.show(
|
|
context: context,
|
|
contextMenuBuilder: (BuildContext context) {
|
|
return Placeholder(key: key2);
|
|
},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(key1), findsNothing);
|
|
expect(find.byKey(key2), findsOneWidget);
|
|
|
|
controller2.remove();
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(key1), findsNothing);
|
|
expect(find.byKey(key2), findsNothing);
|
|
});
|
|
|
|
testWidgets('A menu can be hidden and then reshown', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey();
|
|
late final BuildContext context;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (BuildContext localContext) {
|
|
context = localContext;
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byKey(key1), findsNothing);
|
|
|
|
final ContextMenuController controller = ContextMenuController();
|
|
|
|
// Instantiating the controller does not shown it.
|
|
await tester.pump();
|
|
expect(find.byKey(key1), findsNothing);
|
|
|
|
controller.show(
|
|
context: context,
|
|
contextMenuBuilder: (BuildContext context) {
|
|
return Placeholder(key: key1);
|
|
},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(key1), findsOneWidget);
|
|
|
|
controller.remove();
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(key1), findsNothing);
|
|
|
|
controller.show(
|
|
context: context,
|
|
contextMenuBuilder: (BuildContext context) {
|
|
return Placeholder(key: key1);
|
|
},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(key1), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('markNeedsBuild causes the builder to update', (WidgetTester tester) async {
|
|
int buildCount = 0;
|
|
late final BuildContext context;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (BuildContext localContext) {
|
|
context = localContext;
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final ContextMenuController controller = ContextMenuController();
|
|
controller.show(
|
|
context: context,
|
|
contextMenuBuilder: (BuildContext context) {
|
|
buildCount++;
|
|
return const Placeholder();
|
|
},
|
|
);
|
|
expect(buildCount, 0);
|
|
await tester.pump();
|
|
expect(buildCount, 1);
|
|
|
|
controller.markNeedsBuild();
|
|
expect(buildCount, 1);
|
|
await tester.pump();
|
|
expect(buildCount, 2);
|
|
|
|
controller.remove();
|
|
});
|
|
|
|
testWidgets('Calling show when a built-in widget is already showing its context menu hides the built-in menu', (WidgetTester tester) async {
|
|
final GlobalKey builtInKey = GlobalKey();
|
|
final GlobalKey directKey = GlobalKey();
|
|
late final BuildContext context;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (BuildContext localContext) {
|
|
context = localContext;
|
|
return EditableText(
|
|
controller: TextEditingController(),
|
|
backgroundCursorColor: Colors.grey,
|
|
focusNode: FocusNode(),
|
|
style: const TextStyle(),
|
|
cursorColor: Colors.red,
|
|
selectionControls: materialTextSelectionHandleControls,
|
|
contextMenuBuilder: (
|
|
BuildContext context,
|
|
EditableTextState editableTextState,
|
|
) {
|
|
return Placeholder(key: builtInKey);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byKey(builtInKey), findsNothing);
|
|
expect(find.byKey(directKey), findsNothing);
|
|
|
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
|
|
|
await tester.tapAt(textOffsetToPosition(tester, 0));
|
|
await tester.pump();
|
|
expect(state.showToolbar(), true);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(builtInKey), findsOneWidget);
|
|
expect(find.byKey(directKey), findsNothing);
|
|
|
|
final ContextMenuController controller = ContextMenuController();
|
|
controller.show(
|
|
context: context,
|
|
contextMenuBuilder: (BuildContext context) {
|
|
return Placeholder(key: directKey);
|
|
},
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(builtInKey), findsNothing);
|
|
expect(find.byKey(directKey), findsOneWidget);
|
|
expect(controller.isShown, isTrue);
|
|
|
|
// And showing the built-in menu hides the directly shown menu.
|
|
expect(state.showToolbar(), isTrue);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(builtInKey), findsOneWidget);
|
|
expect(find.byKey(directKey), findsNothing);
|
|
expect(controller.isShown, isFalse);
|
|
|
|
// Calling remove on the hidden ContextMenuController does not hide the
|
|
// built-in menu.
|
|
controller.remove();
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(builtInKey), findsOneWidget);
|
|
expect(find.byKey(directKey), findsNothing);
|
|
expect(controller.isShown, isFalse);
|
|
|
|
state.hideToolbar();
|
|
await tester.pump();
|
|
expect(find.byKey(builtInKey), findsNothing);
|
|
expect(find.byKey(directKey), findsNothing);
|
|
expect(controller.isShown, isFalse);
|
|
},
|
|
skip: isContextMenuProvidedByPlatform, // [intended] no Flutter-drawn text selection toolbar on web.
|
|
);
|
|
}
|