mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Implements the framework side of secure paste milestone 2, where the iOS system context menu items can be customized. Depends on PR https://github.com/flutter/flutter/pull/161103. Currently I've merged that PR into this one for testing, but I think that PR should merge separately first. ### Widget API (most users) ```dart TextField( contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { return SystemContextMenu.editableText( editableTextState: editableTextState, // items is optional items: <IOSSystemContextMenuItem>[ const IOSSystemContextMenuItemCut(), constIOS SystemContextMenuItemCopy(), const IOSSystemContextMenuItemPaste(), const IOSSystemContextMenuItemSelectAll(), const IOSSystemContextMenuItemSearchWeb( title: 'Search!', // title is optional for this button, defaults to localized string ), // Milestone 3: IOSSystemContextMenuItemCustom( // title and onPressed are required title: 'custom button', onPressed: () { print('pressed the custom button.'); } ), ], ); }, ), ``` ### Raw Controller API ```dart _systemContextMenuController.show( widget.anchor, <IOSSystemContextMenuItemData>[ // Notice these are different classes than those used for the widget. That's // mainly because I can't provide localized defaults here, so the titles are // required in the classes that have titles. const IOSSystemContextMenuItemDataCut(), const IOSSystemContextMenuItemDataCopy(), const IOSSystemContextMenuItemDataPaste(), const IOSSystemContextMenuItemDataSelectAll(), const IOSSystemContextMenuItemDataSearchWeb( title: 'Search!', // title is required. ), // Milestone 3: IOSSystemContextMenuItemDataCustom( // title and onPressed are required as before. title: 'custom button', onPressed: () { print('pressed the custom button.'); } ), ], ); ``` <details> <summary>Json format</summary> ```dart return _channel.invokeMethod<Map<String, dynamic>>( 'ContextMenu.showSystemContextMenu', <String, dynamic>{ 'targetRect': <String, double>{ 'x': targetRect.left, 'y': targetRect.top, 'width': targetRect.width, 'height': targetRect.height, }, 'items': <dynamic>[ <String, dynamic>{ 'type': 'default', 'action': 'paste', }, <String, dynamic>{ 'type': 'default', 'action': 'copy', }, <String, dynamic>{ 'type': 'default', 'title': 'Crazy Title', 'action': 'share', }, ], }, ); ``` </summary> </details> ### Localization changes This change requires the SystemContextMenu widget in the widgets library to be able to look up the default localized label for several context menu buttons like "Copy", etc. Those strings previously resided in MaterialLocalizations and CupertinoLocalizations, but not in WidgetsLocalizations, so I have copied the necessary strings into WidgetsLocalizations. --------- Co-authored-by: Huan Lin <hellohuanlin@gmail.com>
122 lines
3.9 KiB
Dart
122 lines
3.9 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 'dart:async';
|
|
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
final TestAutomatedTestWidgetsFlutterBinding binding = TestAutomatedTestWidgetsFlutterBinding();
|
|
|
|
testWidgets('English translations exist for all WidgetsLocalizations properties', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const WidgetsLocalizations localizations = DefaultWidgetsLocalizations();
|
|
|
|
expect(localizations.reorderItemUp, isNotNull);
|
|
expect(localizations.reorderItemDown, isNotNull);
|
|
expect(localizations.reorderItemLeft, isNotNull);
|
|
expect(localizations.reorderItemRight, isNotNull);
|
|
expect(localizations.reorderItemToEnd, isNotNull);
|
|
expect(localizations.reorderItemToStart, isNotNull);
|
|
expect(localizations.copyButtonLabel, isNotNull);
|
|
expect(localizations.cutButtonLabel, isNotNull);
|
|
expect(localizations.pasteButtonLabel, isNotNull);
|
|
expect(localizations.selectAllButtonLabel, isNotNull);
|
|
expect(localizations.lookUpButtonLabel, isNotNull);
|
|
expect(localizations.searchWebButtonLabel, isNotNull);
|
|
expect(localizations.shareButtonLabel, isNotNull);
|
|
});
|
|
|
|
testWidgets('Locale is available when Localizations widget stops deferring frames', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FakeLocalizationsDelegate delegate = FakeLocalizationsDelegate();
|
|
await tester.pumpWidget(
|
|
Localizations(
|
|
locale: const Locale('fo'),
|
|
delegates: <LocalizationsDelegate<dynamic>>[WidgetsLocalizationsDelegate(), delegate],
|
|
child: const Text('loaded'),
|
|
),
|
|
);
|
|
final dynamic state = tester.state(find.byType(Localizations));
|
|
// ignore: avoid_dynamic_calls
|
|
expect(state!.locale, isNull);
|
|
expect(find.text('loaded'), findsNothing);
|
|
|
|
late Locale locale;
|
|
binding.onAllowFrame = () {
|
|
// ignore: avoid_dynamic_calls
|
|
locale = state.locale as Locale;
|
|
};
|
|
delegate.completer.complete('foo');
|
|
await tester.idle();
|
|
expect(locale, const Locale('fo'));
|
|
await tester.pump();
|
|
expect(find.text('loaded'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Localizations.localeOf throws when no localizations exist', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final GlobalKey contextKey = GlobalKey(debugLabel: 'Test Key');
|
|
await tester.pumpWidget(Container(key: contextKey));
|
|
|
|
expect(
|
|
() => Localizations.localeOf(contextKey.currentContext!),
|
|
throwsA(
|
|
isAssertionError.having(
|
|
(AssertionError e) => e.message,
|
|
'message',
|
|
contains('does not include a Localizations ancestor'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Localizations.maybeLocaleOf returns null when no localizations exist', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final GlobalKey contextKey = GlobalKey(debugLabel: 'Test Key');
|
|
await tester.pumpWidget(Container(key: contextKey));
|
|
|
|
expect(Localizations.maybeLocaleOf(contextKey.currentContext!), isNull);
|
|
});
|
|
}
|
|
|
|
class FakeLocalizationsDelegate extends LocalizationsDelegate<String> {
|
|
final Completer<String> completer = Completer<String>();
|
|
|
|
@override
|
|
bool isSupported(Locale locale) => true;
|
|
|
|
@override
|
|
Future<String> load(Locale locale) => completer.future;
|
|
|
|
@override
|
|
bool shouldReload(LocalizationsDelegate<String> old) => false;
|
|
}
|
|
|
|
class TestAutomatedTestWidgetsFlutterBinding extends AutomatedTestWidgetsFlutterBinding {
|
|
VoidCallback? onAllowFrame;
|
|
|
|
@override
|
|
void allowFirstFrame() {
|
|
onAllowFrame?.call();
|
|
super.allowFirstFrame();
|
|
}
|
|
}
|
|
|
|
class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
|
|
@override
|
|
bool isSupported(Locale locale) => true;
|
|
|
|
@override
|
|
Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);
|
|
|
|
@override
|
|
bool shouldReload(WidgetsLocalizationsDelegate old) => false;
|
|
}
|