mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Context Menus (#107193)
* 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.
This commit is contained in:
parent
ef1236e038
commit
0b451b6dfd
@ -0,0 +1,159 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This sample demonstrates allowing a context menu to be shown in a widget
|
||||||
|
// subtree in response to user gestures.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
/// A builder that includes an Offset to draw the context menu at.
|
||||||
|
typedef ContextMenuBuilder = Widget Function(BuildContext context, Offset offset);
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
void _showDialog (BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
DialogRoute<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) =>
|
||||||
|
const AlertDialog(title: Text('You clicked print!')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Context menu outside of text'),
|
||||||
|
),
|
||||||
|
body: _ContextMenuRegion(
|
||||||
|
contextMenuBuilder: (BuildContext context, Offset offset) {
|
||||||
|
// The custom context menu will look like the default context menu
|
||||||
|
// on the current platform with a single 'Print' button.
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: offset,
|
||||||
|
),
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
_showDialog(context);
|
||||||
|
},
|
||||||
|
label: 'Print',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// In this case this wraps a big open space in a GestureDetector in
|
||||||
|
// order to show the context menu, but it could also wrap a single
|
||||||
|
// wiget like an Image to give it a context menu.
|
||||||
|
child: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: 20.0),
|
||||||
|
const Text('Right click or long press anywhere (not just on this text!) to show the custom menu.'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows and hides the context menu based on user gestures.
|
||||||
|
///
|
||||||
|
/// By default, shows the menu on right clicks and long presses.
|
||||||
|
class _ContextMenuRegion extends StatefulWidget {
|
||||||
|
/// Creates an instance of [_ContextMenuRegion].
|
||||||
|
const _ContextMenuRegion({
|
||||||
|
required this.child,
|
||||||
|
required this.contextMenuBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Builds the context menu.
|
||||||
|
final ContextMenuBuilder contextMenuBuilder;
|
||||||
|
|
||||||
|
/// The child widget that will be listened to for gestures.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ContextMenuRegion> createState() => _ContextMenuRegionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContextMenuRegionState extends State<_ContextMenuRegion> {
|
||||||
|
Offset? _longPressOffset;
|
||||||
|
|
||||||
|
final ContextMenuController _contextMenuController = ContextMenuController();
|
||||||
|
|
||||||
|
static bool get _longPressEnabled {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return true;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSecondaryTapUp(TapUpDetails details) {
|
||||||
|
_show(details.globalPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTap() {
|
||||||
|
if (!_contextMenuController.isShown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressStart(LongPressStartDetails details) {
|
||||||
|
_longPressOffset = details.globalPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPress() {
|
||||||
|
assert(_longPressOffset != null);
|
||||||
|
_show(_longPressOffset!);
|
||||||
|
_longPressOffset = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _show(Offset position) {
|
||||||
|
_contextMenuController.show(
|
||||||
|
context: context,
|
||||||
|
contextMenuBuilder: (BuildContext context) {
|
||||||
|
return widget.contextMenuBuilder(context, position);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hide() {
|
||||||
|
_contextMenuController.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hide();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onSecondaryTapUp: _onSecondaryTapUp,
|
||||||
|
onTap: _onTap,
|
||||||
|
onLongPress: _longPressEnabled ? _onLongPress : null,
|
||||||
|
onLongPressStart: _longPressEnabled ? _onLongPressStart : null,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This example demonstrates showing the default buttons, but customizing their
|
||||||
|
// appearance.
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(MyApp());
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
MyApp({super.key});
|
||||||
|
|
||||||
|
final TextEditingController _controller = TextEditingController(
|
||||||
|
text: 'Right click or long press to see the menu with custom buttons.',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Custom button appearance'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(height: 20.0),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return AdaptiveTextSelectionToolbar(
|
||||||
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
|
// Build the default buttons, but make them look custom.
|
||||||
|
// In a real project you may want to build different
|
||||||
|
// buttons depending on the platform.
|
||||||
|
children: editableTextState.contextMenuButtonItems.map((ContextMenuButtonItem buttonItem) {
|
||||||
|
return CupertinoButton(
|
||||||
|
borderRadius: null,
|
||||||
|
color: const Color(0xffaaaa00),
|
||||||
|
disabledColor: const Color(0xffaaaaff),
|
||||||
|
onPressed: buttonItem.onPressed,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
pressedOpacity: 0.7,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200.0,
|
||||||
|
child: Text(
|
||||||
|
CupertinoTextSelectionToolbarButton.getButtonLabel(context, buttonItem),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
|
||||||
|
// This example demonstrates showing a custom context menu only when some
|
||||||
|
// narrowly defined text is selected.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(MyApp());
|
||||||
|
|
||||||
|
const String emailAddress = 'me@example.com';
|
||||||
|
const String text = 'Select the email address and open the menu: $emailAddress';
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
MyApp({super.key});
|
||||||
|
|
||||||
|
final TextEditingController _controller = TextEditingController(
|
||||||
|
text: text,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _showDialog (BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
DialogRoute<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) =>
|
||||||
|
const AlertDialog(title: Text('You clicked send email!')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Custom button for emails'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: 20.0),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
||||||
|
final List<ContextMenuButtonItem> buttonItems =
|
||||||
|
editableTextState.contextMenuButtonItems;
|
||||||
|
// Here we add an "Email" button to the default TextField
|
||||||
|
// context menu for the current platform, but only if an email
|
||||||
|
// address is currently selected.
|
||||||
|
final TextEditingValue value = _controller.value;
|
||||||
|
if (_isValidEmail(value.selection.textInside(value.text))) {
|
||||||
|
buttonItems.insert(0, ContextMenuButtonItem(
|
||||||
|
label: 'Send email',
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
_showDialog(context);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
|
buttonItems: buttonItems,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isValidEmail(String text) {
|
||||||
|
return RegExp(
|
||||||
|
r'(?<name>[a-zA-Z0-9]+)'
|
||||||
|
r'@'
|
||||||
|
r'(?<domain>[a-zA-Z0-9]+)'
|
||||||
|
r'\.'
|
||||||
|
r'(?<topLevelDomain>[a-zA-Z0-9]+)',
|
||||||
|
).hasMatch(text);
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This example demonstrates how to create a custom toolbar that retains the
|
||||||
|
// look of the default buttons for the current platform.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(MyApp());
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
MyApp({super.key});
|
||||||
|
|
||||||
|
final TextEditingController _controller = TextEditingController(
|
||||||
|
text: 'Right click or long press to see the menu with a custom toolbar.',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Custom toolbar, default-looking buttons'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(height: 20.0),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return _MyTextSelectionToolbar(
|
||||||
|
anchor: editableTextState.contextMenuAnchors.primaryAnchor,
|
||||||
|
// getAdaptiveButtons creates the default button widgets for
|
||||||
|
// the current platform.
|
||||||
|
children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
|
||||||
|
context,
|
||||||
|
// These buttons just close the menu when clicked.
|
||||||
|
<ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'One',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Two',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Three',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Four',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Five',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple, yet totally custom, text selection toolbar.
|
||||||
|
///
|
||||||
|
/// Displays its children in a scrollable grid.
|
||||||
|
class _MyTextSelectionToolbar extends StatelessWidget {
|
||||||
|
const _MyTextSelectionToolbar({
|
||||||
|
required this.anchor,
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Offset anchor;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Positioned(
|
||||||
|
top: anchor.dy,
|
||||||
|
left: anchor.dx,
|
||||||
|
child: Container(
|
||||||
|
width: 200.0,
|
||||||
|
height: 200.0,
|
||||||
|
color: Colors.cyanAccent.withOpacity(0.5),
|
||||||
|
child: GridView.count(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
crossAxisCount: 2,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This example demonstrates a custom context menu in non-editable text using
|
||||||
|
// SelectionArea.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
const String text = 'I am some text inside of SelectionArea. Right click or long press me to show the customized context menu.';
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
void _showDialog (BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
DialogRoute<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) =>
|
||||||
|
const AlertDialog(title: Text('You clicked print!')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Context menu anywhere'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200.0,
|
||||||
|
child: SelectionArea(
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
SelectableRegionState selectableRegionState,
|
||||||
|
) {
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: selectableRegionState.contextMenuAnchors,
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
...selectableRegionState.contextMenuButtonItems,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
_showDialog(context);
|
||||||
|
},
|
||||||
|
label: 'Print',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
children: const <Widget>[
|
||||||
|
SizedBox(height: 20.0),
|
||||||
|
Text(text),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
// 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/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_api_samples/material/context_menu/context_menu_controller.0.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('showing and hiding the custom context menu in the whole app', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.MyApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Right clicking the middle of the app shows the custom context menu.
|
||||||
|
final Offset center = tester.getCenter(find.byType(Scaffold));
|
||||||
|
final TestGesture gesture = await tester.startGesture(
|
||||||
|
center,
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.text('Print'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap to dismiss.
|
||||||
|
await tester.tapAt(center);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Long pressing also shows the custom context menu.
|
||||||
|
await tester.longPressAt(center);
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.text('Print'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
// 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/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_api_samples/material/context_menu/editable_text_toolbar_builder.0.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('showing and hiding the context menu in TextField with custom buttons', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
example.MyApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Long pressing the field shows the default context menu but with custom
|
||||||
|
// buttons.
|
||||||
|
await tester.longPress(find.byType(EditableText));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoButton), findsAtLeastNWidgets(1));
|
||||||
|
|
||||||
|
// Tap to dismiss.
|
||||||
|
await tester.tapAt(tester.getTopLeft(find.byType(EditableText)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
// 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/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_api_samples/material/context_menu/editable_text_toolbar_builder.1.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('showing and hiding the custom context menu in TextField with a specific selection', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
example.MyApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Right clicking the Text in the TextField shows the custom context menu,
|
||||||
|
// but no email button since no email address is selected.
|
||||||
|
TestGesture gesture = await tester.startGesture(
|
||||||
|
tester.getTopLeft(find.text(example.text)),
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.text('Send email'), findsNothing);
|
||||||
|
|
||||||
|
// Tap to dismiss.
|
||||||
|
await tester.tapAt(tester.getTopLeft(find.byType(EditableText)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Select the email address.
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
state.updateEditingValue(state.textEditingValue.copyWith(
|
||||||
|
selection: TextSelection(
|
||||||
|
baseOffset: example.text.indexOf(example.emailAddress),
|
||||||
|
extentOffset: example.text.length,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Right clicking the Text in the TextField shows the custom context menu
|
||||||
|
// with the email button.
|
||||||
|
gesture = await tester.startGesture(
|
||||||
|
tester.getCenter(find.text(example.text)),
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.text('Send email'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap to dismiss.
|
||||||
|
await tester.tapAt(tester.getTopLeft(find.byType(EditableText)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
// 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/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_api_samples/material/context_menu/editable_text_toolbar_builder.2.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('showing and hiding the context menu in TextField with a custom toolbar', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
example.MyApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(EditableText));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Long pressing the field shows the custom context menu.
|
||||||
|
await tester.longPress(find.byType(EditableText));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// The buttons use the default widgets but with custom labels.
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsAtLeastNWidgets(1));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsAtLeastNWidgets(1));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsAtLeastNWidgets(1));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsAtLeastNWidgets(1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
expect(find.text('Paste'), findsNothing);
|
||||||
|
expect(find.text('Select all'), findsNothing);
|
||||||
|
|
||||||
|
// Tap to dismiss.
|
||||||
|
await tester.tapAt(tester.getTopLeft(find.byType(EditableText)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsNothing);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
expect(find.text('Paste'), findsNothing);
|
||||||
|
expect(find.text('Select all'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
// 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/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_api_samples/material/context_menu/selectable_region_toolbar_builder.0.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('showing and hiding the custom context menu on SelectionArea', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.MyApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow the selection overlay geometry to be created.
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Right clicking the Text in the SelectionArea shows the custom context
|
||||||
|
// menu.
|
||||||
|
final TestGesture gesture = await tester.startGesture(
|
||||||
|
tester.getCenter(find.text(example.text)),
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
buttons: kSecondaryMouseButton,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.text('Print'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap to dismiss.
|
||||||
|
await tester.tapAt(tester.getCenter(find.byType(Scaffold)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -23,6 +23,7 @@
|
|||||||
library cupertino;
|
library cupertino;
|
||||||
|
|
||||||
export 'src/cupertino/activity_indicator.dart';
|
export 'src/cupertino/activity_indicator.dart';
|
||||||
|
export 'src/cupertino/adaptive_text_selection_toolbar.dart';
|
||||||
export 'src/cupertino/app.dart';
|
export 'src/cupertino/app.dart';
|
||||||
export 'src/cupertino/bottom_tab_bar.dart';
|
export 'src/cupertino/bottom_tab_bar.dart';
|
||||||
export 'src/cupertino/button.dart';
|
export 'src/cupertino/button.dart';
|
||||||
@ -33,6 +34,8 @@ export 'src/cupertino/context_menu_action.dart';
|
|||||||
export 'src/cupertino/date_picker.dart';
|
export 'src/cupertino/date_picker.dart';
|
||||||
export 'src/cupertino/debug.dart';
|
export 'src/cupertino/debug.dart';
|
||||||
export 'src/cupertino/desktop_text_selection.dart';
|
export 'src/cupertino/desktop_text_selection.dart';
|
||||||
|
export 'src/cupertino/desktop_text_selection_toolbar.dart';
|
||||||
|
export 'src/cupertino/desktop_text_selection_toolbar_button.dart';
|
||||||
export 'src/cupertino/dialog.dart';
|
export 'src/cupertino/dialog.dart';
|
||||||
export 'src/cupertino/form_row.dart';
|
export 'src/cupertino/form_row.dart';
|
||||||
export 'src/cupertino/form_section.dart';
|
export 'src/cupertino/form_section.dart';
|
||||||
|
@ -22,6 +22,7 @@ library material;
|
|||||||
|
|
||||||
export 'src/material/about.dart';
|
export 'src/material/about.dart';
|
||||||
export 'src/material/action_chip.dart';
|
export 'src/material/action_chip.dart';
|
||||||
|
export 'src/material/adaptive_text_selection_toolbar.dart';
|
||||||
export 'src/material/animated_icons.dart';
|
export 'src/material/animated_icons.dart';
|
||||||
export 'src/material/app.dart';
|
export 'src/material/app.dart';
|
||||||
export 'src/material/app_bar.dart';
|
export 'src/material/app_bar.dart';
|
||||||
@ -64,6 +65,8 @@ export 'src/material/date.dart';
|
|||||||
export 'src/material/date_picker.dart';
|
export 'src/material/date_picker.dart';
|
||||||
export 'src/material/debug.dart';
|
export 'src/material/debug.dart';
|
||||||
export 'src/material/desktop_text_selection.dart';
|
export 'src/material/desktop_text_selection.dart';
|
||||||
|
export 'src/material/desktop_text_selection_toolbar.dart';
|
||||||
|
export 'src/material/desktop_text_selection_toolbar_button.dart';
|
||||||
export 'src/material/dialog.dart';
|
export 'src/material/dialog.dart';
|
||||||
export 'src/material/dialog_theme.dart';
|
export 'src/material/dialog_theme.dart';
|
||||||
export 'src/material/divider.dart';
|
export 'src/material/divider.dart';
|
||||||
|
@ -0,0 +1,226 @@
|
|||||||
|
// 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/foundation.dart' show defaultTargetPlatform;
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'desktop_text_selection_toolbar.dart';
|
||||||
|
import 'desktop_text_selection_toolbar_button.dart';
|
||||||
|
import 'text_selection_toolbar.dart';
|
||||||
|
import 'text_selection_toolbar_button.dart';
|
||||||
|
|
||||||
|
/// The default Cupertino context menu for text selection for the current
|
||||||
|
/// platform with the given children.
|
||||||
|
///
|
||||||
|
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.platforms}
|
||||||
|
/// Builds the mobile Cupertino context menu on all mobile platforms, not just
|
||||||
|
/// iOS, and builds the desktop Cupertino context menu on all desktop platforms,
|
||||||
|
/// not just MacOS. For a widget that builds the native-looking context menu for
|
||||||
|
/// all platforms, see [AdaptiveTextSelectionToolbar].
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which does the same thing as this widget
|
||||||
|
/// but for all platforms, not just the Cupertino-styled platforms.
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds
|
||||||
|
/// the Cupertino button Widgets for the current platform given
|
||||||
|
/// [ContextMenuButtonItem]s.
|
||||||
|
class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||||
|
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||||
|
/// given [children].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar.buttonItems], which takes a list
|
||||||
|
/// of [ContextMenuButtonItem]s instead of [children] widgets.
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar.editable], which builds the
|
||||||
|
/// default Cupertino children for an editable field.
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar.editableText], which builds the
|
||||||
|
/// default Cupertino children for an [EditableText].
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar.selectable], which builds the
|
||||||
|
/// Cupertino children for content that is selectable but not editable.
|
||||||
|
/// {@endtemplate}
|
||||||
|
const CupertinoAdaptiveTextSelectionToolbar({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
required this.anchors,
|
||||||
|
}) : buttonItems = null;
|
||||||
|
|
||||||
|
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] whose
|
||||||
|
/// children will be built from the given [buttonItems].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar.new], which takes the children
|
||||||
|
/// directly as a list of widgets.
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||||
|
const CupertinoAdaptiveTextSelectionToolbar.buttonItems({
|
||||||
|
super.key,
|
||||||
|
required this.buttonItems,
|
||||||
|
required this.anchors,
|
||||||
|
}) : children = null;
|
||||||
|
|
||||||
|
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||||
|
/// default children for an editable field.
|
||||||
|
///
|
||||||
|
/// If a callback is null, then its corresponding button will not be built.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.editable], which is similar to this but
|
||||||
|
/// includes Material and Cupertino toolbars.
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||||
|
CupertinoAdaptiveTextSelectionToolbar.editable({
|
||||||
|
super.key,
|
||||||
|
required ClipboardStatus clipboardStatus,
|
||||||
|
required VoidCallback? onCopy,
|
||||||
|
required VoidCallback? onCut,
|
||||||
|
required VoidCallback? onPaste,
|
||||||
|
required VoidCallback? onSelectAll,
|
||||||
|
required this.anchors,
|
||||||
|
}) : children = null,
|
||||||
|
buttonItems = EditableText.getEditableButtonItems(
|
||||||
|
clipboardStatus: clipboardStatus,
|
||||||
|
onCopy: onCopy,
|
||||||
|
onCut: onCut,
|
||||||
|
onPaste: onPaste,
|
||||||
|
onSelectAll: onSelectAll,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||||
|
/// default children for an [EditableText].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.editableText], which is similar to this
|
||||||
|
/// but includes Material and Cupertino toolbars.
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||||
|
CupertinoAdaptiveTextSelectionToolbar.editableText({
|
||||||
|
super.key,
|
||||||
|
required EditableTextState editableTextState,
|
||||||
|
}) : children = null,
|
||||||
|
buttonItems = editableTextState.contextMenuButtonItems,
|
||||||
|
anchors = editableTextState.contextMenuAnchors;
|
||||||
|
|
||||||
|
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||||
|
/// default children for selectable, but not editable, content.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.selectable], which is similar to this but
|
||||||
|
/// includes Material and Cupertino toolbars.
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||||
|
CupertinoAdaptiveTextSelectionToolbar.selectable({
|
||||||
|
super.key,
|
||||||
|
required VoidCallback onCopy,
|
||||||
|
required VoidCallback onSelectAll,
|
||||||
|
required SelectionGeometry selectionGeometry,
|
||||||
|
required this.anchors,
|
||||||
|
}) : children = null,
|
||||||
|
buttonItems = SelectableRegion.getSelectableButtonItems(
|
||||||
|
selectionGeometry: selectionGeometry,
|
||||||
|
onCopy: onCopy,
|
||||||
|
onSelectAll: onSelectAll,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.anchors}
|
||||||
|
final TextSelectionToolbarAnchors anchors;
|
||||||
|
|
||||||
|
/// The children of the toolbar, typically buttons.
|
||||||
|
final List<Widget>? children;
|
||||||
|
|
||||||
|
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||||
|
/// widgets for the current platform.
|
||||||
|
final List<ContextMenuButtonItem>? buttonItems;
|
||||||
|
|
||||||
|
/// Returns a List of Widgets generated by turning [buttonItems] into the
|
||||||
|
/// the default context menu buttons for Cupertino on the current platform.
|
||||||
|
///
|
||||||
|
/// This is useful when building a text selection toolbar with the default
|
||||||
|
/// button appearance for the given platform, but where the toolbar and/or the
|
||||||
|
/// button actions and labels may be custom.
|
||||||
|
///
|
||||||
|
/// Does not build Material buttons. On non-Apple platforms, Cupertino buttons
|
||||||
|
/// will still be used, because the Cupertino library does not access the
|
||||||
|
/// Material library. To get the native-looking buttons on every platform, use
|
||||||
|
/// use [AdaptiveTextSelectionToolbar.getAdaptiveButtons] in the Material
|
||||||
|
/// library.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the Material
|
||||||
|
/// equivalent of this class and builds only the Material buttons. It
|
||||||
|
/// includes a live example of using `getAdaptiveButtons`.
|
||||||
|
static Iterable<Widget> getAdaptiveButtons(BuildContext context, List<ContextMenuButtonItem> buttonItems) {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||||
|
return CupertinoTextSelectionToolbarButton.buttonItem(
|
||||||
|
buttonItem: buttonItem,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||||
|
return CupertinoDesktopTextSelectionToolbarButton.buttonItem(
|
||||||
|
buttonItem: buttonItem,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// If there aren't any buttons to build, build an empty toolbar.
|
||||||
|
if ((children?.isEmpty ?? false) || (buttonItems?.isEmpty ?? false)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Widget> resultChildren = children
|
||||||
|
?? getAdaptiveButtons(context, buttonItems!).toList();
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
return CupertinoTextSelectionToolbar(
|
||||||
|
anchorAbove: anchors.primaryAnchor,
|
||||||
|
anchorBelow: anchors.secondaryAnchor ?? anchors.primaryAnchor,
|
||||||
|
children: resultChildren,
|
||||||
|
);
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return CupertinoDesktopTextSelectionToolbar(
|
||||||
|
anchor: anchors.primaryAnchor,
|
||||||
|
children: resultChildren,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,33 +3,18 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' show clampDouble;
|
import 'package:flutter/foundation.dart' show clampDouble;
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'button.dart';
|
import 'desktop_text_selection_toolbar.dart';
|
||||||
import 'colors.dart';
|
import 'desktop_text_selection_toolbar_button.dart';
|
||||||
import 'localizations.dart';
|
import 'localizations.dart';
|
||||||
import 'theme.dart';
|
|
||||||
|
|
||||||
// Minimal padding from all edges of the selection toolbar to all edges of the
|
/// MacOS Cupertino styled text selection handle controls.
|
||||||
// screen.
|
///
|
||||||
const double _kToolbarScreenPadding = 8.0;
|
/// Specifically does not manage the toolbar, which is left to
|
||||||
|
/// [EditableText.contextMenuBuilder].
|
||||||
// These values were measured from a screenshot of TextEdit on MacOS 10.15.7 on
|
class _CupertinoDesktopTextSelectionHandleControls extends CupertinoDesktopTextSelectionControls with TextSelectionHandleControls {
|
||||||
// a Macbook Pro.
|
}
|
||||||
const double _kToolbarWidth = 222.0;
|
|
||||||
const Radius _kToolbarBorderRadius = Radius.circular(4.0);
|
|
||||||
|
|
||||||
// These values were measured from a screenshot of TextEdit on MacOS 10.16 on a
|
|
||||||
// Macbook Pro.
|
|
||||||
const CupertinoDynamicColor _kToolbarBorderColor = CupertinoDynamicColor.withBrightness(
|
|
||||||
color: Color(0xFFBBBBBB),
|
|
||||||
darkColor: Color(0xFF505152),
|
|
||||||
);
|
|
||||||
const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness(
|
|
||||||
color: Color(0xffECE8E6),
|
|
||||||
darkColor: Color(0xff302928),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Desktop Cupertino styled text selection controls.
|
/// Desktop Cupertino styled text selection controls.
|
||||||
///
|
///
|
||||||
@ -42,7 +27,11 @@ class CupertinoDesktopTextSelectionControls extends TextSelectionControls {
|
|||||||
return Size.zero;
|
return Size.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for the Mac-style copy/paste text selection toolbar.
|
/// Builder for the MacOS-style copy/paste text selection toolbar.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
Widget buildToolbar(
|
Widget buildToolbar(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -80,6 +69,10 @@ class CupertinoDesktopTextSelectionControls extends TextSelectionControls {
|
|||||||
return Offset.zero;
|
return Offset.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
void handleSelectAll(TextSelectionDelegate delegate) {
|
void handleSelectAll(TextSelectionDelegate delegate) {
|
||||||
super.handleSelectAll(delegate);
|
super.handleSelectAll(delegate);
|
||||||
@ -87,7 +80,15 @@ class CupertinoDesktopTextSelectionControls extends TextSelectionControls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text selection controls that follows Mac design conventions.
|
/// Text selection handle controls that follow MacOS design conventions.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `cupertinoDesktopTextSelectionControls` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final TextSelectionControls cupertinoDesktopTextSelectionHandleControls =
|
||||||
|
_CupertinoDesktopTextSelectionHandleControls();
|
||||||
|
|
||||||
|
/// Text selection controls that follows MacOS design conventions.
|
||||||
final TextSelectionControls cupertinoDesktopTextSelectionControls =
|
final TextSelectionControls cupertinoDesktopTextSelectionControls =
|
||||||
CupertinoDesktopTextSelectionControls();
|
CupertinoDesktopTextSelectionControls();
|
||||||
|
|
||||||
@ -145,8 +146,8 @@ class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_Cupertin
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -180,7 +181,7 @@ class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_Cupertin
|
|||||||
items.add(onePhysicalPixelVerticalDivider);
|
items.add(onePhysicalPixelVerticalDivider);
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(_CupertinoDesktopTextSelectionToolbarButton.text(
|
items.add(CupertinoDesktopTextSelectionToolbarButton.text(
|
||||||
context: context,
|
context: context,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
text: text,
|
text: text,
|
||||||
@ -206,182 +207,9 @@ class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_Cupertin
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _CupertinoDesktopTextSelectionToolbar(
|
return CupertinoDesktopTextSelectionToolbar(
|
||||||
anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor,
|
anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor,
|
||||||
children: items,
|
children: items,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Mac-style text selection toolbar.
|
|
||||||
///
|
|
||||||
/// Typically displays buttons for text manipulation, e.g. copying and pasting
|
|
||||||
/// text.
|
|
||||||
///
|
|
||||||
/// Tries to position itself as closely as possible to [anchor] while remaining
|
|
||||||
/// fully on-screen.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [TextSelectionControls.buildToolbar], where this is used by default to
|
|
||||||
/// build a Mac-style toolbar.
|
|
||||||
/// * [TextSelectionToolbar], which is similar, but builds an Android-style
|
|
||||||
/// toolbar.
|
|
||||||
class _CupertinoDesktopTextSelectionToolbar extends StatelessWidget {
|
|
||||||
/// Creates an instance of CupertinoTextSelectionToolbar.
|
|
||||||
const _CupertinoDesktopTextSelectionToolbar({
|
|
||||||
required this.anchor,
|
|
||||||
required this.children,
|
|
||||||
}) : assert(children.length > 0);
|
|
||||||
|
|
||||||
/// The point at which the toolbar will attempt to position itself as closely
|
|
||||||
/// as possible.
|
|
||||||
final Offset anchor;
|
|
||||||
|
|
||||||
/// {@macro flutter.material.TextSelectionToolbar.children}
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
/// * [CupertinoDesktopTextSelectionToolbarButton], which builds a default
|
|
||||||
/// Mac-style text selection toolbar text button.
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
// Builds a toolbar just like the default Mac toolbar, with the right color
|
|
||||||
// background, padding, and rounded corners.
|
|
||||||
static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
|
|
||||||
return Container(
|
|
||||||
width: _kToolbarWidth,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _kToolbarBackgroundColor.resolveFrom(context),
|
|
||||||
border: Border.all(
|
|
||||||
color: _kToolbarBorderColor.resolveFrom(context),
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.all(_kToolbarBorderRadius),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
// This value was measured from a screenshot of TextEdit on MacOS
|
|
||||||
// 10.15.7 on a Macbook Pro.
|
|
||||||
vertical: 3.0,
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
assert(debugCheckHasMediaQuery(context));
|
|
||||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
|
||||||
|
|
||||||
final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
|
|
||||||
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
_kToolbarScreenPadding,
|
|
||||||
paddingAbove,
|
|
||||||
_kToolbarScreenPadding,
|
|
||||||
_kToolbarScreenPadding,
|
|
||||||
),
|
|
||||||
child: CustomSingleChildLayout(
|
|
||||||
delegate: DesktopTextSelectionToolbarLayoutDelegate(
|
|
||||||
anchor: anchor - localAdjustment,
|
|
||||||
),
|
|
||||||
child: _defaultToolbarBuilder(context, Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: children,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// These values were measured from a screenshot of TextEdit on MacOS 10.15.7 on
|
|
||||||
// a Macbook Pro.
|
|
||||||
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
|
||||||
inherit: false,
|
|
||||||
fontSize: 14.0,
|
|
||||||
letterSpacing: -0.15,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
);
|
|
||||||
|
|
||||||
// This value was measured from a screenshot of TextEdit on MacOS 10.15.7 on a
|
|
||||||
// Macbook Pro.
|
|
||||||
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB(
|
|
||||||
20.0,
|
|
||||||
0.0,
|
|
||||||
20.0,
|
|
||||||
3.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A button in the style of the Mac context menu buttons.
|
|
||||||
class _CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget {
|
|
||||||
/// Creates an instance of CupertinoDesktopTextSelectionToolbarButton.
|
|
||||||
const _CupertinoDesktopTextSelectionToolbarButton({
|
|
||||||
required this.onPressed,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] whose child is
|
|
||||||
/// a [Text] widget styled like the default Mac context menu button.
|
|
||||||
_CupertinoDesktopTextSelectionToolbarButton.text({
|
|
||||||
required BuildContext context,
|
|
||||||
required this.onPressed,
|
|
||||||
required String text,
|
|
||||||
}) : child = Text(
|
|
||||||
text,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: _kToolbarButtonFontStyle.copyWith(
|
|
||||||
color: const CupertinoDynamicColor.withBrightness(
|
|
||||||
color: CupertinoColors.black,
|
|
||||||
darkColor: CupertinoColors.white,
|
|
||||||
).resolveFrom(context),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
|
|
||||||
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_CupertinoDesktopTextSelectionToolbarButtonState createState() => _CupertinoDesktopTextSelectionToolbarButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CupertinoDesktopTextSelectionToolbarButtonState extends State<_CupertinoDesktopTextSelectionToolbarButton> {
|
|
||||||
bool _isHovered = false;
|
|
||||||
|
|
||||||
void _onEnter(PointerEnterEvent event) {
|
|
||||||
setState(() {
|
|
||||||
_isHovered = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onExit(PointerExitEvent event) {
|
|
||||||
setState(() {
|
|
||||||
_isHovered = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: MouseRegion(
|
|
||||||
onEnter: _onEnter,
|
|
||||||
onExit: _onExit,
|
|
||||||
child: CupertinoButton(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
borderRadius: null,
|
|
||||||
color: _isHovered ? CupertinoTheme.of(context).primaryColor : null,
|
|
||||||
minSize: 0.0,
|
|
||||||
onPressed: widget.onPressed,
|
|
||||||
padding: _kToolbarButtonPadding,
|
|
||||||
pressedOpacity: 0.7,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
// 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/widgets.dart';
|
||||||
|
|
||||||
|
import 'colors.dart';
|
||||||
|
|
||||||
|
// The minimum padding from all edges of the selection toolbar to all edges of
|
||||||
|
// the screen.
|
||||||
|
const double _kToolbarScreenPadding = 8.0;
|
||||||
|
|
||||||
|
// These values were measured from a screenshot of TextEdit on macOS 10.15.7 on
|
||||||
|
// a Macbook Pro.
|
||||||
|
const double _kToolbarWidth = 222.0;
|
||||||
|
const Radius _kToolbarBorderRadius = Radius.circular(4.0);
|
||||||
|
const EdgeInsets _kToolbarPadding = EdgeInsets.symmetric(
|
||||||
|
vertical: 3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// These values were measured from a screenshot of TextEdit on macOS 10.16 on a
|
||||||
|
// Macbook Pro.
|
||||||
|
const CupertinoDynamicColor _kToolbarBorderColor = CupertinoDynamicColor.withBrightness(
|
||||||
|
color: Color(0xFFBBBBBB),
|
||||||
|
darkColor: Color(0xFF505152),
|
||||||
|
);
|
||||||
|
const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness(
|
||||||
|
color: Color(0xffECE8E6),
|
||||||
|
darkColor: Color(0xff302928),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A macOS-style text selection toolbar.
|
||||||
|
///
|
||||||
|
/// Typically displays buttons for text manipulation, e.g. copying and pasting
|
||||||
|
/// text.
|
||||||
|
///
|
||||||
|
/// Tries to position itself as closely as possible to [anchor] while remaining
|
||||||
|
/// fully inside the viewport.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar], where this is used to build the
|
||||||
|
/// toolbar for desktop platforms.
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], where this is used to build the toolbar on
|
||||||
|
/// macOS.
|
||||||
|
/// * [DesktopTextSelectionToolbar], which is similar but builds a
|
||||||
|
/// Material-style desktop toolbar.
|
||||||
|
class CupertinoDesktopTextSelectionToolbar extends StatelessWidget {
|
||||||
|
/// Creates a const instance of CupertinoTextSelectionToolbar.
|
||||||
|
const CupertinoDesktopTextSelectionToolbar({
|
||||||
|
super.key,
|
||||||
|
required this.anchor,
|
||||||
|
required this.children,
|
||||||
|
}) : assert(children.length > 0);
|
||||||
|
|
||||||
|
/// {@macro flutter.material.DesktopTextSelectionToolbar.anchor}
|
||||||
|
final Offset anchor;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.TextSelectionToolbar.children}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [CupertinoDesktopTextSelectionToolbarButton], which builds a default
|
||||||
|
/// macOS-style text selection toolbar text button.
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
// Builds a toolbar just like the default Mac toolbar, with the right color
|
||||||
|
// background, padding, and rounded corners.
|
||||||
|
static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
|
||||||
|
return Container(
|
||||||
|
width: _kToolbarWidth,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _kToolbarBackgroundColor.resolveFrom(context),
|
||||||
|
border: Border.all(
|
||||||
|
color: _kToolbarBorderColor.resolveFrom(context),
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(_kToolbarBorderRadius),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: _kToolbarPadding,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
|
||||||
|
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
_kToolbarScreenPadding,
|
||||||
|
paddingAbove,
|
||||||
|
_kToolbarScreenPadding,
|
||||||
|
_kToolbarScreenPadding,
|
||||||
|
),
|
||||||
|
child: CustomSingleChildLayout(
|
||||||
|
delegate: DesktopTextSelectionToolbarLayoutDelegate(
|
||||||
|
anchor: anchor - localAdjustment,
|
||||||
|
),
|
||||||
|
child: _defaultToolbarBuilder(
|
||||||
|
context,
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
// 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/gestures.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'button.dart';
|
||||||
|
import 'colors.dart';
|
||||||
|
import 'text_selection_toolbar_button.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
// These values were measured from a screenshot of TextEdit on MacOS 10.15.7 on
|
||||||
|
// a Macbook Pro.
|
||||||
|
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
||||||
|
inherit: false,
|
||||||
|
fontSize: 14.0,
|
||||||
|
letterSpacing: -0.15,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
);
|
||||||
|
|
||||||
|
// This value was measured from a screenshot of TextEdit on MacOS 10.15.7 on a
|
||||||
|
// Macbook Pro.
|
||||||
|
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB(
|
||||||
|
20.0,
|
||||||
|
0.0,
|
||||||
|
20.0,
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A button in the style of the Mac context menu buttons.
|
||||||
|
class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget {
|
||||||
|
/// Creates an instance of CupertinoDesktopTextSelectionToolbarButton.
|
||||||
|
///
|
||||||
|
/// [child] cannot be null.
|
||||||
|
const CupertinoDesktopTextSelectionToolbarButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required Widget this.child,
|
||||||
|
}) : assert(child != null),
|
||||||
|
buttonItem = null;
|
||||||
|
|
||||||
|
/// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] whose child is
|
||||||
|
/// a [Text] widget styled like the default Mac context menu button.
|
||||||
|
CupertinoDesktopTextSelectionToolbarButton.text({
|
||||||
|
super.key,
|
||||||
|
required BuildContext context,
|
||||||
|
required this.onPressed,
|
||||||
|
required String text,
|
||||||
|
}) : buttonItem = null,
|
||||||
|
child = Text(
|
||||||
|
text,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: _kToolbarButtonFontStyle.copyWith(
|
||||||
|
color: const CupertinoDynamicColor.withBrightness(
|
||||||
|
color: CupertinoColors.black,
|
||||||
|
darkColor: CupertinoColors.white,
|
||||||
|
).resolveFrom(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] from
|
||||||
|
/// the given [ContextMenuButtonItem].
|
||||||
|
///
|
||||||
|
/// [buttonItem] cannot be null.
|
||||||
|
CupertinoDesktopTextSelectionToolbarButton.buttonItem({
|
||||||
|
super.key,
|
||||||
|
required ContextMenuButtonItem this.buttonItem,
|
||||||
|
}) : assert(buttonItem != null),
|
||||||
|
onPressed = buttonItem.onPressed,
|
||||||
|
child = null;
|
||||||
|
|
||||||
|
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
/// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
|
||||||
|
final ContextMenuButtonItem? buttonItem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CupertinoDesktopTextSelectionToolbarButton> createState() => _CupertinoDesktopTextSelectionToolbarButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CupertinoDesktopTextSelectionToolbarButtonState extends State<CupertinoDesktopTextSelectionToolbarButton> {
|
||||||
|
bool _isHovered = false;
|
||||||
|
|
||||||
|
void _onEnter(PointerEnterEvent event) {
|
||||||
|
setState(() {
|
||||||
|
_isHovered = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onExit(PointerExitEvent event) {
|
||||||
|
setState(() {
|
||||||
|
_isHovered = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Widget child = widget.child ?? Text(
|
||||||
|
CupertinoTextSelectionToolbarButton.getButtonLabel(context, widget.buttonItem!),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: _kToolbarButtonFontStyle.copyWith(
|
||||||
|
color: const CupertinoDynamicColor.withBrightness(
|
||||||
|
color: CupertinoColors.black,
|
||||||
|
darkColor: CupertinoColors.white,
|
||||||
|
).resolveFrom(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: _onEnter,
|
||||||
|
onExit: _onExit,
|
||||||
|
child: CupertinoButton(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
borderRadius: null,
|
||||||
|
color: _isHovered ? CupertinoTheme.of(context).primaryColor : null,
|
||||||
|
minSize: 0.0,
|
||||||
|
onPressed: widget.onPressed,
|
||||||
|
padding: _kToolbarButtonPadding,
|
||||||
|
pressedOpacity: 0.7,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'adaptive_text_selection_toolbar.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
import 'desktop_text_selection.dart';
|
import 'desktop_text_selection.dart';
|
||||||
import 'icons.dart';
|
import 'icons.dart';
|
||||||
@ -234,7 +235,11 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.textAlignVertical,
|
this.textAlignVertical,
|
||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
ToolbarOptions? toolbarOptions,
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
this.toolbarOptions,
|
||||||
this.showCursor,
|
this.showCursor,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.obscuringCharacter = '•',
|
this.obscuringCharacter = '•',
|
||||||
@ -273,6 +278,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.scribbleEnabled = true,
|
this.scribbleEnabled = true,
|
||||||
this.enableIMEPersonalizedLearning = true,
|
this.enableIMEPersonalizedLearning = true,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
this.spellCheckConfiguration,
|
this.spellCheckConfiguration,
|
||||||
this.magnifierConfiguration,
|
this.magnifierConfiguration,
|
||||||
}) : assert(textAlign != null),
|
}) : assert(textAlign != null),
|
||||||
@ -313,31 +319,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
|
||||||
toolbarOptions = toolbarOptions ??
|
|
||||||
(obscureText
|
|
||||||
? (readOnly
|
|
||||||
// No point in even offering "Select All" in a read-only obscured
|
|
||||||
// field.
|
|
||||||
? const ToolbarOptions()
|
|
||||||
// Writable, but obscured.
|
|
||||||
: const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
paste: true,
|
|
||||||
))
|
|
||||||
: (readOnly
|
|
||||||
// Read-only, not obscured.
|
|
||||||
? const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
copy: true,
|
|
||||||
)
|
|
||||||
// Writable, not obscured.
|
|
||||||
: const ToolbarOptions(
|
|
||||||
copy: true,
|
|
||||||
cut: true,
|
|
||||||
selectAll: true,
|
|
||||||
paste: true,
|
|
||||||
)));
|
|
||||||
|
|
||||||
/// Creates a borderless iOS-style text field.
|
/// Creates a borderless iOS-style text field.
|
||||||
///
|
///
|
||||||
@ -397,7 +379,11 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.textAlignVertical,
|
this.textAlignVertical,
|
||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
ToolbarOptions? toolbarOptions,
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
this.toolbarOptions,
|
||||||
this.showCursor,
|
this.showCursor,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.obscuringCharacter = '•',
|
this.obscuringCharacter = '•',
|
||||||
@ -436,6 +422,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.scribbleEnabled = true,
|
this.scribbleEnabled = true,
|
||||||
this.enableIMEPersonalizedLearning = true,
|
this.enableIMEPersonalizedLearning = true,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
this.spellCheckConfiguration,
|
this.spellCheckConfiguration,
|
||||||
this.magnifierConfiguration,
|
this.magnifierConfiguration,
|
||||||
}) : assert(textAlign != null),
|
}) : assert(textAlign != null),
|
||||||
@ -477,31 +464,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
assert(clipBehavior != null),
|
assert(clipBehavior != null),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
|
||||||
toolbarOptions = toolbarOptions ??
|
|
||||||
(obscureText
|
|
||||||
? (readOnly
|
|
||||||
// No point in even offering "Select All" in a read-only obscured
|
|
||||||
// field.
|
|
||||||
? const ToolbarOptions()
|
|
||||||
// Writable, but obscured.
|
|
||||||
: const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
paste: true,
|
|
||||||
))
|
|
||||||
: (readOnly
|
|
||||||
// Read-only, not obscured.
|
|
||||||
? const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
copy: true,
|
|
||||||
)
|
|
||||||
// Writable, not obscured.
|
|
||||||
: const ToolbarOptions(
|
|
||||||
copy: true,
|
|
||||||
cut: true,
|
|
||||||
selectAll: true,
|
|
||||||
paste: true,
|
|
||||||
)));
|
|
||||||
|
|
||||||
/// Controls the text being edited.
|
/// Controls the text being edited.
|
||||||
///
|
///
|
||||||
@ -605,7 +568,11 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
/// If not set, select all and paste will default to be enabled. Copy and cut
|
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||||
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||||
/// paste and cut will be disabled regardless.
|
/// paste and cut will be disabled regardless.
|
||||||
final ToolbarOptions toolbarOptions;
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final ToolbarOptions? toolbarOptions;
|
||||||
|
|
||||||
/// {@macro flutter.material.InputDecorator.textAlignVertical}
|
/// {@macro flutter.material.InputDecorator.textAlignVertical}
|
||||||
final TextAlignVertical? textAlignVertical;
|
final TextAlignVertical? textAlignVertical;
|
||||||
@ -787,6 +754,21 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
||||||
final bool enableIMEPersonalizedLearning;
|
final bool enableIMEPersonalizedLearning;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||||
|
///
|
||||||
|
/// If not provided, will build a default menu based on the platform.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar], which is built by default.
|
||||||
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return CupertinoAdaptiveTextSelectionToolbar.editableText(
|
||||||
|
editableTextState: editableTextState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
|
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
|
||||||
///
|
///
|
||||||
/// {@macro flutter.widgets.magnifier.intro}
|
/// {@macro flutter.widgets.magnifier.intro}
|
||||||
@ -1226,12 +1208,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
|
|||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
textSelectionControls ??= cupertinoTextSelectionControls;
|
textSelectionControls ??= cupertinoTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TargetPlatform.macOS:
|
case TargetPlatform.macOS:
|
||||||
case TargetPlatform.windows:
|
case TargetPlatform.windows:
|
||||||
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
|
textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
|
||||||
handleDidGainAccessibilityFocus = () {
|
handleDidGainAccessibilityFocus = () {
|
||||||
// Automatically activate the TextField when it receives accessibility focus.
|
// Automatically activate the TextField when it receives accessibility focus.
|
||||||
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
|
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
|
||||||
@ -1380,6 +1362,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
|
|||||||
restorationId: 'editable',
|
restorationId: 'editable',
|
||||||
scribbleEnabled: widget.scribbleEnabled,
|
scribbleEnabled: widget.scribbleEnabled,
|
||||||
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
||||||
|
contextMenuBuilder: widget.contextMenuBuilder,
|
||||||
spellCheckConfiguration: spellCheckConfiguration,
|
spellCheckConfiguration: spellCheckConfiguration,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'adaptive_text_selection_toolbar.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
import 'form_row.dart';
|
import 'form_row.dart';
|
||||||
import 'text_field.dart';
|
import 'text_field.dart';
|
||||||
@ -116,6 +117,10 @@ class CupertinoTextFormFieldRow extends FormField<String> {
|
|||||||
TextAlignVertical? textAlignVertical,
|
TextAlignVertical? textAlignVertical,
|
||||||
bool autofocus = false,
|
bool autofocus = false,
|
||||||
bool readOnly = false,
|
bool readOnly = false,
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
ToolbarOptions? toolbarOptions,
|
ToolbarOptions? toolbarOptions,
|
||||||
bool? showCursor,
|
bool? showCursor,
|
||||||
String obscuringCharacter = '•',
|
String obscuringCharacter = '•',
|
||||||
@ -151,6 +156,7 @@ class CupertinoTextFormFieldRow extends FormField<String> {
|
|||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: CupertinoColors.placeholderText,
|
color: CupertinoColors.placeholderText,
|
||||||
),
|
),
|
||||||
|
EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
}) : assert(initialValue == null || controller == null),
|
}) : assert(initialValue == null || controller == null),
|
||||||
assert(textAlign != null),
|
assert(textAlign != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
@ -234,6 +240,7 @@ class CupertinoTextFormFieldRow extends FormField<String> {
|
|||||||
autofillHints: autofillHints,
|
autofillHints: autofillHints,
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
placeholderStyle: placeholderStyle,
|
placeholderStyle: placeholderStyle,
|
||||||
|
contextMenuBuilder: contextMenuBuilder,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -262,6 +269,12 @@ class CupertinoTextFormFieldRow extends FormField<String> {
|
|||||||
/// initialize its [TextEditingController.text] with [initialValue].
|
/// initialize its [TextEditingController.text] with [initialValue].
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return CupertinoAdaptiveTextSelectionToolbar.editableText(
|
||||||
|
editableTextState: editableTextState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FormFieldState<String> createState() => _CupertinoTextFormFieldRowState();
|
FormFieldState<String> createState() => _CupertinoTextFormFieldRowState();
|
||||||
}
|
}
|
||||||
|
@ -22,142 +22,6 @@ const double _kSelectionHandleRadius = 6;
|
|||||||
// screen. Eyeballed value.
|
// screen. Eyeballed value.
|
||||||
const double _kArrowScreenPadding = 26.0;
|
const double _kArrowScreenPadding = 26.0;
|
||||||
|
|
||||||
// Generates the child that's passed into CupertinoTextSelectionToolbar.
|
|
||||||
class _CupertinoTextSelectionControlsToolbar extends StatefulWidget {
|
|
||||||
const _CupertinoTextSelectionControlsToolbar({
|
|
||||||
required this.clipboardStatus,
|
|
||||||
required this.endpoints,
|
|
||||||
required this.globalEditableRegion,
|
|
||||||
required this.handleCopy,
|
|
||||||
required this.handleCut,
|
|
||||||
required this.handlePaste,
|
|
||||||
required this.handleSelectAll,
|
|
||||||
required this.selectionMidpoint,
|
|
||||||
required this.textLineHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ClipboardStatusNotifier? clipboardStatus;
|
|
||||||
final List<TextSelectionPoint> endpoints;
|
|
||||||
final Rect globalEditableRegion;
|
|
||||||
final VoidCallback? handleCopy;
|
|
||||||
final VoidCallback? handleCut;
|
|
||||||
final VoidCallback? handlePaste;
|
|
||||||
final VoidCallback? handleSelectAll;
|
|
||||||
final Offset selectionMidpoint;
|
|
||||||
final double textLineHeight;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_CupertinoTextSelectionControlsToolbarState createState() => _CupertinoTextSelectionControlsToolbarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSelectionControlsToolbar> {
|
|
||||||
void _onChangedClipboardStatus() {
|
|
||||||
setState(() {
|
|
||||||
// Inform the widget that the value of clipboardStatus has changed.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(_CupertinoTextSelectionControlsToolbar oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.clipboardStatus != widget.clipboardStatus) {
|
|
||||||
oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
|
||||||
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// Don't render the menu until the state of the clipboard is known.
|
|
||||||
if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(debugCheckHasMediaQuery(context));
|
|
||||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
|
||||||
|
|
||||||
// The toolbar should appear below the TextField when there is not enough
|
|
||||||
// space above the TextField to show it, assuming there's always enough
|
|
||||||
// space at the bottom in this case.
|
|
||||||
final double anchorX = clampDouble(widget.selectionMidpoint.dx + widget.globalEditableRegion.left,
|
|
||||||
_kArrowScreenPadding + mediaQuery.padding.left,
|
|
||||||
mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding,
|
|
||||||
);
|
|
||||||
|
|
||||||
final double topAmountInEditableRegion = widget.endpoints.first.point.dy - widget.textLineHeight;
|
|
||||||
final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top;
|
|
||||||
|
|
||||||
// The y-coordinate has to be calculated instead of directly quoting
|
|
||||||
// selectionMidpoint.dy, since the caller
|
|
||||||
// (TextSelectionOverlay._buildToolbar) does not know whether the toolbar is
|
|
||||||
// going to be facing up or down.
|
|
||||||
final Offset anchorAbove = Offset(
|
|
||||||
anchorX,
|
|
||||||
anchorTop,
|
|
||||||
);
|
|
||||||
final Offset anchorBelow = Offset(
|
|
||||||
anchorX,
|
|
||||||
widget.endpoints.last.point.dy + widget.globalEditableRegion.top,
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<Widget> items = <Widget>[];
|
|
||||||
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
|
|
||||||
final Widget onePhysicalPixelVerticalDivider =
|
|
||||||
SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
|
|
||||||
|
|
||||||
void addToolbarButton(
|
|
||||||
String text,
|
|
||||||
VoidCallback onPressed,
|
|
||||||
) {
|
|
||||||
if (items.isNotEmpty) {
|
|
||||||
items.add(onePhysicalPixelVerticalDivider);
|
|
||||||
}
|
|
||||||
|
|
||||||
items.add(CupertinoTextSelectionToolbarButton.text(
|
|
||||||
onPressed: onPressed,
|
|
||||||
text: text,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.handleCut != null) {
|
|
||||||
addToolbarButton(localizations.cutButtonLabel, widget.handleCut!);
|
|
||||||
}
|
|
||||||
if (widget.handleCopy != null) {
|
|
||||||
addToolbarButton(localizations.copyButtonLabel, widget.handleCopy!);
|
|
||||||
}
|
|
||||||
if (widget.handlePaste != null
|
|
||||||
&& widget.clipboardStatus?.value == ClipboardStatus.pasteable) {
|
|
||||||
addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste!);
|
|
||||||
}
|
|
||||||
if (widget.handleSelectAll != null) {
|
|
||||||
addToolbarButton(localizations.selectAllButtonLabel, widget.handleSelectAll!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no option available, build an empty widget.
|
|
||||||
if (items.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return CupertinoTextSelectionToolbar(
|
|
||||||
anchorAbove: anchorAbove,
|
|
||||||
anchorBelow: anchorBelow,
|
|
||||||
children: items,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws a single text selection handle with a bar and a ball.
|
/// Draws a single text selection handle with a bar and a ball.
|
||||||
class _TextSelectionHandlePainter extends CustomPainter {
|
class _TextSelectionHandlePainter extends CustomPainter {
|
||||||
const _TextSelectionHandlePainter(this.color);
|
const _TextSelectionHandlePainter(this.color);
|
||||||
@ -190,6 +54,17 @@ class _TextSelectionHandlePainter extends CustomPainter {
|
|||||||
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => color != oldPainter.color;
|
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => color != oldPainter.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// iOS Cupertino styled text selection handle controls.
|
||||||
|
///
|
||||||
|
/// Specifically does not manage the toolbar, which is left to
|
||||||
|
/// [EditableText.contextMenuBuilder].
|
||||||
|
@Deprecated(
|
||||||
|
'Use `CupertinoTextSelectionControls`. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
class CupertinoTextSelectionHandleControls extends CupertinoTextSelectionControls with TextSelectionHandleControls {
|
||||||
|
}
|
||||||
|
|
||||||
/// iOS Cupertino styled text selection controls.
|
/// iOS Cupertino styled text selection controls.
|
||||||
///
|
///
|
||||||
/// The [cupertinoTextSelectionControls] global variable has a
|
/// The [cupertinoTextSelectionControls] global variable has a
|
||||||
@ -205,6 +80,10 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for iOS-style copy/paste text selection toolbar.
|
/// Builder for iOS-style copy/paste text selection toolbar.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
Widget buildToolbar(
|
Widget buildToolbar(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -213,7 +92,7 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
|
|||||||
Offset selectionMidpoint,
|
Offset selectionMidpoint,
|
||||||
List<TextSelectionPoint> endpoints,
|
List<TextSelectionPoint> endpoints,
|
||||||
TextSelectionDelegate delegate,
|
TextSelectionDelegate delegate,
|
||||||
ClipboardStatusNotifier? clipboardStatus,
|
ValueNotifier<ClipboardStatus>? clipboardStatus,
|
||||||
Offset? lastSecondaryTapDownPosition,
|
Offset? lastSecondaryTapDownPosition,
|
||||||
) {
|
) {
|
||||||
return _CupertinoTextSelectionControlsToolbar(
|
return _CupertinoTextSelectionControlsToolbar(
|
||||||
@ -305,5 +184,150 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text selection controls that follows iOS design conventions.
|
/// Text selection handle controls that follow iOS design conventions.
|
||||||
final TextSelectionControls cupertinoTextSelectionControls = CupertinoTextSelectionControls();
|
@Deprecated(
|
||||||
|
'Use `cupertinoTextSelectionControls` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final TextSelectionControls cupertinoTextSelectionHandleControls =
|
||||||
|
CupertinoTextSelectionHandleControls();
|
||||||
|
|
||||||
|
/// Text selection controls that follow iOS design conventions.
|
||||||
|
final TextSelectionControls cupertinoTextSelectionControls =
|
||||||
|
CupertinoTextSelectionControls();
|
||||||
|
|
||||||
|
// Generates the child that's passed into CupertinoTextSelectionToolbar.
|
||||||
|
class _CupertinoTextSelectionControlsToolbar extends StatefulWidget {
|
||||||
|
const _CupertinoTextSelectionControlsToolbar({
|
||||||
|
required this.clipboardStatus,
|
||||||
|
required this.endpoints,
|
||||||
|
required this.globalEditableRegion,
|
||||||
|
required this.handleCopy,
|
||||||
|
required this.handleCut,
|
||||||
|
required this.handlePaste,
|
||||||
|
required this.handleSelectAll,
|
||||||
|
required this.selectionMidpoint,
|
||||||
|
required this.textLineHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ValueNotifier<ClipboardStatus>? clipboardStatus;
|
||||||
|
final List<TextSelectionPoint> endpoints;
|
||||||
|
final Rect globalEditableRegion;
|
||||||
|
final VoidCallback? handleCopy;
|
||||||
|
final VoidCallback? handleCut;
|
||||||
|
final VoidCallback? handlePaste;
|
||||||
|
final VoidCallback? handleSelectAll;
|
||||||
|
final Offset selectionMidpoint;
|
||||||
|
final double textLineHeight;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CupertinoTextSelectionControlsToolbarState createState() => _CupertinoTextSelectionControlsToolbarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSelectionControlsToolbar> {
|
||||||
|
void _onChangedClipboardStatus() {
|
||||||
|
setState(() {
|
||||||
|
// Inform the widget that the value of clipboardStatus has changed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_CupertinoTextSelectionControlsToolbar oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.clipboardStatus != widget.clipboardStatus) {
|
||||||
|
oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||||
|
widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Don't render the menu until the state of the clipboard is known.
|
||||||
|
if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
// The toolbar should appear below the TextField when there is not enough
|
||||||
|
// space above the TextField to show it, assuming there's always enough
|
||||||
|
// space at the bottom in this case.
|
||||||
|
final double anchorX = clampDouble(widget.selectionMidpoint.dx + widget.globalEditableRegion.left,
|
||||||
|
_kArrowScreenPadding + mediaQuery.padding.left,
|
||||||
|
mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double topAmountInEditableRegion = widget.endpoints.first.point.dy - widget.textLineHeight;
|
||||||
|
final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top;
|
||||||
|
|
||||||
|
// The y-coordinate has to be calculated instead of directly quoting
|
||||||
|
// selectionMidpoint.dy, since the caller
|
||||||
|
// (TextSelectionOverlay._buildToolbar) does not know whether the toolbar is
|
||||||
|
// going to be facing up or down.
|
||||||
|
final Offset anchorAbove = Offset(
|
||||||
|
anchorX,
|
||||||
|
anchorTop,
|
||||||
|
);
|
||||||
|
final Offset anchorBelow = Offset(
|
||||||
|
anchorX,
|
||||||
|
widget.endpoints.last.point.dy + widget.globalEditableRegion.top,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Widget> items = <Widget>[];
|
||||||
|
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
|
||||||
|
final Widget onePhysicalPixelVerticalDivider =
|
||||||
|
SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
|
||||||
|
|
||||||
|
void addToolbarButton(
|
||||||
|
String text,
|
||||||
|
VoidCallback onPressed,
|
||||||
|
) {
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
items.add(onePhysicalPixelVerticalDivider);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(CupertinoTextSelectionToolbarButton.text(
|
||||||
|
onPressed: onPressed,
|
||||||
|
text: text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.handleCut != null) {
|
||||||
|
addToolbarButton(localizations.cutButtonLabel, widget.handleCut!);
|
||||||
|
}
|
||||||
|
if (widget.handleCopy != null) {
|
||||||
|
addToolbarButton(localizations.copyButtonLabel, widget.handleCopy!);
|
||||||
|
}
|
||||||
|
if (widget.handlePaste != null
|
||||||
|
&& widget.clipboardStatus?.value == ClipboardStatus.pasteable) {
|
||||||
|
addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste!);
|
||||||
|
}
|
||||||
|
if (widget.handleSelectAll != null) {
|
||||||
|
addToolbarButton(localizations.selectAllButtonLabel, widget.handleSelectAll!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no option available, build an empty widget.
|
||||||
|
if (items.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CupertinoTextSelectionToolbar(
|
||||||
|
anchorAbove: anchorAbove,
|
||||||
|
anchorBelow: anchorBelow,
|
||||||
|
children: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show clampDouble;
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
@ -21,6 +22,10 @@ const double _kToolbarContentDistance = 8.0;
|
|||||||
const double _kToolbarScreenPadding = 8.0;
|
const double _kToolbarScreenPadding = 8.0;
|
||||||
const Size _kToolbarArrowSize = Size(14.0, 7.0);
|
const Size _kToolbarArrowSize = Size(14.0, 7.0);
|
||||||
|
|
||||||
|
// Minimal padding from tip of the selection toolbar arrow to horizontal edges of the
|
||||||
|
// screen. Eyeballed value.
|
||||||
|
const double _kArrowScreenPadding = 26.0;
|
||||||
|
|
||||||
// Values extracted from https://developer.apple.com/design/resources/.
|
// Values extracted from https://developer.apple.com/design/resources/.
|
||||||
const Radius _kToolbarBorderRadius = Radius.circular(8);
|
const Radius _kToolbarBorderRadius = Radius.circular(8);
|
||||||
|
|
||||||
@ -45,6 +50,13 @@ typedef CupertinoToolbarBuilder = Widget Function(
|
|||||||
Widget child,
|
Widget child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class _CupertinoToolbarButtonDivider extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An iOS-style text selection toolbar.
|
/// An iOS-style text selection toolbar.
|
||||||
///
|
///
|
||||||
/// Typically displays buttons for text manipulation, e.g. copying and pasting
|
/// Typically displays buttons for text manipulation, e.g. copying and pasting
|
||||||
@ -58,8 +70,8 @@ typedef CupertinoToolbarBuilder = Widget Function(
|
|||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [TextSelectionControls.buildToolbar], where this is used by default to
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar for the current
|
||||||
/// build an iOS-style toolbar.
|
/// platform.
|
||||||
/// * [TextSelectionToolbar], which is similar, but builds an Android-style
|
/// * [TextSelectionToolbar], which is similar, but builds an Android-style
|
||||||
/// toolbar.
|
/// toolbar.
|
||||||
class CupertinoTextSelectionToolbar extends StatelessWidget {
|
class CupertinoTextSelectionToolbar extends StatelessWidget {
|
||||||
@ -91,6 +103,19 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
|
|||||||
/// default Cupertino toolbar.
|
/// default Cupertino toolbar.
|
||||||
final CupertinoToolbarBuilder toolbarBuilder;
|
final CupertinoToolbarBuilder toolbarBuilder;
|
||||||
|
|
||||||
|
// Add the visial vertical line spacer between children buttons.
|
||||||
|
static List<Widget> _addChildrenSpacers(List<Widget> children) {
|
||||||
|
final List<Widget> nextChildren = <Widget>[];
|
||||||
|
for (int i = 0; i < children.length; i++) {
|
||||||
|
final Widget child = children[i];
|
||||||
|
if (i != 0) {
|
||||||
|
nextChildren.add(_CupertinoToolbarButtonDivider());
|
||||||
|
}
|
||||||
|
nextChildren.add(child);
|
||||||
|
}
|
||||||
|
return nextChildren;
|
||||||
|
}
|
||||||
|
|
||||||
// Builds a toolbar just like the default iOS toolbar, with the right color
|
// Builds a toolbar just like the default iOS toolbar, with the right color
|
||||||
// background and a rounded cutout with an arrow.
|
// background and a rounded cutout with an arrow.
|
||||||
static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) {
|
static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) {
|
||||||
@ -115,8 +140,19 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
|
|||||||
+ _kToolbarHeight;
|
+ _kToolbarHeight;
|
||||||
final bool fitsAbove = anchorAbove.dy >= toolbarHeightNeeded;
|
final bool fitsAbove = anchorAbove.dy >= toolbarHeightNeeded;
|
||||||
|
|
||||||
const Offset contentPaddingAdjustment = Offset(0.0, _kToolbarContentDistance);
|
// The arrow, which points to the anchor, has some margin so it can't get
|
||||||
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
// too close to the horizontal edges of the screen.
|
||||||
|
final double leftMargin = _kArrowScreenPadding + mediaQuery.padding.left;
|
||||||
|
final double rightMargin = mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding;
|
||||||
|
|
||||||
|
final Offset anchorAboveAdjusted = Offset(
|
||||||
|
clampDouble(anchorAbove.dx, leftMargin, rightMargin),
|
||||||
|
anchorAbove.dy - _kToolbarContentDistance - paddingAbove,
|
||||||
|
);
|
||||||
|
final Offset anchorBelowAdjusted = Offset(
|
||||||
|
clampDouble(anchorBelow.dx, leftMargin, rightMargin),
|
||||||
|
anchorBelow.dy - _kToolbarContentDistance + paddingAbove,
|
||||||
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
@ -127,15 +163,15 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: CustomSingleChildLayout(
|
child: CustomSingleChildLayout(
|
||||||
delegate: TextSelectionToolbarLayoutDelegate(
|
delegate: TextSelectionToolbarLayoutDelegate(
|
||||||
anchorAbove: anchorAbove - localAdjustment - contentPaddingAdjustment,
|
anchorAbove: anchorAboveAdjusted,
|
||||||
anchorBelow: anchorBelow - localAdjustment + contentPaddingAdjustment,
|
anchorBelow: anchorBelowAdjusted,
|
||||||
fitsAbove: fitsAbove,
|
fitsAbove: fitsAbove,
|
||||||
),
|
),
|
||||||
child: _CupertinoTextSelectionToolbarContent(
|
child: _CupertinoTextSelectionToolbarContent(
|
||||||
anchor: fitsAbove ? anchorAbove : anchorBelow,
|
anchor: fitsAbove ? anchorAboveAdjusted : anchorBelowAdjusted,
|
||||||
isAbove: fitsAbove,
|
isAbove: fitsAbove,
|
||||||
toolbarBuilder: toolbarBuilder,
|
toolbarBuilder: toolbarBuilder,
|
||||||
children: children,
|
children: _addChildrenSpacers(children),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,8 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'button.dart';
|
import 'button.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
|
import 'debug.dart';
|
||||||
|
import 'localizations.dart';
|
||||||
|
|
||||||
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
||||||
inherit: false,
|
inherit: false,
|
||||||
@ -24,11 +26,14 @@ const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 16.0, h
|
|||||||
/// A button in the style of the iOS text selection toolbar buttons.
|
/// A button in the style of the iOS text selection toolbar buttons.
|
||||||
class CupertinoTextSelectionToolbarButton extends StatelessWidget {
|
class CupertinoTextSelectionToolbarButton extends StatelessWidget {
|
||||||
/// Create an instance of [CupertinoTextSelectionToolbarButton].
|
/// Create an instance of [CupertinoTextSelectionToolbarButton].
|
||||||
|
///
|
||||||
|
/// [child] cannot be null.
|
||||||
const CupertinoTextSelectionToolbarButton({
|
const CupertinoTextSelectionToolbarButton({
|
||||||
super.key,
|
super.key,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
required this.child,
|
required Widget this.child,
|
||||||
});
|
}) : assert(child != null),
|
||||||
|
buttonItem = null;
|
||||||
|
|
||||||
/// Create an instance of [CupertinoTextSelectionToolbarButton] whose child is
|
/// Create an instance of [CupertinoTextSelectionToolbarButton] whose child is
|
||||||
/// a [Text] widget styled like the default iOS text selection toolbar button.
|
/// a [Text] widget styled like the default iOS text selection toolbar button.
|
||||||
@ -36,7 +41,8 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
required String text,
|
required String text,
|
||||||
}) : child = Text(
|
}) : buttonItem = null,
|
||||||
|
child = Text(
|
||||||
text,
|
text,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: _kToolbarButtonFontStyle.copyWith(
|
style: _kToolbarButtonFontStyle.copyWith(
|
||||||
@ -44,20 +50,67 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Create an instance of [CupertinoTextSelectionToolbarButton] from the given
|
||||||
|
/// [ContextMenuButtonItem].
|
||||||
|
///
|
||||||
|
/// [buttonItem] cannot be null.
|
||||||
|
CupertinoTextSelectionToolbarButton.buttonItem({
|
||||||
|
super.key,
|
||||||
|
required ContextMenuButtonItem this.buttonItem,
|
||||||
|
}) : assert(buttonItem != null),
|
||||||
|
child = null,
|
||||||
|
onPressed = buttonItem.onPressed;
|
||||||
|
|
||||||
/// {@template flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
|
/// {@template flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
|
||||||
/// The child of this button.
|
/// The child of this button.
|
||||||
///
|
///
|
||||||
/// Usually a [Text] or an [Icon].
|
/// Usually a [Text] or an [Icon].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final Widget child;
|
final Widget? child;
|
||||||
|
|
||||||
/// {@template flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
|
/// {@template flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
|
||||||
/// Called when this button is pressed.
|
/// Called when this button is pressed.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
/// {@template flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed}
|
||||||
|
/// The buttonItem used to generate the button when using
|
||||||
|
/// [CupertinoTextSelectionToolbarButton.buttonItem].
|
||||||
|
/// {@endtemplate}
|
||||||
|
final ContextMenuButtonItem? buttonItem;
|
||||||
|
|
||||||
|
/// Returns the default button label String for the button of the given
|
||||||
|
/// [ContextMenuButtonItem]'s [ContextMenuButtonType].
|
||||||
|
static String getButtonLabel(BuildContext context, ContextMenuButtonItem buttonItem) {
|
||||||
|
if (buttonItem.label != null) {
|
||||||
|
return buttonItem.label!;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(debugCheckHasCupertinoLocalizations(context));
|
||||||
|
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
|
||||||
|
switch (buttonItem.type) {
|
||||||
|
case ContextMenuButtonType.cut:
|
||||||
|
return localizations.cutButtonLabel;
|
||||||
|
case ContextMenuButtonType.copy:
|
||||||
|
return localizations.copyButtonLabel;
|
||||||
|
case ContextMenuButtonType.paste:
|
||||||
|
return localizations.pasteButtonLabel;
|
||||||
|
case ContextMenuButtonType.selectAll:
|
||||||
|
return localizations.selectAllButtonLabel;
|
||||||
|
case ContextMenuButtonType.custom:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final Widget child = this.child ?? Text(
|
||||||
|
getButtonLabel(context, buttonItem!),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: _kToolbarButtonFontStyle.copyWith(
|
||||||
|
color: onPressed != null ? CupertinoColors.white : CupertinoColors.inactiveGray,
|
||||||
|
),
|
||||||
|
);
|
||||||
return CupertinoButton(
|
return CupertinoButton(
|
||||||
borderRadius: null,
|
borderRadius: null,
|
||||||
color: _kToolbarBackgroundColor,
|
color: _kToolbarBackgroundColor,
|
||||||
|
@ -0,0 +1,319 @@
|
|||||||
|
// 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/cupertino.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'debug.dart';
|
||||||
|
import 'desktop_text_selection_toolbar.dart';
|
||||||
|
import 'desktop_text_selection_toolbar_button.dart';
|
||||||
|
import 'material_localizations.dart';
|
||||||
|
import 'text_selection_toolbar.dart';
|
||||||
|
import 'text_selection_toolbar_text_button.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// The default context menu for text selection for the current platform.
|
||||||
|
///
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.contextMenuBuilders}
|
||||||
|
/// Typically, this widget would be passed to `contextMenuBuilder` in a
|
||||||
|
/// supported parent widget, such as:
|
||||||
|
///
|
||||||
|
/// * [EditableText.contextMenuBuilder]
|
||||||
|
/// * [TextField.contextMenuBuilder]
|
||||||
|
/// * [CupertinoTextField.contextMenuBuilder]
|
||||||
|
/// * [SelectionArea.contextMenuBuilder]
|
||||||
|
/// * [SelectableText.contextMenuBuilder]
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [EditableText.getEditableButtonItems], which returns the default
|
||||||
|
/// [ContextMenuButtonItem]s for [EditableText] on the platform.
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
|
||||||
|
/// Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar], which does the same thing as this
|
||||||
|
/// widget but only for Cupertino context menus.
|
||||||
|
/// * [TextSelectionToolbar], the default toolbar for Android.
|
||||||
|
/// * [DesktopTextSelectionToolbar], the default toolbar for desktop platforms
|
||||||
|
/// other than MacOS.
|
||||||
|
/// * [CupertinoTextSelectionToolbar], the default toolbar for iOS.
|
||||||
|
/// * [CupertinoDesktopTextSelectionToolbar], the default toolbar for MacOS.
|
||||||
|
class AdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||||
|
/// Create an instance of [AdaptiveTextSelectionToolbar] with the
|
||||||
|
/// given [children].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.buttonItems], which takes a list of
|
||||||
|
/// [ContextMenuButtonItem]s instead of [children] widgets.
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.editable], which builds the default
|
||||||
|
/// children for an editable field.
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.editableText], which builds the default
|
||||||
|
/// children for an [EditableText].
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.selectable], which builds the default
|
||||||
|
/// children for content that is selectable but not editable.
|
||||||
|
/// {@endtemplate}
|
||||||
|
const AdaptiveTextSelectionToolbar({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
required this.anchors,
|
||||||
|
}) : buttonItems = null;
|
||||||
|
|
||||||
|
/// Create an instance of [AdaptiveTextSelectionToolbar] whose children will
|
||||||
|
/// be built from the given [buttonItems].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.new], which takes the children directly as
|
||||||
|
/// a list of widgets.
|
||||||
|
/// {@endtemplate}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||||
|
const AdaptiveTextSelectionToolbar.buttonItems({
|
||||||
|
super.key,
|
||||||
|
required this.buttonItems,
|
||||||
|
required this.anchors,
|
||||||
|
}) : children = null;
|
||||||
|
|
||||||
|
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||||
|
/// children for an editable field.
|
||||||
|
///
|
||||||
|
/// If a callback is null, then its corresponding button will not be built.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||||
|
AdaptiveTextSelectionToolbar.editable({
|
||||||
|
super.key,
|
||||||
|
required ClipboardStatus clipboardStatus,
|
||||||
|
required VoidCallback? onCopy,
|
||||||
|
required VoidCallback? onCut,
|
||||||
|
required VoidCallback? onPaste,
|
||||||
|
required VoidCallback? onSelectAll,
|
||||||
|
required this.anchors,
|
||||||
|
}) : children = null,
|
||||||
|
buttonItems = EditableText.getEditableButtonItems(
|
||||||
|
clipboardStatus: clipboardStatus,
|
||||||
|
onCopy: onCopy,
|
||||||
|
onCut: onCut,
|
||||||
|
onPaste: onPaste,
|
||||||
|
onSelectAll: onSelectAll,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||||
|
/// children for an [EditableText].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||||
|
AdaptiveTextSelectionToolbar.editableText({
|
||||||
|
super.key,
|
||||||
|
required EditableTextState editableTextState,
|
||||||
|
}) : children = null,
|
||||||
|
buttonItems = editableTextState.contextMenuButtonItems,
|
||||||
|
anchors = editableTextState.contextMenuAnchors;
|
||||||
|
|
||||||
|
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||||
|
/// children for selectable, but not editable, content.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||||
|
AdaptiveTextSelectionToolbar.selectable({
|
||||||
|
super.key,
|
||||||
|
required VoidCallback onCopy,
|
||||||
|
required VoidCallback onSelectAll,
|
||||||
|
required SelectionGeometry selectionGeometry,
|
||||||
|
required this.anchors,
|
||||||
|
}) : children = null,
|
||||||
|
buttonItems = SelectableRegion.getSelectableButtonItems(
|
||||||
|
selectionGeometry: selectionGeometry,
|
||||||
|
onCopy: onCopy,
|
||||||
|
onSelectAll: onSelectAll,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||||
|
/// children for a [SelectableRegion].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||||
|
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||||
|
AdaptiveTextSelectionToolbar.selectableRegion({
|
||||||
|
super.key,
|
||||||
|
required SelectableRegionState selectableRegionState,
|
||||||
|
}) : children = null,
|
||||||
|
buttonItems = selectableRegionState.contextMenuButtonItems,
|
||||||
|
anchors = selectableRegionState.contextMenuAnchors;
|
||||||
|
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||||
|
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||||
|
/// widgets for the current platform.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final List<ContextMenuButtonItem>? buttonItems;
|
||||||
|
|
||||||
|
/// The children of the toolbar, typically buttons.
|
||||||
|
final List<Widget>? children;
|
||||||
|
|
||||||
|
/// {@template flutter.material.AdaptiveTextSelectionToolbar.anchors}
|
||||||
|
/// The location on which to anchor the menu.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final TextSelectionToolbarAnchors anchors;
|
||||||
|
|
||||||
|
/// Returns the default button label String for the button of the given
|
||||||
|
/// [ContextMenuButtonType] on any platform.
|
||||||
|
static String getButtonLabel(BuildContext context, ContextMenuButtonItem buttonItem) {
|
||||||
|
if (buttonItem.label != null) {
|
||||||
|
return buttonItem.label!;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (Theme.of(context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return CupertinoTextSelectionToolbarButton.getButtonLabel(
|
||||||
|
context,
|
||||||
|
buttonItem,
|
||||||
|
);
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
assert(debugCheckHasMaterialLocalizations(context));
|
||||||
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
|
switch (buttonItem.type) {
|
||||||
|
case ContextMenuButtonType.cut:
|
||||||
|
return localizations.cutButtonLabel;
|
||||||
|
case ContextMenuButtonType.copy:
|
||||||
|
return localizations.copyButtonLabel;
|
||||||
|
case ContextMenuButtonType.paste:
|
||||||
|
return localizations.pasteButtonLabel;
|
||||||
|
case ContextMenuButtonType.selectAll:
|
||||||
|
return localizations.selectAllButtonLabel;
|
||||||
|
case ContextMenuButtonType.custom:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a List of Widgets generated by turning [buttonItems] into the
|
||||||
|
/// the default context menu buttons for the current platform.
|
||||||
|
///
|
||||||
|
/// This is useful when building a text selection toolbar with the default
|
||||||
|
/// button appearance for the given platform, but where the toolbar and/or the
|
||||||
|
/// button actions and labels may be custom.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample demonstrates how to use `getAdaptiveButtons` to generate
|
||||||
|
/// default button widgets in a custom toolbar.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the
|
||||||
|
/// Cupertino equivalent of this class and builds only the Cupertino
|
||||||
|
/// buttons.
|
||||||
|
static Iterable<Widget> getAdaptiveButtons(BuildContext context, List<ContextMenuButtonItem> buttonItems) {
|
||||||
|
switch (Theme.of(context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||||
|
return CupertinoTextSelectionToolbarButton.text(
|
||||||
|
onPressed: buttonItem.onPressed,
|
||||||
|
text: getButtonLabel(context, buttonItem),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.android:
|
||||||
|
final List<Widget> buttons = <Widget>[];
|
||||||
|
for (int i = 0; i < buttonItems.length; i++) {
|
||||||
|
final ContextMenuButtonItem buttonItem = buttonItems[i];
|
||||||
|
buttons.add(TextSelectionToolbarTextButton(
|
||||||
|
padding: TextSelectionToolbarTextButton.getPadding(i, buttonItems.length),
|
||||||
|
onPressed: buttonItem.onPressed,
|
||||||
|
child: Text(getButtonLabel(context, buttonItem)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return buttons;
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||||
|
return DesktopTextSelectionToolbarButton.text(
|
||||||
|
context: context,
|
||||||
|
onPressed: buttonItem.onPressed,
|
||||||
|
text: getButtonLabel(context, buttonItem),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||||
|
return CupertinoDesktopTextSelectionToolbarButton.text(
|
||||||
|
context: context,
|
||||||
|
onPressed: buttonItem.onPressed,
|
||||||
|
text: getButtonLabel(context, buttonItem),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// If there aren't any buttons to build, build an empty toolbar.
|
||||||
|
if ((children != null && children!.isEmpty)
|
||||||
|
|| (buttonItems != null && buttonItems!.isEmpty)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Widget> resultChildren = children != null
|
||||||
|
? children!
|
||||||
|
: getAdaptiveButtons(context, buttonItems!).toList();
|
||||||
|
|
||||||
|
switch (Theme.of(context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return CupertinoTextSelectionToolbar(
|
||||||
|
anchorAbove: anchors.primaryAnchor,
|
||||||
|
anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!,
|
||||||
|
children: resultChildren,
|
||||||
|
);
|
||||||
|
case TargetPlatform.android:
|
||||||
|
return TextSelectionToolbar(
|
||||||
|
anchorAbove: anchors.primaryAnchor,
|
||||||
|
anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!,
|
||||||
|
children: resultChildren,
|
||||||
|
);
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return DesktopTextSelectionToolbar(
|
||||||
|
anchor: anchors.primaryAnchor,
|
||||||
|
children: resultChildren,
|
||||||
|
);
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return CupertinoDesktopTextSelectionToolbar(
|
||||||
|
anchor: anchors.primaryAnchor,
|
||||||
|
children: resultChildren,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,20 +3,19 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' show clampDouble;
|
import 'package:flutter/foundation.dart' show clampDouble;
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'colors.dart';
|
|
||||||
import 'constants.dart';
|
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'material.dart';
|
import 'desktop_text_selection_toolbar.dart';
|
||||||
|
import 'desktop_text_selection_toolbar_button.dart';
|
||||||
import 'material_localizations.dart';
|
import 'material_localizations.dart';
|
||||||
import 'text_button.dart';
|
|
||||||
import 'text_selection_toolbar.dart';
|
|
||||||
import 'theme.dart';
|
|
||||||
|
|
||||||
const double _kToolbarScreenPadding = 8.0;
|
/// Desktop Material styled text selection handle controls.
|
||||||
const double _kToolbarWidth = 222.0;
|
///
|
||||||
|
/// Specifically does not manage the toolbar, which is left to
|
||||||
|
/// [EditableText.contextMenuBuilder].
|
||||||
|
class _DesktopTextSelectionHandleControls extends DesktopTextSelectionControls with TextSelectionHandleControls {
|
||||||
|
}
|
||||||
|
|
||||||
/// Desktop Material styled text selection controls.
|
/// Desktop Material styled text selection controls.
|
||||||
///
|
///
|
||||||
@ -30,6 +29,10 @@ class DesktopTextSelectionControls extends TextSelectionControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for the Material-style desktop copy/paste text selection toolbar.
|
/// Builder for the Material-style desktop copy/paste text selection toolbar.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
Widget buildToolbar(
|
Widget buildToolbar(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -67,6 +70,10 @@ class DesktopTextSelectionControls extends TextSelectionControls {
|
|||||||
return Offset.zero;
|
return Offset.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
bool canSelectAll(TextSelectionDelegate delegate) {
|
bool canSelectAll(TextSelectionDelegate delegate) {
|
||||||
// Allow SelectAll when selection is not collapsed, unless everything has
|
// Allow SelectAll when selection is not collapsed, unless everything has
|
||||||
@ -77,6 +84,10 @@ class DesktopTextSelectionControls extends TextSelectionControls {
|
|||||||
!(value.selection.start == 0 && value.selection.end == value.text.length);
|
!(value.selection.start == 0 && value.selection.end == value.text.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
void handleSelectAll(TextSelectionDelegate delegate) {
|
void handleSelectAll(TextSelectionDelegate delegate) {
|
||||||
super.handleSelectAll(delegate);
|
super.handleSelectAll(delegate);
|
||||||
@ -84,7 +95,17 @@ class DesktopTextSelectionControls extends TextSelectionControls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text selection controls that loosely follows Material design conventions.
|
/// Desktop text selection handle controls that loosely follow Material design
|
||||||
|
/// conventions.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `desktopTextSelectionControls` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final TextSelectionControls desktopTextSelectionHandleControls =
|
||||||
|
_DesktopTextSelectionHandleControls();
|
||||||
|
|
||||||
|
/// Desktop text selection controls that loosely follow Material design
|
||||||
|
/// conventions.
|
||||||
final TextSelectionControls desktopTextSelectionControls =
|
final TextSelectionControls desktopTextSelectionControls =
|
||||||
DesktopTextSelectionControls();
|
DesktopTextSelectionControls();
|
||||||
|
|
||||||
@ -142,8 +163,8 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -173,7 +194,7 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect
|
|||||||
String text,
|
String text,
|
||||||
VoidCallback onPressed,
|
VoidCallback onPressed,
|
||||||
) {
|
) {
|
||||||
items.add(_DesktopTextSelectionToolbarButton.text(
|
items.add(DesktopTextSelectionToolbarButton.text(
|
||||||
context: context,
|
context: context,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
text: text,
|
text: text,
|
||||||
@ -199,153 +220,9 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _DesktopTextSelectionToolbar(
|
return DesktopTextSelectionToolbar(
|
||||||
anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor,
|
anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor,
|
||||||
children: items,
|
children: items,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Material-style desktop text selection toolbar.
|
|
||||||
///
|
|
||||||
/// Typically displays buttons for text manipulation, e.g. copying and pasting
|
|
||||||
/// text.
|
|
||||||
///
|
|
||||||
/// Tries to position itself as closely as possible to [anchor] while remaining
|
|
||||||
/// fully on-screen.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [_DesktopTextSelectionControls.buildToolbar], where this is used by
|
|
||||||
/// default to build a Material-style desktop toolbar.
|
|
||||||
/// * [TextSelectionToolbar], which is similar, but builds an Android-style
|
|
||||||
/// toolbar.
|
|
||||||
class _DesktopTextSelectionToolbar extends StatelessWidget {
|
|
||||||
/// Creates an instance of _DesktopTextSelectionToolbar.
|
|
||||||
const _DesktopTextSelectionToolbar({
|
|
||||||
required this.anchor,
|
|
||||||
required this.children,
|
|
||||||
}) : assert(children.length > 0);
|
|
||||||
|
|
||||||
/// The point at which the toolbar will attempt to position itself as closely
|
|
||||||
/// as possible.
|
|
||||||
final Offset anchor;
|
|
||||||
|
|
||||||
/// {@macro flutter.material.TextSelectionToolbar.children}
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
/// * [DesktopTextSelectionToolbarButton], which builds a default
|
|
||||||
/// Material-style desktop text selection toolbar text button.
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
// Builds a desktop toolbar in the Material style.
|
|
||||||
static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
|
|
||||||
return SizedBox(
|
|
||||||
width: _kToolbarWidth,
|
|
||||||
child: Material(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
elevation: 1.0,
|
|
||||||
type: MaterialType.card,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
assert(debugCheckHasMediaQuery(context));
|
|
||||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
|
||||||
|
|
||||||
final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
|
|
||||||
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
_kToolbarScreenPadding,
|
|
||||||
paddingAbove,
|
|
||||||
_kToolbarScreenPadding,
|
|
||||||
_kToolbarScreenPadding,
|
|
||||||
),
|
|
||||||
child: CustomSingleChildLayout(
|
|
||||||
delegate: DesktopTextSelectionToolbarLayoutDelegate(
|
|
||||||
anchor: anchor - localAdjustment,
|
|
||||||
),
|
|
||||||
child: _defaultToolbarBuilder(context, Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: children,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
|
||||||
inherit: false,
|
|
||||||
fontSize: 14.0,
|
|
||||||
letterSpacing: -0.15,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
);
|
|
||||||
|
|
||||||
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB(
|
|
||||||
20.0,
|
|
||||||
0.0,
|
|
||||||
20.0,
|
|
||||||
3.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A [TextButton] for the Material desktop text selection toolbar.
|
|
||||||
class _DesktopTextSelectionToolbarButton extends StatelessWidget {
|
|
||||||
/// Creates an instance of DesktopTextSelectionToolbarButton.
|
|
||||||
const _DesktopTextSelectionToolbarButton({
|
|
||||||
required this.onPressed,
|
|
||||||
required this.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Create an instance of [_DesktopTextSelectionToolbarButton] whose child is
|
|
||||||
/// a [Text] widget in the style of the Material text selection toolbar.
|
|
||||||
_DesktopTextSelectionToolbarButton.text({
|
|
||||||
required BuildContext context,
|
|
||||||
required this.onPressed,
|
|
||||||
required String text,
|
|
||||||
}) : child = Text(
|
|
||||||
text,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: _kToolbarButtonFontStyle.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.brightness == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black87,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// {@macro flutter.material.TextSelectionToolbarTextButton.onPressed}
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
|
|
||||||
/// {@macro flutter.material.TextSelectionToolbarTextButton.child}
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// TODO(hansmuller): Should be colorScheme.onSurface
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
final bool isDark = theme.colorScheme.brightness == Brightness.dark;
|
|
||||||
final Color foregroundColor = isDark ? Colors.white : Colors.black87;
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: TextButton(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
enabledMouseCursor: SystemMouseCursors.basic,
|
|
||||||
disabledMouseCursor: SystemMouseCursors.basic,
|
|
||||||
foregroundColor: foregroundColor,
|
|
||||||
shape: const RoundedRectangleBorder(),
|
|
||||||
minimumSize: const Size(kMinInteractiveDimension, 36.0),
|
|
||||||
padding: _kToolbarButtonPadding,
|
|
||||||
),
|
|
||||||
onPressed: onPressed,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
// 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/widgets.dart';
|
||||||
|
|
||||||
|
import 'material.dart';
|
||||||
|
import 'text_selection_toolbar.dart';
|
||||||
|
|
||||||
|
// These values were measured from a screenshot of TextEdit on macOS 10.15.7 on
|
||||||
|
// a Macbook Pro.
|
||||||
|
const double _kToolbarScreenPadding = 8.0;
|
||||||
|
const double _kToolbarWidth = 222.0;
|
||||||
|
|
||||||
|
/// A Material-style desktop text selection toolbar.
|
||||||
|
///
|
||||||
|
/// Typically displays buttons for text manipulation, e.g. copying and pasting
|
||||||
|
/// text.
|
||||||
|
///
|
||||||
|
/// Tries to position its top left corner as closely as possible to [anchor]
|
||||||
|
/// while remaining fully inside the viewport.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar for the current
|
||||||
|
/// platform.
|
||||||
|
/// * [TextSelectionToolbar], which is similar, but builds an Android-style
|
||||||
|
/// toolbar.
|
||||||
|
class DesktopTextSelectionToolbar extends StatelessWidget {
|
||||||
|
/// Creates a const instance of DesktopTextSelectionToolbar.
|
||||||
|
const DesktopTextSelectionToolbar({
|
||||||
|
super.key,
|
||||||
|
required this.anchor,
|
||||||
|
required this.children,
|
||||||
|
}) : assert(children.length > 0);
|
||||||
|
|
||||||
|
/// {@template flutter.material.DesktopTextSelectionToolbar.anchor}
|
||||||
|
/// The point where the toolbar will attempt to position itself as closely as
|
||||||
|
/// possible.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final Offset anchor;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.TextSelectionToolbar.children}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [DesktopTextSelectionToolbarButton], which builds a default
|
||||||
|
/// Material-style desktop text selection toolbar text button.
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
// Builds a desktop toolbar in the Material style.
|
||||||
|
static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
|
||||||
|
return SizedBox(
|
||||||
|
width: _kToolbarWidth,
|
||||||
|
child: Material(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
elevation: 1.0,
|
||||||
|
type: MaterialType.card,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
|
||||||
|
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
_kToolbarScreenPadding,
|
||||||
|
paddingAbove,
|
||||||
|
_kToolbarScreenPadding,
|
||||||
|
_kToolbarScreenPadding,
|
||||||
|
),
|
||||||
|
child: CustomSingleChildLayout(
|
||||||
|
delegate: DesktopTextSelectionToolbarLayoutDelegate(
|
||||||
|
anchor: anchor - localAdjustment,
|
||||||
|
),
|
||||||
|
child: _defaultToolbarBuilder(
|
||||||
|
context,
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'colors.dart';
|
||||||
|
import 'constants.dart';
|
||||||
|
import 'text_button.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
||||||
|
inherit: false,
|
||||||
|
fontSize: 14.0,
|
||||||
|
letterSpacing: -0.15,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
);
|
||||||
|
|
||||||
|
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB(
|
||||||
|
20.0,
|
||||||
|
0.0,
|
||||||
|
20.0,
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A [TextButton] for the Material desktop text selection toolbar.
|
||||||
|
class DesktopTextSelectionToolbarButton extends StatelessWidget {
|
||||||
|
/// Creates an instance of DesktopTextSelectionToolbarButton.
|
||||||
|
const DesktopTextSelectionToolbarButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create an instance of [DesktopTextSelectionToolbarButton] whose child is
|
||||||
|
/// a [Text] widget in the style of the Material text selection toolbar.
|
||||||
|
DesktopTextSelectionToolbarButton.text({
|
||||||
|
super.key,
|
||||||
|
required BuildContext context,
|
||||||
|
required this.onPressed,
|
||||||
|
required String text,
|
||||||
|
}) : child = Text(
|
||||||
|
text,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: _kToolbarButtonFontStyle.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.brightness == Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black87,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@macro flutter.material.TextSelectionToolbarTextButton.onPressed}
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.TextSelectionToolbarTextButton.child}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// TODO(hansmuller): Should be colorScheme.onSurface
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
final bool isDark = theme.colorScheme.brightness == Brightness.dark;
|
||||||
|
final Color foregroundColor = isDark ? Colors.white : Colors.black87;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
enabledMouseCursor: SystemMouseCursors.basic,
|
||||||
|
disabledMouseCursor: SystemMouseCursors.basic,
|
||||||
|
foregroundColor: foregroundColor,
|
||||||
|
shape: const RoundedRectangleBorder(),
|
||||||
|
minimumSize: const Size(kMinInteractiveDimension, 36.0),
|
||||||
|
padding: _kToolbarButtonPadding,
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'adaptive_text_selection_toolbar.dart';
|
||||||
import 'desktop_text_selection.dart';
|
import 'desktop_text_selection.dart';
|
||||||
import 'feedback.dart';
|
import 'feedback.dart';
|
||||||
import 'magnifier.dart';
|
import 'magnifier.dart';
|
||||||
@ -190,7 +191,11 @@ class SelectableText extends StatefulWidget {
|
|||||||
this.textScaleFactor,
|
this.textScaleFactor,
|
||||||
this.showCursor = false,
|
this.showCursor = false,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
ToolbarOptions? toolbarOptions,
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
this.toolbarOptions,
|
||||||
this.minLines,
|
this.minLines,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.cursorWidth = 2.0,
|
this.cursorWidth = 2.0,
|
||||||
@ -208,6 +213,7 @@ class SelectableText extends StatefulWidget {
|
|||||||
this.textHeightBehavior,
|
this.textHeightBehavior,
|
||||||
this.textWidthBasis,
|
this.textWidthBasis,
|
||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
this.magnifierConfiguration,
|
this.magnifierConfiguration,
|
||||||
}) : assert(showCursor != null),
|
}) : assert(showCursor != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
@ -224,12 +230,7 @@ class SelectableText extends StatefulWidget {
|
|||||||
data != null,
|
data != null,
|
||||||
'A non-null String must be provided to a SelectableText widget.',
|
'A non-null String must be provided to a SelectableText widget.',
|
||||||
),
|
),
|
||||||
textSpan = null,
|
textSpan = null;
|
||||||
toolbarOptions = toolbarOptions ??
|
|
||||||
const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
copy: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Creates a selectable text widget with a [TextSpan].
|
/// Creates a selectable text widget with a [TextSpan].
|
||||||
///
|
///
|
||||||
@ -248,7 +249,11 @@ class SelectableText extends StatefulWidget {
|
|||||||
this.textScaleFactor,
|
this.textScaleFactor,
|
||||||
this.showCursor = false,
|
this.showCursor = false,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
ToolbarOptions? toolbarOptions,
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
this.toolbarOptions,
|
||||||
this.minLines,
|
this.minLines,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.cursorWidth = 2.0,
|
this.cursorWidth = 2.0,
|
||||||
@ -266,6 +271,7 @@ class SelectableText extends StatefulWidget {
|
|||||||
this.textHeightBehavior,
|
this.textHeightBehavior,
|
||||||
this.textWidthBasis,
|
this.textWidthBasis,
|
||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
this.magnifierConfiguration,
|
this.magnifierConfiguration,
|
||||||
}) : assert(showCursor != null),
|
}) : assert(showCursor != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
@ -280,12 +286,7 @@ class SelectableText extends StatefulWidget {
|
|||||||
textSpan != null,
|
textSpan != null,
|
||||||
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
|
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
|
||||||
),
|
),
|
||||||
data = null,
|
data = null;
|
||||||
toolbarOptions = toolbarOptions ??
|
|
||||||
const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
copy: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The text to display.
|
/// The text to display.
|
||||||
///
|
///
|
||||||
@ -397,7 +398,11 @@ class SelectableText extends StatefulWidget {
|
|||||||
/// Paste and cut will be disabled regardless.
|
/// Paste and cut will be disabled regardless.
|
||||||
///
|
///
|
||||||
/// If not set, select all and copy will be enabled by default.
|
/// If not set, select all and copy will be enabled by default.
|
||||||
final ToolbarOptions toolbarOptions;
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final ToolbarOptions? toolbarOptions;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
||||||
bool get selectionEnabled => enableInteractiveSelection;
|
bool get selectionEnabled => enableInteractiveSelection;
|
||||||
@ -434,6 +439,15 @@ class SelectableText extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.editableText.onSelectionChanged}
|
/// {@macro flutter.widgets.editableText.onSelectionChanged}
|
||||||
final SelectionChangedCallback? onSelectionChanged;
|
final SelectionChangedCallback? onSelectionChanged;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||||
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return AdaptiveTextSelectionToolbar.editableText(
|
||||||
|
editableTextState: editableTextState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
|
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
|
||||||
///
|
///
|
||||||
/// {@macro flutter.widgets.magnifier.intro}
|
/// {@macro flutter.widgets.magnifier.intro}
|
||||||
@ -639,7 +653,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
|
|||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||||
forcePressEnabled = true;
|
forcePressEnabled = true;
|
||||||
textSelectionControls ??= cupertinoTextSelectionControls;
|
textSelectionControls ??= cupertinoTextSelectionHandleControls;
|
||||||
paintCursorAboveText = true;
|
paintCursorAboveText = true;
|
||||||
cursorOpacityAnimates = true;
|
cursorOpacityAnimates = true;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
||||||
@ -651,7 +665,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
|
|||||||
case TargetPlatform.macOS:
|
case TargetPlatform.macOS:
|
||||||
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
|
textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
|
||||||
paintCursorAboveText = true;
|
paintCursorAboveText = true;
|
||||||
cursorOpacityAnimates = true;
|
cursorOpacityAnimates = true;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
||||||
@ -663,7 +677,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
|
|||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= materialTextSelectionControls;
|
textSelectionControls ??= materialTextSelectionHandleControls;
|
||||||
paintCursorAboveText = false;
|
paintCursorAboveText = false;
|
||||||
cursorOpacityAnimates = false;
|
cursorOpacityAnimates = false;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
||||||
@ -673,7 +687,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
|
|||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
case TargetPlatform.windows:
|
case TargetPlatform.windows:
|
||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= desktopTextSelectionControls;
|
textSelectionControls ??= desktopTextSelectionHandleControls;
|
||||||
paintCursorAboveText = false;
|
paintCursorAboveText = false;
|
||||||
cursorOpacityAnimates = false;
|
cursorOpacityAnimates = false;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
||||||
@ -694,6 +708,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
|
|||||||
key: editableTextKey,
|
key: editableTextKey,
|
||||||
style: effectiveTextStyle,
|
style: effectiveTextStyle,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
toolbarOptions: widget.toolbarOptions,
|
||||||
textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||||
textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
|
textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
|
||||||
showSelectionHandles: _showSelectionHandles,
|
showSelectionHandles: _showSelectionHandles,
|
||||||
@ -706,7 +721,6 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
|
|||||||
textScaleFactor: widget.textScaleFactor,
|
textScaleFactor: widget.textScaleFactor,
|
||||||
autofocus: widget.autofocus,
|
autofocus: widget.autofocus,
|
||||||
forceLine: false,
|
forceLine: false,
|
||||||
toolbarOptions: widget.toolbarOptions,
|
|
||||||
minLines: widget.minLines,
|
minLines: widget.minLines,
|
||||||
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
||||||
selectionColor: selectionColor,
|
selectionColor: selectionColor,
|
||||||
@ -729,6 +743,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
|
|||||||
dragStartBehavior: widget.dragStartBehavior,
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
scrollPhysics: widget.scrollPhysics,
|
scrollPhysics: widget.scrollPhysics,
|
||||||
autofillHints: null,
|
autofillHints: null,
|
||||||
|
contextMenuBuilder: widget.contextMenuBuilder,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'adaptive_text_selection_toolbar.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'desktop_text_selection.dart';
|
import 'desktop_text_selection.dart';
|
||||||
import 'magnifier.dart';
|
import 'magnifier.dart';
|
||||||
@ -41,6 +42,7 @@ class SelectionArea extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
this.magnifierConfiguration,
|
this.magnifierConfiguration,
|
||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
required this.child,
|
required this.child,
|
||||||
@ -65,6 +67,23 @@ class SelectionArea extends StatefulWidget {
|
|||||||
/// If it is null, the platform specific selection control is used.
|
/// If it is null, the platform specific selection control is used.
|
||||||
final TextSelectionControls? selectionControls;
|
final TextSelectionControls? selectionControls;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||||
|
///
|
||||||
|
/// If not provided, will build a default menu based on the ambient
|
||||||
|
/// [ThemeData.platform].
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This example shows how to build a custom context menu for any selected
|
||||||
|
/// content in a SelectionArea.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which is built by default.
|
||||||
|
final SelectableRegionContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
/// Called when the selected content changes.
|
/// Called when the selected content changes.
|
||||||
final ValueChanged<SelectedContent?>? onSelectionChanged;
|
final ValueChanged<SelectedContent?>? onSelectionChanged;
|
||||||
|
|
||||||
@ -73,6 +92,12 @@ class SelectionArea extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(BuildContext context, SelectableRegionState selectableRegionState) {
|
||||||
|
return AdaptiveTextSelectionToolbar.selectableRegion(
|
||||||
|
selectableRegionState: selectableRegionState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _SelectionAreaState();
|
State<StatefulWidget> createState() => _SelectionAreaState();
|
||||||
}
|
}
|
||||||
@ -100,22 +125,24 @@ class _SelectionAreaState extends State<SelectionArea> {
|
|||||||
switch (Theme.of(context).platform) {
|
switch (Theme.of(context).platform) {
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
controls ??= materialTextSelectionControls;
|
controls ??= materialTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
controls ??= cupertinoTextSelectionControls;
|
controls ??= cupertinoTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
case TargetPlatform.windows:
|
case TargetPlatform.windows:
|
||||||
controls ??= desktopTextSelectionControls;
|
controls ??= desktopTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.macOS:
|
case TargetPlatform.macOS:
|
||||||
controls ??= cupertinoDesktopTextSelectionControls;
|
controls ??= cupertinoDesktopTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SelectableRegion(
|
return SelectableRegion(
|
||||||
focusNode: _effectiveFocusNode,
|
|
||||||
selectionControls: controls,
|
selectionControls: controls,
|
||||||
|
focusNode: _effectiveFocusNode,
|
||||||
|
contextMenuBuilder: widget.contextMenuBuilder,
|
||||||
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
|
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
|
||||||
onSelectionChanged: widget.onSelectionChanged,
|
onSelectionChanged: widget.onSelectionChanged,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'adaptive_text_selection_toolbar.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'desktop_text_selection.dart';
|
import 'desktop_text_selection.dart';
|
||||||
@ -263,7 +264,11 @@ class TextField extends StatefulWidget {
|
|||||||
this.textAlignVertical,
|
this.textAlignVertical,
|
||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
ToolbarOptions? toolbarOptions,
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
this.toolbarOptions,
|
||||||
this.showCursor,
|
this.showCursor,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.obscuringCharacter = '•',
|
this.obscuringCharacter = '•',
|
||||||
@ -305,6 +310,7 @@ class TextField extends StatefulWidget {
|
|||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.scribbleEnabled = true,
|
this.scribbleEnabled = true,
|
||||||
this.enableIMEPersonalizedLearning = true,
|
this.enableIMEPersonalizedLearning = true,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
this.spellCheckConfiguration,
|
this.spellCheckConfiguration,
|
||||||
this.magnifierConfiguration,
|
this.magnifierConfiguration,
|
||||||
}) : assert(textAlign != null),
|
}) : assert(textAlign != null),
|
||||||
@ -343,31 +349,7 @@ class TextField extends StatefulWidget {
|
|||||||
assert(clipBehavior != null),
|
assert(clipBehavior != null),
|
||||||
assert(enableIMEPersonalizedLearning != null),
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
|
||||||
toolbarOptions = toolbarOptions ??
|
|
||||||
(obscureText
|
|
||||||
? (readOnly
|
|
||||||
// No point in even offering "Select All" in a read-only obscured
|
|
||||||
// field.
|
|
||||||
? const ToolbarOptions()
|
|
||||||
// Writable, but obscured.
|
|
||||||
: const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
paste: true,
|
|
||||||
))
|
|
||||||
: (readOnly
|
|
||||||
// Read-only, not obscured.
|
|
||||||
? const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
copy: true,
|
|
||||||
)
|
|
||||||
// Writable, not obscured.
|
|
||||||
: const ToolbarOptions(
|
|
||||||
copy: true,
|
|
||||||
cut: true,
|
|
||||||
selectAll: true,
|
|
||||||
paste: true,
|
|
||||||
)));
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
|
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
|
||||||
///
|
///
|
||||||
@ -513,7 +495,11 @@ class TextField extends StatefulWidget {
|
|||||||
/// If not set, select all and paste will default to be enabled. Copy and cut
|
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||||
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||||
/// paste and cut will be disabled regardless.
|
/// paste and cut will be disabled regardless.
|
||||||
final ToolbarOptions toolbarOptions;
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final ToolbarOptions? toolbarOptions;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.showCursor}
|
/// {@macro flutter.widgets.editableText.showCursor}
|
||||||
final bool? showCursor;
|
final bool? showCursor;
|
||||||
@ -779,6 +765,21 @@ class TextField extends StatefulWidget {
|
|||||||
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
||||||
final bool enableIMEPersonalizedLearning;
|
final bool enableIMEPersonalizedLearning;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||||
|
///
|
||||||
|
/// If not provided, will build a default menu based on the platform.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which is built by default.
|
||||||
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return AdaptiveTextSelectionToolbar.editableText(
|
||||||
|
editableTextState: editableTextState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// {@macro flutter.widgets.EditableText.spellCheckConfiguration}
|
/// {@macro flutter.widgets.EditableText.spellCheckConfiguration}
|
||||||
///
|
///
|
||||||
/// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this
|
/// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this
|
||||||
@ -1208,7 +1209,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||||
forcePressEnabled = true;
|
forcePressEnabled = true;
|
||||||
textSelectionControls ??= cupertinoTextSelectionControls;
|
textSelectionControls ??= cupertinoTextSelectionHandleControls;
|
||||||
paintCursorAboveText = true;
|
paintCursorAboveText = true;
|
||||||
cursorOpacityAnimates = true;
|
cursorOpacityAnimates = true;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
||||||
@ -1221,7 +1222,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
case TargetPlatform.macOS:
|
case TargetPlatform.macOS:
|
||||||
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
|
textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
|
||||||
paintCursorAboveText = true;
|
paintCursorAboveText = true;
|
||||||
cursorOpacityAnimates = false;
|
cursorOpacityAnimates = false;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
||||||
@ -1239,7 +1240,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= materialTextSelectionControls;
|
textSelectionControls ??= materialTextSelectionHandleControls;
|
||||||
paintCursorAboveText = false;
|
paintCursorAboveText = false;
|
||||||
cursorOpacityAnimates = false;
|
cursorOpacityAnimates = false;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
||||||
@ -1248,7 +1249,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
|
|
||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= desktopTextSelectionControls;
|
textSelectionControls ??= desktopTextSelectionHandleControls;
|
||||||
paintCursorAboveText = false;
|
paintCursorAboveText = false;
|
||||||
cursorOpacityAnimates = false;
|
cursorOpacityAnimates = false;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
||||||
@ -1257,7 +1258,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
|
|
||||||
case TargetPlatform.windows:
|
case TargetPlatform.windows:
|
||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= desktopTextSelectionControls;
|
textSelectionControls ??= desktopTextSelectionHandleControls;
|
||||||
paintCursorAboveText = false;
|
paintCursorAboveText = false;
|
||||||
cursorOpacityAnimates = false;
|
cursorOpacityAnimates = false;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
||||||
@ -1334,6 +1335,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
restorationId: 'editable',
|
restorationId: 'editable',
|
||||||
scribbleEnabled: widget.scribbleEnabled,
|
scribbleEnabled: widget.scribbleEnabled,
|
||||||
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
||||||
|
contextMenuBuilder: widget.contextMenuBuilder,
|
||||||
spellCheckConfiguration: spellCheckConfiguration,
|
spellCheckConfiguration: spellCheckConfiguration,
|
||||||
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
|
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'adaptive_text_selection_toolbar.dart';
|
||||||
import 'input_decorator.dart';
|
import 'input_decorator.dart';
|
||||||
import 'text_field.dart';
|
import 'text_field.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
@ -110,6 +111,10 @@ class TextFormField extends FormField<String> {
|
|||||||
TextAlignVertical? textAlignVertical,
|
TextAlignVertical? textAlignVertical,
|
||||||
bool autofocus = false,
|
bool autofocus = false,
|
||||||
bool readOnly = false,
|
bool readOnly = false,
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
ToolbarOptions? toolbarOptions,
|
ToolbarOptions? toolbarOptions,
|
||||||
bool? showCursor,
|
bool? showCursor,
|
||||||
String obscuringCharacter = '•',
|
String obscuringCharacter = '•',
|
||||||
@ -148,6 +153,7 @@ class TextFormField extends FormField<String> {
|
|||||||
super.restorationId,
|
super.restorationId,
|
||||||
bool enableIMEPersonalizedLearning = true,
|
bool enableIMEPersonalizedLearning = true,
|
||||||
MouseCursor? mouseCursor,
|
MouseCursor? mouseCursor,
|
||||||
|
EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
}) : assert(initialValue == null || controller == null),
|
}) : assert(initialValue == null || controller == null),
|
||||||
assert(textAlign != null),
|
assert(textAlign != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
@ -236,6 +242,7 @@ class TextFormField extends FormField<String> {
|
|||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
|
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
|
||||||
mouseCursor: mouseCursor,
|
mouseCursor: mouseCursor,
|
||||||
|
contextMenuBuilder: contextMenuBuilder,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -247,6 +254,12 @@ class TextFormField extends FormField<String> {
|
|||||||
/// initialize its [TextEditingController.text] with [initialValue].
|
/// initialize its [TextEditingController.text] with [initialValue].
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return AdaptiveTextSelectionToolbar.editableText(
|
||||||
|
editableTextState: editableTextState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FormFieldState<String> createState() => _TextFormFieldState();
|
FormFieldState<String> createState() => _TextFormFieldState();
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,17 @@ const double _kHandleSize = 22.0;
|
|||||||
const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
|
const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
|
||||||
const double _kToolbarContentDistance = 8.0;
|
const double _kToolbarContentDistance = 8.0;
|
||||||
|
|
||||||
|
/// Android Material styled text selection handle controls.
|
||||||
|
///
|
||||||
|
/// Specifically does not manage the toolbar, which is left to
|
||||||
|
/// [EditableText.contextMenuBuilder].
|
||||||
|
@Deprecated(
|
||||||
|
'Use `MaterialTextSelectionControls`. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
class MaterialTextSelectionHandleControls extends MaterialTextSelectionControls with TextSelectionHandleControls {
|
||||||
|
}
|
||||||
|
|
||||||
/// Android Material styled text selection controls.
|
/// Android Material styled text selection controls.
|
||||||
///
|
///
|
||||||
/// The [materialTextSelectionControls] global variable has a
|
/// The [materialTextSelectionControls] global variable has a
|
||||||
@ -29,6 +40,10 @@ class MaterialTextSelectionControls extends TextSelectionControls {
|
|||||||
Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize);
|
Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize);
|
||||||
|
|
||||||
/// Builder for material-style copy/paste text selection toolbar.
|
/// Builder for material-style copy/paste text selection toolbar.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
Widget buildToolbar(
|
Widget buildToolbar(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -40,7 +55,7 @@ class MaterialTextSelectionControls extends TextSelectionControls {
|
|||||||
ClipboardStatusNotifier? clipboardStatus,
|
ClipboardStatusNotifier? clipboardStatus,
|
||||||
Offset? lastSecondaryTapDownPosition,
|
Offset? lastSecondaryTapDownPosition,
|
||||||
) {
|
) {
|
||||||
return _TextSelectionControlsToolbar(
|
return _TextSelectionControlsToolbar(
|
||||||
globalEditableRegion: globalEditableRegion,
|
globalEditableRegion: globalEditableRegion,
|
||||||
textLineHeight: textLineHeight,
|
textLineHeight: textLineHeight,
|
||||||
selectionMidpoint: selectionMidpoint,
|
selectionMidpoint: selectionMidpoint,
|
||||||
@ -107,6 +122,10 @@ class MaterialTextSelectionControls extends TextSelectionControls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
bool canSelectAll(TextSelectionDelegate delegate) {
|
bool canSelectAll(TextSelectionDelegate delegate) {
|
||||||
// Android allows SelectAll when selection is not collapsed, unless
|
// Android allows SelectAll when selection is not collapsed, unless
|
||||||
@ -183,8 +202,8 @@ class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToo
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -289,5 +308,12 @@ class _TextSelectionHandlePainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Text selection handle controls that follow the Material Design specification.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `materialTextSelectionControls` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final TextSelectionControls materialTextSelectionHandleControls = MaterialTextSelectionHandleControls();
|
||||||
|
|
||||||
/// Text selection controls that follow the Material Design specification.
|
/// Text selection controls that follow the Material Design specification.
|
||||||
final TextSelectionControls materialTextSelectionControls = MaterialTextSelectionControls();
|
final TextSelectionControls materialTextSelectionControls = MaterialTextSelectionControls();
|
||||||
|
@ -19,6 +19,12 @@ import 'material_localizations.dart';
|
|||||||
const double _kToolbarScreenPadding = 8.0;
|
const double _kToolbarScreenPadding = 8.0;
|
||||||
const double _kToolbarHeight = 44.0;
|
const double _kToolbarHeight = 44.0;
|
||||||
|
|
||||||
|
const double _kHandleSize = 22.0;
|
||||||
|
|
||||||
|
// Padding between the toolbar and the anchor.
|
||||||
|
const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
|
||||||
|
const double _kToolbarContentDistance = 8.0;
|
||||||
|
|
||||||
/// A fully-functional Material-style text selection toolbar.
|
/// A fully-functional Material-style text selection toolbar.
|
||||||
///
|
///
|
||||||
/// Tries to position itself above [anchorAbove], but if it doesn't fit, then
|
/// Tries to position itself above [anchorAbove], but if it doesn't fit, then
|
||||||
@ -29,8 +35,8 @@ const double _kToolbarHeight = 44.0;
|
|||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [TextSelectionControls.buildToolbar], where this is used by default to
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar for the current
|
||||||
/// build an Android-style toolbar.
|
/// platform.
|
||||||
/// * [CupertinoTextSelectionToolbar], which is similar, but builds an iOS-
|
/// * [CupertinoTextSelectionToolbar], which is similar, but builds an iOS-
|
||||||
/// style toolbar.
|
/// style toolbar.
|
||||||
class TextSelectionToolbar extends StatelessWidget {
|
class TextSelectionToolbar extends StatelessWidget {
|
||||||
@ -87,10 +93,17 @@ class TextSelectionToolbar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Incorporate the padding distance between the content and toolbar.
|
||||||
|
final Offset anchorAbovePadded =
|
||||||
|
anchorAbove - const Offset(0.0, _kToolbarContentDistance);
|
||||||
|
final Offset anchorBelowPadded =
|
||||||
|
anchorBelow + const Offset(0.0, _kToolbarContentDistanceBelow);
|
||||||
|
|
||||||
final double paddingAbove = MediaQuery.of(context).padding.top
|
final double paddingAbove = MediaQuery.of(context).padding.top
|
||||||
+ _kToolbarScreenPadding;
|
+ _kToolbarScreenPadding;
|
||||||
final double availableHeight = anchorAbove.dy - paddingAbove;
|
final double availableHeight = anchorAbovePadded.dy - _kToolbarContentDistance - paddingAbove;
|
||||||
final bool fitsAbove = _kToolbarHeight <= availableHeight;
|
final bool fitsAbove = _kToolbarHeight <= availableHeight;
|
||||||
|
// Makes up for the Padding above the Stack.
|
||||||
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -100,21 +113,17 @@ class TextSelectionToolbar extends StatelessWidget {
|
|||||||
_kToolbarScreenPadding,
|
_kToolbarScreenPadding,
|
||||||
_kToolbarScreenPadding,
|
_kToolbarScreenPadding,
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: CustomSingleChildLayout(
|
||||||
children: <Widget>[
|
delegate: TextSelectionToolbarLayoutDelegate(
|
||||||
CustomSingleChildLayout(
|
anchorAbove: anchorAbovePadded - localAdjustment,
|
||||||
delegate: TextSelectionToolbarLayoutDelegate(
|
anchorBelow: anchorBelowPadded - localAdjustment,
|
||||||
anchorAbove: anchorAbove - localAdjustment,
|
fitsAbove: fitsAbove,
|
||||||
anchorBelow: anchorBelow - localAdjustment,
|
),
|
||||||
fitsAbove: fitsAbove,
|
child: _TextSelectionToolbarOverflowable(
|
||||||
),
|
isAbove: fitsAbove,
|
||||||
child: _TextSelectionToolbarOverflowable(
|
toolbarBuilder: toolbarBuilder,
|
||||||
isAbove: fitsAbove,
|
children: children,
|
||||||
toolbarBuilder: toolbarBuilder,
|
),
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -156,8 +165,8 @@ class _TextSelectionToolbarOverflowableState extends State<_TextSelectionToolbar
|
|||||||
// changed and saved values are no longer relevant. This should be called in
|
// changed and saved values are no longer relevant. This should be called in
|
||||||
// setState or another context where a rebuild is happening.
|
// setState or another context where a rebuild is happening.
|
||||||
void _reset() {
|
void _reset() {
|
||||||
// Change _TextSelectionToolbarTrailingEdgeAlign's key when the menu changes in
|
// Change _TextSelectionToolbarTrailingEdgeAlign's key when the menu changes
|
||||||
// order to cause it to rebuild. This lets it recalculate its
|
// in order to cause it to rebuild. This lets it recalculate its
|
||||||
// saved width for the new set of children, and it prevents AnimatedSize
|
// saved width for the new set of children, and it prevents AnimatedSize
|
||||||
// from animating the size change.
|
// from animating the size change.
|
||||||
_containerKey = UniqueKey();
|
_containerKey = UniqueKey();
|
||||||
|
@ -1100,7 +1100,7 @@ class TextPainter {
|
|||||||
/// visually contiguous.
|
/// visually contiguous.
|
||||||
///
|
///
|
||||||
/// Leading or trailing newline characters will be represented by zero-width
|
/// Leading or trailing newline characters will be represented by zero-width
|
||||||
/// `Textbox`es.
|
/// `TextBox`es.
|
||||||
///
|
///
|
||||||
/// The method only returns `TextBox`es of glyphs that are entirely enclosed by
|
/// The method only returns `TextBox`es of glyphs that are entirely enclosed by
|
||||||
/// the given `selection`: a multi-code-unit glyph will be excluded if only
|
/// the given `selection`: a multi-code-unit glyph will be excluded if only
|
||||||
|
@ -1981,8 +1981,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
|||||||
Offset? _lastTapDownPosition;
|
Offset? _lastTapDownPosition;
|
||||||
Offset? _lastSecondaryTapDownPosition;
|
Offset? _lastSecondaryTapDownPosition;
|
||||||
|
|
||||||
|
/// {@template flutter.rendering.RenderEditable.lastSecondaryTapDownPosition}
|
||||||
/// The position of the most recent secondary tap down event on this text
|
/// The position of the most recent secondary tap down event on this text
|
||||||
/// input.
|
/// input.
|
||||||
|
/// {@endtemplate}
|
||||||
Offset? get lastSecondaryTapDownPosition => _lastSecondaryTapDownPosition;
|
Offset? get lastSecondaryTapDownPosition => _lastSecondaryTapDownPosition;
|
||||||
|
|
||||||
/// Tracks the position of a secondary tap event.
|
/// Tracks the position of a secondary tap event.
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
// 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 'framework.dart';
|
||||||
|
|
||||||
|
/// The buttons that can appear in a context menu by default.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [ContextMenuButtonItem], which uses this enum to describe a button in a
|
||||||
|
/// context menu.
|
||||||
|
enum ContextMenuButtonType {
|
||||||
|
/// A button that cuts the current text selection.
|
||||||
|
cut,
|
||||||
|
|
||||||
|
/// A button that copies the current text selection.
|
||||||
|
copy,
|
||||||
|
|
||||||
|
/// A button that pastes the clipboard contents into the focused text field.
|
||||||
|
paste,
|
||||||
|
|
||||||
|
/// A button that selects all the contents of the focused text field.
|
||||||
|
selectAll,
|
||||||
|
|
||||||
|
/// Anything other than the default button types.
|
||||||
|
custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type and callback for a context menu button.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which can take a list of
|
||||||
|
/// ContextMenuButtonItems and create a platform-specific context menu with
|
||||||
|
/// the indicated buttons.
|
||||||
|
@immutable
|
||||||
|
class ContextMenuButtonItem {
|
||||||
|
/// Creates a const instance of [ContextMenuButtonItem].
|
||||||
|
const ContextMenuButtonItem({
|
||||||
|
required this.onPressed,
|
||||||
|
this.type = ContextMenuButtonType.custom,
|
||||||
|
this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The callback to be called when the button is pressed.
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
/// The type of button this represents.
|
||||||
|
final ContextMenuButtonType type;
|
||||||
|
|
||||||
|
/// The label to display on the button.
|
||||||
|
///
|
||||||
|
/// If a [type] other than [ContextMenuButtonType.custom] is given
|
||||||
|
/// and a label is not provided, then the default label for that type for the
|
||||||
|
/// platform will be looked up.
|
||||||
|
final String? label;
|
||||||
|
|
||||||
|
/// Creates a new [ContextMenuButtonItem] with the provided parameters
|
||||||
|
/// overridden.
|
||||||
|
ContextMenuButtonItem copyWith({
|
||||||
|
VoidCallback? onPressed,
|
||||||
|
ContextMenuButtonType? type,
|
||||||
|
String? label,
|
||||||
|
}) {
|
||||||
|
return ContextMenuButtonItem(
|
||||||
|
onPressed: onPressed ?? this.onPressed,
|
||||||
|
type: type ?? this.type,
|
||||||
|
label: label ?? this.label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is ContextMenuButtonItem
|
||||||
|
&& other.label == label
|
||||||
|
&& other.onPressed == onPressed
|
||||||
|
&& other.type == type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(label, onPressed, type);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ContextMenuButtonItem $type, $label';
|
||||||
|
}
|
123
packages/flutter/lib/src/widgets/context_menu_controller.dart
Normal file
123
packages/flutter/lib/src/widgets/context_menu_controller.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// 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 'framework.dart';
|
||||||
|
import 'inherited_theme.dart';
|
||||||
|
import 'navigator.dart';
|
||||||
|
import 'overlay.dart';
|
||||||
|
|
||||||
|
/// Builds and manages a context menu at a given location.
|
||||||
|
///
|
||||||
|
/// There can only ever be one context menu shown at a given time in the entire
|
||||||
|
/// app.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This example shows how to use a GestureDetector to show a context menu
|
||||||
|
/// anywhere in a widget subtree that receives a right click or long press.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/context_menu/context_menu_controller.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
class ContextMenuController {
|
||||||
|
/// Creates a context menu that can be shown with [show].
|
||||||
|
ContextMenuController({
|
||||||
|
this.onRemove,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Called when this menu is removed.
|
||||||
|
final VoidCallback? onRemove;
|
||||||
|
|
||||||
|
/// The currently shown instance, if any.
|
||||||
|
static ContextMenuController? _shownInstance;
|
||||||
|
|
||||||
|
// The OverlayEntry is static because only one context menu can be displayed
|
||||||
|
// at one time.
|
||||||
|
static OverlayEntry? _menuOverlayEntry;
|
||||||
|
|
||||||
|
/// Shows the given context menu.
|
||||||
|
///
|
||||||
|
/// Since there can only be one shown context menu at a time, calling this
|
||||||
|
/// will also remove any other context menu that is visible.
|
||||||
|
void show({
|
||||||
|
required BuildContext context,
|
||||||
|
required WidgetBuilder contextMenuBuilder,
|
||||||
|
Widget? debugRequiredFor,
|
||||||
|
}) {
|
||||||
|
removeAny();
|
||||||
|
final OverlayState overlayState = Overlay.of(
|
||||||
|
context,
|
||||||
|
rootOverlay: true,
|
||||||
|
debugRequiredFor: debugRequiredFor,
|
||||||
|
);
|
||||||
|
final CapturedThemes capturedThemes = InheritedTheme.capture(
|
||||||
|
from: context,
|
||||||
|
to: Navigator.maybeOf(context)?.context,
|
||||||
|
);
|
||||||
|
|
||||||
|
_menuOverlayEntry = OverlayEntry(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return capturedThemes.wrap(contextMenuBuilder(context));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
overlayState.insert(_menuOverlayEntry!);
|
||||||
|
_shownInstance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the currently shown context menu from the UI.
|
||||||
|
///
|
||||||
|
/// Does nothing if no context menu is currently shown.
|
||||||
|
///
|
||||||
|
/// If a menu is removed, and that menu provided an [onRemove] callback when
|
||||||
|
/// it was created, then that callback will be called.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [remove], which removes only the current instance.
|
||||||
|
static void removeAny() {
|
||||||
|
_menuOverlayEntry?.remove();
|
||||||
|
_menuOverlayEntry = null;
|
||||||
|
if (_shownInstance != null) {
|
||||||
|
_shownInstance!.onRemove?.call();
|
||||||
|
_shownInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if and only if this menu is currently being shown.
|
||||||
|
bool get isShown => _shownInstance == this;
|
||||||
|
|
||||||
|
/// Cause the underlying [OverlayEntry] to rebuild during the next pipeline
|
||||||
|
/// flush.
|
||||||
|
///
|
||||||
|
/// It's necessary to call this function if the output of [contextMenuBuilder]
|
||||||
|
/// has changed.
|
||||||
|
///
|
||||||
|
/// Errors if the context menu is not currently shown.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [OverlayEntry.markNeedsBuild]
|
||||||
|
void markNeedsBuild() {
|
||||||
|
assert(isShown);
|
||||||
|
_menuOverlayEntry?.markNeedsBuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove this menu from the UI.
|
||||||
|
///
|
||||||
|
/// Does nothing if this instance is not currently shown. In other words, if
|
||||||
|
/// another context menu is currently shown, that menu will not be removed.
|
||||||
|
///
|
||||||
|
/// This method should only be called once. The instance cannot be shown again
|
||||||
|
/// after removing. Create a new instance.
|
||||||
|
///
|
||||||
|
/// If an [onRemove] method was given to this instance, it will be called.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [removeAny], which removes any shown instance of the context menu.
|
||||||
|
void remove() {
|
||||||
|
if (!isShown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeAny();
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
|
||||||
/// Positions the toolbar at [anchor] if it fits, otherwise moves it so that it
|
/// Positions the toolbar at [anchor] if it fits, otherwise moves it so that it
|
||||||
/// just fits fully on-screen.
|
/// just fits fully on-screen.
|
||||||
///
|
///
|
||||||
|
@ -19,6 +19,7 @@ import 'automatic_keep_alive.dart';
|
|||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'binding.dart';
|
import 'binding.dart';
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
|
import 'context_menu_button_item.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'default_selection_style.dart';
|
import 'default_selection_style.dart';
|
||||||
import 'default_text_editing_shortcuts.dart';
|
import 'default_text_editing_shortcuts.dart';
|
||||||
@ -39,6 +40,7 @@ import 'tap_region.dart';
|
|||||||
import 'text.dart';
|
import 'text.dart';
|
||||||
import 'text_editing_intents.dart';
|
import 'text_editing_intents.dart';
|
||||||
import 'text_selection.dart';
|
import 'text_selection.dart';
|
||||||
|
import 'text_selection_toolbar_anchors.dart';
|
||||||
import 'ticker_provider.dart';
|
import 'ticker_provider.dart';
|
||||||
import 'widget_span.dart';
|
import 'widget_span.dart';
|
||||||
|
|
||||||
@ -54,6 +56,18 @@ typedef SelectionChangedCallback = void Function(TextSelection selection, Select
|
|||||||
/// Signature for the callback that reports the app private command results.
|
/// Signature for the callback that reports the app private command results.
|
||||||
typedef AppPrivateCommandCallback = void Function(String, Map<String, dynamic>);
|
typedef AppPrivateCommandCallback = void Function(String, Map<String, dynamic>);
|
||||||
|
|
||||||
|
/// Signature for a widget builder that builds a context menu for the given
|
||||||
|
/// [EditableTextState].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [SelectableRegionContextMenuBuilder], which performs the same role for
|
||||||
|
/// [SelectableRegion].
|
||||||
|
typedef EditableTextContextMenuBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
);
|
||||||
|
|
||||||
// The time it takes for the cursor to fade from fully opaque to fully
|
// The time it takes for the cursor to fade from fully opaque to fully
|
||||||
// transparent and vice versa. A full cursor blink, from transparent to opaque
|
// transparent and vice versa. A full cursor blink, from transparent to opaque
|
||||||
// to transparent, is twice this duration.
|
// to transparent, is twice this duration.
|
||||||
@ -264,10 +278,18 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
|
|||||||
/// [EditableText] and its derived widgets have their own default [ToolbarOptions].
|
/// [EditableText] and its derived widgets have their own default [ToolbarOptions].
|
||||||
/// Create a custom [ToolbarOptions] if you want explicit control over the toolbar
|
/// Create a custom [ToolbarOptions] if you want explicit control over the toolbar
|
||||||
/// option.
|
/// option.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
class ToolbarOptions {
|
class ToolbarOptions {
|
||||||
/// Create a toolbar configuration with given options.
|
/// Create a toolbar configuration with given options.
|
||||||
///
|
///
|
||||||
/// All options default to false if they are not explicitly set.
|
/// All options default to false if they are not explicitly set.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
const ToolbarOptions({
|
const ToolbarOptions({
|
||||||
this.copy = false,
|
this.copy = false,
|
||||||
this.cut = false,
|
this.cut = false,
|
||||||
@ -278,6 +300,9 @@ class ToolbarOptions {
|
|||||||
assert(paste != null),
|
assert(paste != null),
|
||||||
assert(selectAll != null);
|
assert(selectAll != null);
|
||||||
|
|
||||||
|
/// An instance of [ToolbarOptions] with no options enabled.
|
||||||
|
static const ToolbarOptions empty = ToolbarOptions();
|
||||||
|
|
||||||
/// Whether to show copy option in toolbar.
|
/// Whether to show copy option in toolbar.
|
||||||
///
|
///
|
||||||
/// Defaults to false. Must not be null.
|
/// Defaults to false. Must not be null.
|
||||||
@ -637,6 +662,10 @@ class EditableText extends StatefulWidget {
|
|||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.scrollPhysics,
|
this.scrollPhysics,
|
||||||
this.autocorrectionTextRectColor,
|
this.autocorrectionTextRectColor,
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
ToolbarOptions? toolbarOptions,
|
ToolbarOptions? toolbarOptions,
|
||||||
this.autofillHints = const <String>[],
|
this.autofillHints = const <String>[],
|
||||||
this.autofillClient,
|
this.autofillClient,
|
||||||
@ -645,6 +674,7 @@ class EditableText extends StatefulWidget {
|
|||||||
this.scrollBehavior,
|
this.scrollBehavior,
|
||||||
this.scribbleEnabled = true,
|
this.scribbleEnabled = true,
|
||||||
this.enableIMEPersonalizedLearning = true,
|
this.enableIMEPersonalizedLearning = true,
|
||||||
|
this.contextMenuBuilder,
|
||||||
this.spellCheckConfiguration,
|
this.spellCheckConfiguration,
|
||||||
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
|
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
|
||||||
}) : assert(controller != null),
|
}) : assert(controller != null),
|
||||||
@ -683,12 +713,12 @@ class EditableText extends StatefulWidget {
|
|||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||||
toolbarOptions = toolbarOptions ??
|
toolbarOptions = selectionControls is TextSelectionHandleControls && toolbarOptions == null ? ToolbarOptions.empty : toolbarOptions ??
|
||||||
(obscureText
|
(obscureText
|
||||||
? (readOnly
|
? (readOnly
|
||||||
// No point in even offering "Select All" in a read-only obscured
|
// No point in even offering "Select All" in a read-only obscured
|
||||||
// field.
|
// field.
|
||||||
? const ToolbarOptions()
|
? ToolbarOptions.empty
|
||||||
// Writable, but obscured.
|
// Writable, but obscured.
|
||||||
: const ToolbarOptions(
|
: const ToolbarOptions(
|
||||||
selectAll: true,
|
selectAll: true,
|
||||||
@ -1564,6 +1594,41 @@ class EditableText extends StatefulWidget {
|
|||||||
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
||||||
final bool enableIMEPersonalizedLearning;
|
final bool enableIMEPersonalizedLearning;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.EditableText.contextMenuBuilder}
|
||||||
|
/// Builds the text selection toolbar when requested by the user.
|
||||||
|
///
|
||||||
|
/// `primaryAnchor` is the desired anchor position for the context menu, while
|
||||||
|
/// `secondaryAnchor` is the fallback location if the menu doesn't fit.
|
||||||
|
///
|
||||||
|
/// `buttonItems` represents the buttons that would be built by default for
|
||||||
|
/// this widget.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This example shows how to customize the menu, in this case by keeping the
|
||||||
|
/// default buttons for the platform but modifying their appearance.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This example shows how to show a custom button only when an email address
|
||||||
|
/// is currently selected.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which builds the default text selection
|
||||||
|
/// toolbar for the current platform, but allows customization of the
|
||||||
|
/// buttons.
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
|
||||||
|
/// button Widgets for the current platform given
|
||||||
|
/// [ContextMenuButtonItem]s.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// If not provided, no context menu will be shown.
|
||||||
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
/// {@template flutter.widgets.EditableText.spellCheckConfiguration}
|
/// {@template flutter.widgets.EditableText.spellCheckConfiguration}
|
||||||
/// Configuration that details how spell check should be performed.
|
/// Configuration that details how spell check should be performed.
|
||||||
///
|
///
|
||||||
@ -1587,6 +1652,61 @@ class EditableText extends StatefulWidget {
|
|||||||
|
|
||||||
bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText);
|
bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText);
|
||||||
|
|
||||||
|
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
|
||||||
|
/// platform's default selection menu for an editable field.
|
||||||
|
///
|
||||||
|
/// For example, [EditableText] uses this to generate the default buttons for
|
||||||
|
/// its context menu.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [EditableTextState.contextMenuButtonItems], which gives the
|
||||||
|
/// [ContextMenuButtonItem]s for a specific EditableText.
|
||||||
|
/// * [SelectableRegion.getSelectableButtonItems], which performs a similar
|
||||||
|
/// role but for content that is selectable but not editable.
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
|
||||||
|
/// take a list of [ContextMenuButtonItem]s with
|
||||||
|
/// [AdaptiveTextSelectionToolbar.buttonItems].
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
|
||||||
|
/// Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||||
|
static List<ContextMenuButtonItem> getEditableButtonItems({
|
||||||
|
required final ClipboardStatus? clipboardStatus,
|
||||||
|
required final VoidCallback? onCopy,
|
||||||
|
required final VoidCallback? onCut,
|
||||||
|
required final VoidCallback? onPaste,
|
||||||
|
required final VoidCallback? onSelectAll,
|
||||||
|
}) {
|
||||||
|
// If the paste button is enabled, don't render anything until the state
|
||||||
|
// of the clipboard is known, since it's used to determine if paste is
|
||||||
|
// shown.
|
||||||
|
if (onPaste != null && clipboardStatus == ClipboardStatus.unknown) {
|
||||||
|
return <ContextMenuButtonItem>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ContextMenuButtonItem>[
|
||||||
|
if (onCut != null)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: onCut,
|
||||||
|
type: ContextMenuButtonType.cut,
|
||||||
|
),
|
||||||
|
if (onCopy != null)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: onCopy,
|
||||||
|
type: ContextMenuButtonType.copy,
|
||||||
|
),
|
||||||
|
if (onPaste != null)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: onPaste,
|
||||||
|
type: ContextMenuButtonType.paste,
|
||||||
|
),
|
||||||
|
if (onSelectAll != null)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: onSelectAll,
|
||||||
|
type: ContextMenuButtonType.selectAll,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Infer the keyboard type of an `EditableText` if it's not specified.
|
// Infer the keyboard type of an `EditableText` if it's not specified.
|
||||||
static TextInputType _inferKeyboardType({
|
static TextInputType _inferKeyboardType({
|
||||||
required Iterable<String>? autofillHints,
|
required Iterable<String>? autofillHints,
|
||||||
@ -1778,7 +1898,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
|
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
|
||||||
final GlobalKey _editableKey = GlobalKey();
|
final GlobalKey _editableKey = GlobalKey();
|
||||||
final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
|
|
||||||
|
/// Detects whether the clipboard can paste.
|
||||||
|
final ClipboardStatusNotifier? clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
|
||||||
|
|
||||||
TextInputConnection? _textInputConnection;
|
TextInputConnection? _textInputConnection;
|
||||||
bool get _hasInputConnection => _textInputConnection?.attached ?? false;
|
bool get _hasInputConnection => _textInputConnection?.attached ?? false;
|
||||||
@ -1854,16 +1976,61 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
|
bool get cutEnabled {
|
||||||
|
if (widget.selectionControls is! TextSelectionHandleControls) {
|
||||||
|
return widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
|
||||||
|
}
|
||||||
|
return !widget.readOnly
|
||||||
|
&& !widget.obscureText
|
||||||
|
&& !textEditingValue.selection.isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get copyEnabled => widget.toolbarOptions.copy && !widget.obscureText;
|
bool get copyEnabled {
|
||||||
|
if (widget.selectionControls is! TextSelectionHandleControls) {
|
||||||
|
return widget.toolbarOptions.copy && !widget.obscureText;
|
||||||
|
}
|
||||||
|
return !widget.obscureText
|
||||||
|
&& !textEditingValue.selection.isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
|
bool get pasteEnabled {
|
||||||
|
if (widget.selectionControls is! TextSelectionHandleControls) {
|
||||||
|
return widget.toolbarOptions.paste && !widget.readOnly;
|
||||||
|
}
|
||||||
|
return !widget.readOnly
|
||||||
|
&& (clipboardStatus == null
|
||||||
|
|| clipboardStatus!.value == ClipboardStatus.pasteable);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get selectAllEnabled => widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
|
bool get selectAllEnabled {
|
||||||
|
if (widget.selectionControls is! TextSelectionHandleControls) {
|
||||||
|
return widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.enableInteractiveSelection
|
||||||
|
|| (widget.readOnly
|
||||||
|
&& widget.obscureText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return false;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return textEditingValue.text.isNotEmpty
|
||||||
|
&& textEditingValue.selection.isCollapsed;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return textEditingValue.text.isNotEmpty
|
||||||
|
&& !(textEditingValue.selection.start == 0
|
||||||
|
&& textEditingValue.selection.end == textEditingValue.text.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onChangedClipboardStatus() {
|
void _onChangedClipboardStatus() {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -1912,7 +2079,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_clipboardStatus?.update();
|
clipboardStatus?.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cut current selection to [Clipboard].
|
/// Cut current selection to [Clipboard].
|
||||||
@ -1938,7 +2105,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
});
|
});
|
||||||
hideToolbar();
|
hideToolbar();
|
||||||
}
|
}
|
||||||
_clipboardStatus?.update();
|
clipboardStatus?.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paste text from [Clipboard].
|
/// Paste text from [Clipboard].
|
||||||
@ -1997,6 +2164,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (cause == SelectionChangedCause.toolbar) {
|
if (cause == SelectionChangedCause.toolbar) {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
hideToolbar();
|
||||||
|
}
|
||||||
switch (defaultTargetPlatform) {
|
switch (defaultTargetPlatform) {
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
@ -2033,12 +2210,154 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
return configuration.copyWith(spellCheckService: spellCheckService);
|
return configuration.copyWith(spellCheckService: spellCheckService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the [ContextMenuButtonItem]s for the given [ToolbarOptions].
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead of `toolbarOptions`. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
List<ContextMenuButtonItem>? buttonItemsForToolbarOptions([TargetPlatform? targetPlatform]) {
|
||||||
|
final ToolbarOptions toolbarOptions = widget.toolbarOptions;
|
||||||
|
if (toolbarOptions == ToolbarOptions.empty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <ContextMenuButtonItem>[
|
||||||
|
if (toolbarOptions.cut && cutEnabled)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
selectAll(SelectionChangedCause.toolbar);
|
||||||
|
},
|
||||||
|
type: ContextMenuButtonType.selectAll,
|
||||||
|
),
|
||||||
|
if (toolbarOptions.copy && copyEnabled)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
copySelection(SelectionChangedCause.toolbar);
|
||||||
|
},
|
||||||
|
type: ContextMenuButtonType.copy,
|
||||||
|
),
|
||||||
|
if (toolbarOptions.paste && clipboardStatus != null && pasteEnabled)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
pasteText(SelectionChangedCause.toolbar);
|
||||||
|
},
|
||||||
|
type: ContextMenuButtonType.paste,
|
||||||
|
),
|
||||||
|
if (toolbarOptions.selectAll && selectAllEnabled)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
selectAll(SelectionChangedCause.toolbar);
|
||||||
|
},
|
||||||
|
type: ContextMenuButtonType.selectAll,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the line heights at the start and end of the selection for the given
|
||||||
|
/// [EditableTextState].
|
||||||
|
_GlyphHeights _getGlyphHeights() {
|
||||||
|
final TextSelection selection = textEditingValue.selection;
|
||||||
|
|
||||||
|
// Only calculate handle rects if the text in the previous frame
|
||||||
|
// is the same as the text in the current frame. This is done because
|
||||||
|
// widget.renderObject contains the renderEditable from the previous frame.
|
||||||
|
// If the text changed between the current and previous frames then
|
||||||
|
// widget.renderObject.getRectForComposingRange might fail. In cases where
|
||||||
|
// the current frame is different from the previous we fall back to
|
||||||
|
// renderObject.preferredLineHeight.
|
||||||
|
final InlineSpan span = renderEditable.text!;
|
||||||
|
final String prevText = span.toPlainText();
|
||||||
|
final String currText = textEditingValue.text;
|
||||||
|
if (prevText != currText || selection == null || !selection.isValid || selection.isCollapsed) {
|
||||||
|
return _GlyphHeights(
|
||||||
|
start: renderEditable.preferredLineHeight,
|
||||||
|
end: renderEditable.preferredLineHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String selectedGraphemes = selection.textInside(currText);
|
||||||
|
final int firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
|
||||||
|
final Rect? startCharacterRect = renderEditable.getRectForComposingRange(TextRange(
|
||||||
|
start: selection.start,
|
||||||
|
end: selection.start + firstSelectedGraphemeExtent,
|
||||||
|
));
|
||||||
|
final int lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
|
||||||
|
final Rect? endCharacterRect = renderEditable.getRectForComposingRange(TextRange(
|
||||||
|
start: selection.end - lastSelectedGraphemeExtent,
|
||||||
|
end: selection.end,
|
||||||
|
));
|
||||||
|
return _GlyphHeights(
|
||||||
|
start: startCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
||||||
|
end: endCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.EditableText.getAnchors}
|
||||||
|
/// Returns the anchor points for the default context menu.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s
|
||||||
|
/// for the default context menu buttons.
|
||||||
|
TextSelectionToolbarAnchors get contextMenuAnchors {
|
||||||
|
if (renderEditable.lastSecondaryTapDownPosition != null) {
|
||||||
|
return TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: renderEditable.lastSecondaryTapDownPosition!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _GlyphHeights glyphHeights = _getGlyphHeights();
|
||||||
|
final TextSelection selection = textEditingValue.selection;
|
||||||
|
final List<TextSelectionPoint> points =
|
||||||
|
renderEditable.getEndpointsForSelection(selection);
|
||||||
|
return TextSelectionToolbarAnchors.fromSelection(
|
||||||
|
renderBox: renderEditable,
|
||||||
|
startGlyphHeight: glyphHeights.start,
|
||||||
|
endGlyphHeight: glyphHeights.end,
|
||||||
|
selectionEndpoints: points,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
|
||||||
|
/// platform's default selection menu for [EditableText].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [EditableText.getEditableButtonItems], which performs a similar role,
|
||||||
|
/// but for any editable field, not just specifically EditableText.
|
||||||
|
/// * [SelectableRegionState.contextMenuButtonItems], which peforms a similar
|
||||||
|
/// role but for content that is selectable but not editable.
|
||||||
|
/// * [contextMenuAnchors], which provides the anchor points for the default
|
||||||
|
/// context menu.
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
|
||||||
|
/// take a list of [ContextMenuButtonItem]s with
|
||||||
|
/// [AdaptiveTextSelectionToolbar.buttonItems].
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
|
||||||
|
/// button Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||||
|
List<ContextMenuButtonItem> get contextMenuButtonItems {
|
||||||
|
return buttonItemsForToolbarOptions() ?? EditableText.getEditableButtonItems(
|
||||||
|
clipboardStatus: clipboardStatus?.value,
|
||||||
|
onCopy: copyEnabled
|
||||||
|
? () => copySelection(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
onCut: cutEnabled
|
||||||
|
? () => cutSelection(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
onPaste: pasteEnabled
|
||||||
|
? () => pasteText(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
onSelectAll: selectAllEnabled
|
||||||
|
? () => selectAll(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// State lifecycle:
|
// State lifecycle:
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
widget.focusNode.addListener(_handleFocusChanged);
|
widget.focusNode.addListener(_handleFocusChanged);
|
||||||
_scrollController.addListener(_onEditableScroll);
|
_scrollController.addListener(_onEditableScroll);
|
||||||
@ -2159,8 +2478,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (widget.selectionEnabled && pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false)) {
|
final bool canPaste = widget.selectionControls is TextSelectionHandleControls
|
||||||
_clipboardStatus?.update();
|
? pasteEnabled
|
||||||
|
: widget.selectionControls?.canPaste(this) ?? false;
|
||||||
|
if (widget.selectionEnabled && pasteEnabled && clipboardStatus != null && canPaste) {
|
||||||
|
clipboardStatus!.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2181,8 +2503,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
widget.focusNode.removeListener(_handleFocusChanged);
|
widget.focusNode.removeListener(_handleFocusChanged);
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||||
_clipboardStatus?.dispose();
|
clipboardStatus?.dispose();
|
||||||
_cursorVisibilityNotifier.dispose();
|
_cursorVisibilityNotifier.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
|
assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
|
||||||
@ -2733,7 +3055,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
TextSelectionOverlay _createSelectionOverlay() {
|
TextSelectionOverlay _createSelectionOverlay() {
|
||||||
final TextSelectionOverlay selectionOverlay = TextSelectionOverlay(
|
final TextSelectionOverlay selectionOverlay = TextSelectionOverlay(
|
||||||
clipboardStatus: _clipboardStatus,
|
clipboardStatus: clipboardStatus,
|
||||||
context: context,
|
context: context,
|
||||||
value: _value,
|
value: _value,
|
||||||
debugRequiredFor: widget,
|
debugRequiredFor: widget,
|
||||||
@ -2745,6 +3067,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
selectionDelegate: this,
|
selectionDelegate: this,
|
||||||
dragStartBehavior: widget.dragStartBehavior,
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
onSelectionHandleTapped: widget.onSelectionHandleTapped,
|
onSelectionHandleTapped: widget.onSelectionHandleTapped,
|
||||||
|
contextMenuBuilder: widget.contextMenuBuilder == null
|
||||||
|
? null
|
||||||
|
: (BuildContext context) {
|
||||||
|
return widget.contextMenuBuilder!(
|
||||||
|
context,
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
},
|
||||||
magnifierConfiguration: widget.magnifierConfiguration,
|
magnifierConfiguration: widget.magnifierConfiguration,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2784,7 +3114,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (widget.selectionControls == null) {
|
if (widget.selectionControls == null && widget.contextMenuBuilder == null) {
|
||||||
_selectionOverlay?.dispose();
|
_selectionOverlay?.dispose();
|
||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
} else {
|
} else {
|
||||||
@ -3305,10 +3635,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_selectionOverlay == null || _selectionOverlay!.toolbarIsVisible) {
|
if (_selectionOverlay == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_clipboardStatus?.update();
|
clipboardStatus?.update();
|
||||||
_selectionOverlay!.showToolbar();
|
_selectionOverlay!.showToolbar();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -3356,13 +3686,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Hides the magnifier if it is visible.
|
/// Hides the magnifier if it is visible.
|
||||||
void hideMagnifier({required bool shouldShowToolbar}) {
|
void hideMagnifier() {
|
||||||
if (_selectionOverlay == null) {
|
if (_selectionOverlay == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_selectionOverlay!.magnifierIsVisible) {
|
if (_selectionOverlay!.magnifierIsVisible) {
|
||||||
_selectionOverlay!.hideMagnifier(shouldShowToolbar: shouldShowToolbar);
|
_selectionOverlay!.hideMagnifier();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3427,29 +3757,41 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) {
|
VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) {
|
||||||
return widget.selectionEnabled
|
return widget.selectionEnabled
|
||||||
&& copyEnabled
|
|
||||||
&& _hasFocus
|
&& _hasFocus
|
||||||
&& (controls?.canCopy(this) ?? false)
|
&& (widget.selectionControls is TextSelectionHandleControls
|
||||||
? () => controls!.handleCopy(this)
|
? copyEnabled
|
||||||
|
: copyEnabled && (widget.selectionControls?.canCopy(this) ?? false))
|
||||||
|
? () {
|
||||||
|
controls?.handleCopy(this);
|
||||||
|
copySelection(SelectionChangedCause.toolbar);
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
VoidCallback? _semanticsOnCut(TextSelectionControls? controls) {
|
VoidCallback? _semanticsOnCut(TextSelectionControls? controls) {
|
||||||
return widget.selectionEnabled
|
return widget.selectionEnabled
|
||||||
&& cutEnabled
|
|
||||||
&& _hasFocus
|
&& _hasFocus
|
||||||
&& (controls?.canCut(this) ?? false)
|
&& (widget.selectionControls is TextSelectionHandleControls
|
||||||
? () => controls!.handleCut(this)
|
? cutEnabled
|
||||||
|
: cutEnabled && (widget.selectionControls?.canCut(this) ?? false))
|
||||||
|
? () {
|
||||||
|
controls?.handleCut(this);
|
||||||
|
cutSelection(SelectionChangedCause.toolbar);
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) {
|
VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) {
|
||||||
return widget.selectionEnabled
|
return widget.selectionEnabled
|
||||||
&& pasteEnabled
|
|
||||||
&& _hasFocus
|
&& _hasFocus
|
||||||
&& (controls?.canPaste(this) ?? false)
|
&& (widget.selectionControls is TextSelectionHandleControls
|
||||||
&& (_clipboardStatus == null || _clipboardStatus!.value == ClipboardStatus.pasteable)
|
? pasteEnabled
|
||||||
? () => controls!.handlePaste(this)
|
: pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false))
|
||||||
|
&& (clipboardStatus == null || clipboardStatus!.value == ClipboardStatus.pasteable)
|
||||||
|
? () {
|
||||||
|
controls?.handlePaste(this);
|
||||||
|
pasteText(SelectionChangedCause.toolbar);
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4983,3 +5325,18 @@ _Throttled<T> _throttle<T>({
|
|||||||
return timer!;
|
return timer!;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The start and end glyph heights of some range of text.
|
||||||
|
@immutable
|
||||||
|
class _GlyphHeights {
|
||||||
|
const _GlyphHeights({
|
||||||
|
required this.start,
|
||||||
|
required this.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The glyph height of the first line.
|
||||||
|
final double start;
|
||||||
|
|
||||||
|
/// The glyph height of the last line.
|
||||||
|
final double end;
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ import 'package:vector_math/vector_math_64.dart';
|
|||||||
|
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
|
import 'context_menu_button_item.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'focus_manager.dart';
|
import 'focus_manager.dart';
|
||||||
import 'focus_scope.dart';
|
import 'focus_scope.dart';
|
||||||
@ -25,6 +26,7 @@ import 'platform_selectable_region_context_menu.dart';
|
|||||||
import 'selection_container.dart';
|
import 'selection_container.dart';
|
||||||
import 'text_editing_intents.dart';
|
import 'text_editing_intents.dart';
|
||||||
import 'text_selection.dart';
|
import 'text_selection.dart';
|
||||||
|
import 'text_selection_toolbar_anchors.dart';
|
||||||
|
|
||||||
// Examples can assume:
|
// Examples can assume:
|
||||||
// FocusNode _focusNode = FocusNode();
|
// FocusNode _focusNode = FocusNode();
|
||||||
@ -200,6 +202,7 @@ class SelectableRegion extends StatefulWidget {
|
|||||||
/// toolbar for mobile devices.
|
/// toolbar for mobile devices.
|
||||||
const SelectableRegion({
|
const SelectableRegion({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.contextMenuBuilder,
|
||||||
required this.focusNode,
|
required this.focusNode,
|
||||||
required this.selectionControls,
|
required this.selectionControls,
|
||||||
required this.child,
|
required this.child,
|
||||||
@ -224,6 +227,9 @@ class SelectableRegion extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||||
|
final SelectableRegionContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
/// The delegate to build the selection handles and toolbar for mobile
|
/// The delegate to build the selection handles and toolbar for mobile
|
||||||
/// devices.
|
/// devices.
|
||||||
///
|
///
|
||||||
@ -234,11 +240,54 @@ class SelectableRegion extends StatefulWidget {
|
|||||||
/// Called when the selected content changes.
|
/// Called when the selected content changes.
|
||||||
final ValueChanged<SelectedContent?>? onSelectionChanged;
|
final ValueChanged<SelectedContent?>? onSelectionChanged;
|
||||||
|
|
||||||
|
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
|
||||||
|
/// platform's default selection menu.
|
||||||
|
///
|
||||||
|
/// For example, [SelectableRegion] uses this to generate the default buttons
|
||||||
|
/// for its context menu.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [SelectableRegionState.contextMenuButtonItems], which gives the
|
||||||
|
/// [ContextMenuButtonItem]s for a specific SelectableRegion.
|
||||||
|
/// * [EditableText.getEditableButtonItems], which performs a similar role but
|
||||||
|
/// for content that is both selectable and editable.
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
|
||||||
|
/// take a list of [ContextMenuButtonItem]s with
|
||||||
|
/// [AdaptiveTextSelectionToolbar.buttonItems].
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
|
||||||
|
/// Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||||
|
static List<ContextMenuButtonItem> getSelectableButtonItems({
|
||||||
|
required final SelectionGeometry selectionGeometry,
|
||||||
|
required final VoidCallback onCopy,
|
||||||
|
required final VoidCallback onSelectAll,
|
||||||
|
}) {
|
||||||
|
final bool canCopy = selectionGeometry.hasSelection;
|
||||||
|
final bool canSelectAll = selectionGeometry.hasContent;
|
||||||
|
|
||||||
|
// Determine which buttons will appear so that the order and total number is
|
||||||
|
// known. A button's position in the menu can slightly affect its
|
||||||
|
// appearance.
|
||||||
|
return <ContextMenuButtonItem>[
|
||||||
|
if (canCopy)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: onCopy,
|
||||||
|
type: ContextMenuButtonType.copy,
|
||||||
|
),
|
||||||
|
if (canSelectAll)
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: onSelectAll,
|
||||||
|
type: ContextMenuButtonType.selectAll,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _SelectableRegionState();
|
State<StatefulWidget> createState() => SelectableRegionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SelectableRegionState extends State<SelectableRegion> with TextSelectionDelegate implements SelectionRegistrar {
|
/// State for a [SelectableRegion].
|
||||||
|
class SelectableRegionState extends State<SelectableRegion> with TextSelectionDelegate implements SelectionRegistrar {
|
||||||
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
|
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
|
||||||
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
||||||
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
|
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
|
||||||
@ -258,6 +307,9 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
Orientation? _lastOrientation;
|
Orientation? _lastOrientation;
|
||||||
SelectedContent? _lastSelectedContent;
|
SelectedContent? _lastSelectedContent;
|
||||||
|
|
||||||
|
/// {@macro flutter.rendering.RenderEditable.lastSecondaryTapDownPosition}
|
||||||
|
Offset? lastSecondaryTapDownPosition;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -424,6 +476,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleRightClickDown(TapDownDetails details) {
|
void _handleRightClickDown(TapDownDetails details) {
|
||||||
|
lastSecondaryTapDownPosition = details.globalPosition;
|
||||||
widget.focusNode.requestFocus();
|
widget.focusNode.requestFocus();
|
||||||
_selectWordAt(offset: details.globalPosition);
|
_selectWordAt(offset: details.globalPosition);
|
||||||
_showHandles();
|
_showHandles();
|
||||||
@ -465,7 +518,17 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onAnyDragEnd(DragEndDetails details) {
|
void _onAnyDragEnd(DragEndDetails details) {
|
||||||
_selectionOverlay!.hideMagnifier(shouldShowToolbar: true);
|
if (widget.selectionControls is! TextSelectionHandleControls) {
|
||||||
|
_selectionOverlay!.hideMagnifier();
|
||||||
|
_selectionOverlay!.showToolbar();
|
||||||
|
} else {
|
||||||
|
_selectionOverlay!.hideMagnifier();
|
||||||
|
_selectionOverlay!.showToolbar(
|
||||||
|
contextMenuBuilder: (BuildContext context) {
|
||||||
|
return widget.contextMenuBuilder!(context, this);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
_stopSelectionEndEdgeUpdate();
|
_stopSelectionEndEdgeUpdate();
|
||||||
_updateSelectedContentIfNeeded();
|
_updateSelectedContentIfNeeded();
|
||||||
}
|
}
|
||||||
@ -516,8 +579,6 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
late Offset _selectionStartHandleDragPosition;
|
late Offset _selectionStartHandleDragPosition;
|
||||||
late Offset _selectionEndHandleDragPosition;
|
late Offset _selectionEndHandleDragPosition;
|
||||||
|
|
||||||
late List<TextSelectionPoint> points;
|
|
||||||
|
|
||||||
void _handleSelectionStartHandleDragStart(DragStartDetails details) {
|
void _handleSelectionStartHandleDragStart(DragStartDetails details) {
|
||||||
assert(_selectionDelegate.value.startSelectionPoint != null);
|
assert(_selectionDelegate.value.startSelectionPoint != null);
|
||||||
|
|
||||||
@ -595,19 +656,6 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
}
|
}
|
||||||
final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
|
final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
|
||||||
final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
|
final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
|
||||||
final Offset startLocalPosition = start?.localPosition ?? end!.localPosition;
|
|
||||||
final Offset endLocalPosition = end?.localPosition ?? start!.localPosition;
|
|
||||||
if (startLocalPosition.dy > endLocalPosition.dy) {
|
|
||||||
points = <TextSelectionPoint>[
|
|
||||||
TextSelectionPoint(endLocalPosition, TextDirection.ltr),
|
|
||||||
TextSelectionPoint(startLocalPosition, TextDirection.ltr),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
points = <TextSelectionPoint>[
|
|
||||||
TextSelectionPoint(startLocalPosition, TextDirection.ltr),
|
|
||||||
TextSelectionPoint(endLocalPosition, TextDirection.ltr),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
_selectionOverlay = SelectionOverlay(
|
_selectionOverlay = SelectionOverlay(
|
||||||
context: context,
|
context: context,
|
||||||
debugRequiredFor: widget,
|
debugRequiredFor: widget,
|
||||||
@ -621,7 +669,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
onEndHandleDragStart: _handleSelectionEndHandleDragStart,
|
onEndHandleDragStart: _handleSelectionEndHandleDragStart,
|
||||||
onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
|
onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
|
||||||
onEndHandleDragEnd: _onAnyDragEnd,
|
onEndHandleDragEnd: _onAnyDragEnd,
|
||||||
selectionEndpoints: points,
|
selectionEndpoints: selectionEndpoints,
|
||||||
selectionControls: widget.selectionControls,
|
selectionControls: widget.selectionControls,
|
||||||
selectionDelegate: this,
|
selectionDelegate: this,
|
||||||
clipboardStatus: null,
|
clipboardStatus: null,
|
||||||
@ -639,26 +687,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
assert(_hasSelectionOverlayGeometry);
|
assert(_hasSelectionOverlayGeometry);
|
||||||
final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
|
final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
|
||||||
final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
|
final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
|
||||||
late List<TextSelectionPoint> points;
|
|
||||||
final Offset startLocalPosition = start?.localPosition ?? end!.localPosition;
|
|
||||||
final Offset endLocalPosition = end?.localPosition ?? start!.localPosition;
|
|
||||||
if (startLocalPosition.dy > endLocalPosition.dy) {
|
|
||||||
points = <TextSelectionPoint>[
|
|
||||||
TextSelectionPoint(endLocalPosition, TextDirection.ltr),
|
|
||||||
TextSelectionPoint(startLocalPosition, TextDirection.ltr),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
points = <TextSelectionPoint>[
|
|
||||||
TextSelectionPoint(startLocalPosition, TextDirection.ltr),
|
|
||||||
TextSelectionPoint(endLocalPosition, TextDirection.ltr),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
_selectionOverlay!
|
_selectionOverlay!
|
||||||
..startHandleType = start?.handleType ?? TextSelectionHandleType.left
|
..startHandleType = start?.handleType ?? TextSelectionHandleType.left
|
||||||
..lineHeightAtStart = start?.lineHeight ?? end!.lineHeight
|
..lineHeightAtStart = start?.lineHeight ?? end!.lineHeight
|
||||||
..endHandleType = end?.handleType ?? TextSelectionHandleType.right
|
..endHandleType = end?.handleType ?? TextSelectionHandleType.right
|
||||||
..lineHeightAtEnd = end?.lineHeight ?? start!.lineHeight
|
..lineHeightAtEnd = end?.lineHeight ?? start!.lineHeight
|
||||||
..selectionEndpoints = points;
|
..selectionEndpoints = selectionEndpoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the selection handles.
|
/// Shows the selection handles.
|
||||||
@ -706,7 +740,19 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
}
|
}
|
||||||
|
|
||||||
_selectionOverlay!.toolbarLocation = location;
|
_selectionOverlay!.toolbarLocation = location;
|
||||||
_selectionOverlay!.showToolbar();
|
if (widget.selectionControls is! TextSelectionHandleControls) {
|
||||||
|
_selectionOverlay!.showToolbar();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectionOverlay!.hideToolbar();
|
||||||
|
|
||||||
|
_selectionOverlay!.showToolbar(
|
||||||
|
context: context,
|
||||||
|
contextMenuBuilder: (BuildContext context) {
|
||||||
|
return widget.contextMenuBuilder!(context, this);
|
||||||
|
},
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -830,11 +876,104 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
await Clipboard.setData(ClipboardData(text: data.plainText));
|
await Clipboard.setData(ClipboardData(text: data.plainText));
|
||||||
}
|
}
|
||||||
|
|
||||||
// [TextSelectionDelegate] overrides.
|
/// {@macro flutter.widgets.EditableText.getAnchors}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s
|
||||||
|
/// for the default context menu buttons.
|
||||||
|
TextSelectionToolbarAnchors get contextMenuAnchors {
|
||||||
|
if (lastSecondaryTapDownPosition != null) {
|
||||||
|
return TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: lastSecondaryTapDownPosition!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
|
||||||
|
return TextSelectionToolbarAnchors.fromSelection(
|
||||||
|
renderBox: renderBox,
|
||||||
|
startGlyphHeight: startGlyphHeight,
|
||||||
|
endGlyphHeight: endGlyphHeight,
|
||||||
|
selectionEndpoints: selectionEndpoints,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
|
||||||
|
/// platform's default selection menu.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [SelectableRegion.getSelectableButtonItems], which performs a similar role,
|
||||||
|
/// but for any selectable text, not just specifically SelectableRegion.
|
||||||
|
/// * [EditableTextState.contextMenuButtonItems], which peforms a similar role
|
||||||
|
/// but for content that is not just selectable but also editable.
|
||||||
|
/// * [contextMenuAnchors], which provides the anchor points for the default
|
||||||
|
/// context menu.
|
||||||
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
|
||||||
|
/// take a list of [ContextMenuButtonItem]s with
|
||||||
|
/// [AdaptiveTextSelectionToolbar.buttonItems].
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
|
||||||
|
/// button Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||||
|
List<ContextMenuButtonItem> get contextMenuButtonItems {
|
||||||
|
return SelectableRegion.getSelectableButtonItems(
|
||||||
|
selectionGeometry: _selectionDelegate.value,
|
||||||
|
onCopy: () {
|
||||||
|
_copy();
|
||||||
|
hideToolbar();
|
||||||
|
},
|
||||||
|
onSelectAll: () {
|
||||||
|
selectAll();
|
||||||
|
hideToolbar();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The line height at the start of the current selection.
|
||||||
|
double get startGlyphHeight {
|
||||||
|
return _selectionDelegate.value.startSelectionPoint!.lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The line height at the end of the current selection.
|
||||||
|
double get endGlyphHeight {
|
||||||
|
return _selectionDelegate.value.endSelectionPoint!.lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the local coordinates of the endpoints of the current selection.
|
||||||
|
List<TextSelectionPoint> get selectionEndpoints {
|
||||||
|
final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
|
||||||
|
final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
|
||||||
|
late List<TextSelectionPoint> points;
|
||||||
|
final Offset startLocalPosition = start?.localPosition ?? end!.localPosition;
|
||||||
|
final Offset endLocalPosition = end?.localPosition ?? start!.localPosition;
|
||||||
|
if (startLocalPosition.dy > endLocalPosition.dy) {
|
||||||
|
points = <TextSelectionPoint>[
|
||||||
|
TextSelectionPoint(endLocalPosition, TextDirection.ltr),
|
||||||
|
TextSelectionPoint(startLocalPosition, TextDirection.ltr),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
points = <TextSelectionPoint>[
|
||||||
|
TextSelectionPoint(startLocalPosition, TextDirection.ltr),
|
||||||
|
TextSelectionPoint(endLocalPosition, TextDirection.ltr),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [TextSelectionDelegate] overrides.
|
||||||
|
// TODO(justinmc): After deprecations have been removed, remove
|
||||||
|
// TextSelectionDelegate from this class.
|
||||||
|
// https://github.com/flutter/flutter/issues/111213
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
bool get cutEnabled => false;
|
bool get cutEnabled => false;
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
bool get pasteEnabled => false;
|
bool get pasteEnabled => false;
|
||||||
|
|
||||||
@ -857,28 +996,50 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
_updateSelectedContentIfNeeded();
|
_updateSelectedContentIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
void copySelection(SelectionChangedCause cause) {
|
void copySelection(SelectionChangedCause cause) {
|
||||||
_copy();
|
_copy();
|
||||||
_clearSelection();
|
_clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(chunhtai): remove this workaround after decoupling text selection
|
@Deprecated(
|
||||||
// from text editing in TextSelectionDelegate.
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
TextEditingValue textEditingValue = const TextEditingValue(text: '_');
|
TextEditingValue textEditingValue = const TextEditingValue(text: '_');
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
void bringIntoView(TextPosition position) {/* SelectableRegion must be in view at this point. */}
|
void bringIntoView(TextPosition position) {/* SelectableRegion must be in view at this point. */}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
void cutSelection(SelectionChangedCause cause) {
|
void cutSelection(SelectionChangedCause cause) {
|
||||||
assert(false);
|
assert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {/* SelectableRegion maintains its own state */}
|
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {/* SelectableRegion maintains its own state */}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
Future<void> pasteText(SelectionChangedCause cause) async {
|
Future<void> pasteText(SelectionChangedCause cause) async {
|
||||||
assert(false);
|
assert(false);
|
||||||
@ -909,7 +1070,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
|
|||||||
_selectionDelegate.dispose();
|
_selectionDelegate.dispose();
|
||||||
// In case dispose was triggered before gesture end, remove the magnifier
|
// In case dispose was triggered before gesture end, remove the magnifier
|
||||||
// so it doesn't remain stuck in the overlay forever.
|
// so it doesn't remain stuck in the overlay forever.
|
||||||
_selectionOverlay?.hideMagnifier(shouldShowToolbar: false);
|
_selectionOverlay?.hideMagnifier();
|
||||||
_selectionOverlay?.dispose();
|
_selectionOverlay?.dispose();
|
||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -967,7 +1128,7 @@ abstract class _NonOverrideAction<T extends Intent> extends ContextAction<T> {
|
|||||||
class _SelectAllAction extends _NonOverrideAction<SelectAllTextIntent> {
|
class _SelectAllAction extends _NonOverrideAction<SelectAllTextIntent> {
|
||||||
_SelectAllAction(this.state);
|
_SelectAllAction(this.state);
|
||||||
|
|
||||||
final _SelectableRegionState state;
|
final SelectableRegionState state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) {
|
void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) {
|
||||||
@ -978,7 +1139,7 @@ class _SelectAllAction extends _NonOverrideAction<SelectAllTextIntent> {
|
|||||||
class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> {
|
class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> {
|
||||||
_CopySelectionAction(this.state);
|
_CopySelectionAction(this.state);
|
||||||
|
|
||||||
final _SelectableRegionState state;
|
final SelectableRegionState state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) {
|
void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) {
|
||||||
@ -1795,3 +1956,15 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|||||||
return finalResult!;
|
return finalResult!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Signature for a widget builder that builds a context menu for the given
|
||||||
|
/// [SelectableRegionState].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [EditableTextContextMenuBuilder], which performs the same role for
|
||||||
|
/// [EditableText].
|
||||||
|
typedef SelectableRegionContextMenuBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
SelectableRegionState selectableRegionState,
|
||||||
|
);
|
||||||
|
@ -16,6 +16,7 @@ import 'basic.dart';
|
|||||||
import 'binding.dart';
|
import 'binding.dart';
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
import 'container.dart';
|
import 'container.dart';
|
||||||
|
import 'context_menu_controller.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'editable_text.dart';
|
import 'editable_text.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
@ -113,6 +114,10 @@ abstract class TextSelectionControls {
|
|||||||
/// The [selectionMidpoint] parameter is a general calculation midpoint
|
/// The [selectionMidpoint] parameter is a general calculation midpoint
|
||||||
/// parameter of the toolbar. More detailed position information
|
/// parameter of the toolbar. More detailed position information
|
||||||
/// is computable from the [endpoints] parameter.
|
/// is computable from the [endpoints] parameter.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
Widget buildToolbar(
|
Widget buildToolbar(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Rect globalEditableRegion,
|
Rect globalEditableRegion,
|
||||||
@ -137,6 +142,10 @@ abstract class TextSelectionControls {
|
|||||||
///
|
///
|
||||||
/// Subclasses can use this to decide if they should expose the cut
|
/// Subclasses can use this to decide if they should expose the cut
|
||||||
/// functionality to the user.
|
/// functionality to the user.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
bool canCut(TextSelectionDelegate delegate) {
|
bool canCut(TextSelectionDelegate delegate) {
|
||||||
return delegate.cutEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
return delegate.cutEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
||||||
}
|
}
|
||||||
@ -148,6 +157,10 @@ abstract class TextSelectionControls {
|
|||||||
///
|
///
|
||||||
/// Subclasses can use this to decide if they should expose the copy
|
/// Subclasses can use this to decide if they should expose the copy
|
||||||
/// functionality to the user.
|
/// functionality to the user.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
bool canCopy(TextSelectionDelegate delegate) {
|
bool canCopy(TextSelectionDelegate delegate) {
|
||||||
return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
||||||
}
|
}
|
||||||
@ -161,6 +174,10 @@ abstract class TextSelectionControls {
|
|||||||
/// This does not consider the contents of the clipboard. Subclasses may want
|
/// This does not consider the contents of the clipboard. Subclasses may want
|
||||||
/// to, for example, disallow pasting when the clipboard contains an empty
|
/// to, for example, disallow pasting when the clipboard contains an empty
|
||||||
/// string.
|
/// string.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
bool canPaste(TextSelectionDelegate delegate) {
|
bool canPaste(TextSelectionDelegate delegate) {
|
||||||
return delegate.pasteEnabled;
|
return delegate.pasteEnabled;
|
||||||
}
|
}
|
||||||
@ -171,6 +188,10 @@ abstract class TextSelectionControls {
|
|||||||
///
|
///
|
||||||
/// Subclasses can use this to decide if they should expose the select all
|
/// Subclasses can use this to decide if they should expose the select all
|
||||||
/// functionality to the user.
|
/// functionality to the user.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
bool canSelectAll(TextSelectionDelegate delegate) {
|
bool canSelectAll(TextSelectionDelegate delegate) {
|
||||||
return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
|
return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
|
||||||
}
|
}
|
||||||
@ -181,6 +202,10 @@ abstract class TextSelectionControls {
|
|||||||
/// the user.
|
/// the user.
|
||||||
// TODO(chunhtai): remove optional parameter once migration is done.
|
// TODO(chunhtai): remove optional parameter once migration is done.
|
||||||
// https://github.com/flutter/flutter/issues/99360
|
// https://github.com/flutter/flutter/issues/99360
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
void handleCut(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {
|
void handleCut(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {
|
||||||
delegate.cutSelection(SelectionChangedCause.toolbar);
|
delegate.cutSelection(SelectionChangedCause.toolbar);
|
||||||
}
|
}
|
||||||
@ -191,6 +216,10 @@ abstract class TextSelectionControls {
|
|||||||
/// the user.
|
/// the user.
|
||||||
// TODO(chunhtai): remove optional parameter once migration is done.
|
// TODO(chunhtai): remove optional parameter once migration is done.
|
||||||
// https://github.com/flutter/flutter/issues/99360
|
// https://github.com/flutter/flutter/issues/99360
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
void handleCopy(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {
|
void handleCopy(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {
|
||||||
delegate.copySelection(SelectionChangedCause.toolbar);
|
delegate.copySelection(SelectionChangedCause.toolbar);
|
||||||
}
|
}
|
||||||
@ -204,6 +233,10 @@ abstract class TextSelectionControls {
|
|||||||
/// asynchronous. Race conditions may exist with this API as currently
|
/// asynchronous. Race conditions may exist with this API as currently
|
||||||
/// implemented.
|
/// implemented.
|
||||||
// TODO(ianh): https://github.com/flutter/flutter/issues/11427
|
// TODO(ianh): https://github.com/flutter/flutter/issues/11427
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
Future<void> handlePaste(TextSelectionDelegate delegate) async {
|
Future<void> handlePaste(TextSelectionDelegate delegate) async {
|
||||||
delegate.pasteText(SelectionChangedCause.toolbar);
|
delegate.pasteText(SelectionChangedCause.toolbar);
|
||||||
}
|
}
|
||||||
@ -215,6 +248,10 @@ abstract class TextSelectionControls {
|
|||||||
///
|
///
|
||||||
/// This is called by subclasses when their select-all affordance is activated
|
/// This is called by subclasses when their select-all affordance is activated
|
||||||
/// by the user.
|
/// by the user.
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
void handleSelectAll(TextSelectionDelegate delegate) {
|
void handleSelectAll(TextSelectionDelegate delegate) {
|
||||||
delegate.selectAll(SelectionChangedCause.toolbar);
|
delegate.selectAll(SelectionChangedCause.toolbar);
|
||||||
}
|
}
|
||||||
@ -293,6 +330,7 @@ class TextSelectionOverlay {
|
|||||||
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
||||||
VoidCallback? onSelectionHandleTapped,
|
VoidCallback? onSelectionHandleTapped,
|
||||||
ClipboardStatusNotifier? clipboardStatus,
|
ClipboardStatusNotifier? clipboardStatus,
|
||||||
|
this.contextMenuBuilder,
|
||||||
required TextMagnifierConfiguration magnifierConfiguration,
|
required TextMagnifierConfiguration magnifierConfiguration,
|
||||||
}) : assert(value != null),
|
}) : assert(value != null),
|
||||||
assert(context != null),
|
assert(context != null),
|
||||||
@ -333,6 +371,14 @@ class TextSelectionOverlay {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.SelectionOverlay.context}
|
||||||
|
/// The context in which the selection UI should appear.
|
||||||
|
///
|
||||||
|
/// This context must have an [Overlay] as an ancestor because this object
|
||||||
|
/// will display the text selection handles in that [Overlay].
|
||||||
|
/// {@endtemplate}
|
||||||
|
final BuildContext context;
|
||||||
|
|
||||||
/// Controls the fade-in and fade-out animations for the toolbar and handles.
|
/// Controls the fade-in and fade-out animations for the toolbar and handles.
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
'Use `SelectionOverlay.fadeDuration` instead. '
|
'Use `SelectionOverlay.fadeDuration` instead. '
|
||||||
@ -353,6 +399,11 @@ class TextSelectionOverlay {
|
|||||||
|
|
||||||
late final SelectionOverlay _selectionOverlay;
|
late final SelectionOverlay _selectionOverlay;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||||
|
///
|
||||||
|
/// If not provided, no context menu will be built.
|
||||||
|
final WidgetBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
/// Retrieve current value.
|
/// Retrieve current value.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
TextEditingValue get value => _value;
|
TextEditingValue get value => _value;
|
||||||
@ -365,12 +416,6 @@ class TextSelectionOverlay {
|
|||||||
final ValueNotifier<bool> _effectiveEndHandleVisibility = ValueNotifier<bool>(false);
|
final ValueNotifier<bool> _effectiveEndHandleVisibility = ValueNotifier<bool>(false);
|
||||||
final ValueNotifier<bool> _effectiveToolbarVisibility = ValueNotifier<bool>(false);
|
final ValueNotifier<bool> _effectiveToolbarVisibility = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
/// The context in which the selection handles should appear.
|
|
||||||
///
|
|
||||||
/// This context must have an [Overlay] as an ancestor because this object
|
|
||||||
/// will display the text selection handles in that [Overlay].
|
|
||||||
final BuildContext context;
|
|
||||||
|
|
||||||
void _updateTextSelectionOverlayVisibilities() {
|
void _updateTextSelectionOverlayVisibilities() {
|
||||||
_effectiveStartHandleVisibility.value = _handlesVisible && renderObject.selectionStartInViewport.value;
|
_effectiveStartHandleVisibility.value = _handlesVisible && renderObject.selectionStartInViewport.value;
|
||||||
_effectiveEndHandleVisibility.value = _handlesVisible && renderObject.selectionEndInViewport.value;
|
_effectiveEndHandleVisibility.value = _handlesVisible && renderObject.selectionEndInViewport.value;
|
||||||
@ -406,7 +451,22 @@ class TextSelectionOverlay {
|
|||||||
/// {@macro flutter.widgets.SelectionOverlay.showToolbar}
|
/// {@macro flutter.widgets.SelectionOverlay.showToolbar}
|
||||||
void showToolbar() {
|
void showToolbar() {
|
||||||
_updateSelectionOverlay();
|
_updateSelectionOverlay();
|
||||||
_selectionOverlay.showToolbar();
|
|
||||||
|
if (selectionControls is! TextSelectionHandleControls) {
|
||||||
|
_selectionOverlay.showToolbar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextMenuBuilder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(context.mounted);
|
||||||
|
_selectionOverlay.showToolbar(
|
||||||
|
context: context,
|
||||||
|
contextMenuBuilder: contextMenuBuilder,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// {@macro flutter.widgets.SelectionOverlay.showMagnifier}
|
/// {@macro flutter.widgets.SelectionOverlay.showMagnifier}
|
||||||
@ -436,8 +496,8 @@ class TextSelectionOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// {@macro flutter.widgets.SelectionOverlay.hideMagnifier}
|
/// {@macro flutter.widgets.SelectionOverlay.hideMagnifier}
|
||||||
void hideMagnifier({required bool shouldShowToolbar}) {
|
void hideMagnifier() {
|
||||||
_selectionOverlay.hideMagnifier(shouldShowToolbar: shouldShowToolbar);
|
_selectionOverlay.hideMagnifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the overlay after the selection has changed.
|
/// Updates the overlay after the selection has changed.
|
||||||
@ -487,7 +547,11 @@ class TextSelectionOverlay {
|
|||||||
bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible;
|
bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible;
|
||||||
|
|
||||||
/// Whether the toolbar is currently visible.
|
/// Whether the toolbar is currently visible.
|
||||||
bool get toolbarIsVisible => _selectionOverlay._toolbar != null;
|
bool get toolbarIsVisible {
|
||||||
|
return selectionControls is TextSelectionHandleControls
|
||||||
|
? _selectionOverlay._contextMenuControllerIsShown
|
||||||
|
: _selectionOverlay._toolbar != null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the magnifier is currently visible.
|
/// Whether the magnifier is currently visible.
|
||||||
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
|
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
|
||||||
@ -506,6 +570,7 @@ class TextSelectionOverlay {
|
|||||||
_effectiveToolbarVisibility.dispose();
|
_effectiveToolbarVisibility.dispose();
|
||||||
_effectiveStartHandleVisibility.dispose();
|
_effectiveStartHandleVisibility.dispose();
|
||||||
_effectiveEndHandleVisibility.dispose();
|
_effectiveEndHandleVisibility.dispose();
|
||||||
|
hideToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getStartGlyphHeight() {
|
double _getStartGlyphHeight() {
|
||||||
@ -728,7 +793,25 @@ class TextSelectionOverlay {
|
|||||||
_handleSelectionHandleChanged(newSelection, isEnd: false);
|
_handleSelectionHandleChanged(newSelection, isEnd: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleAnyDragEnd(DragEndDetails details) => _selectionOverlay.hideMagnifier(shouldShowToolbar: !_selection.isCollapsed);
|
void _handleAnyDragEnd(DragEndDetails details) {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectionControls is! TextSelectionHandleControls) {
|
||||||
|
_selectionOverlay.hideMagnifier();
|
||||||
|
if (!_selection.isCollapsed) {
|
||||||
|
_selectionOverlay.showToolbar();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_selectionOverlay.hideMagnifier();
|
||||||
|
if (!_selection.isCollapsed) {
|
||||||
|
_selectionOverlay.showToolbar(
|
||||||
|
context: context,
|
||||||
|
contextMenuBuilder: contextMenuBuilder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the offset that locates a drag on a handle to the correct line of text.
|
// Returns the offset that locates a drag on a handle to the correct line of text.
|
||||||
Offset _getOffsetToTextPositionPoint(TextSelectionHandleType type) {
|
Offset _getOffsetToTextPositionPoint(TextSelectionHandleType type) {
|
||||||
@ -810,6 +893,10 @@ class SelectionOverlay {
|
|||||||
this.toolbarVisible,
|
this.toolbarVisible,
|
||||||
required List<TextSelectionPoint> selectionEndpoints,
|
required List<TextSelectionPoint> selectionEndpoints,
|
||||||
required this.selectionControls,
|
required this.selectionControls,
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` in `showToolbar` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
required this.selectionDelegate,
|
required this.selectionDelegate,
|
||||||
required this.clipboardStatus,
|
required this.clipboardStatus,
|
||||||
required this.startHandleLayerLink,
|
required this.startHandleLayerLink,
|
||||||
@ -817,6 +904,10 @@ class SelectionOverlay {
|
|||||||
required this.toolbarLayerLink,
|
required this.toolbarLayerLink,
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.onSelectionHandleTapped,
|
this.onSelectionHandleTapped,
|
||||||
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` in `showToolbar` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
Offset? toolbarLocation,
|
Offset? toolbarLocation,
|
||||||
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
|
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
|
||||||
}) : _startHandleType = startHandleType,
|
}) : _startHandleType = startHandleType,
|
||||||
@ -827,13 +918,9 @@ class SelectionOverlay {
|
|||||||
_toolbarLocation = toolbarLocation,
|
_toolbarLocation = toolbarLocation,
|
||||||
assert(debugCheckHasOverlay(context));
|
assert(debugCheckHasOverlay(context));
|
||||||
|
|
||||||
/// The context in which the selection handles should appear.
|
/// {@macro flutter.widgets.SelectionOverlay.context}
|
||||||
///
|
|
||||||
/// This context must have an [Overlay] as an ancestor because this object
|
|
||||||
/// will display the text selection handles in that [Overlay].
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
|
|
||||||
|
|
||||||
final ValueNotifier<MagnifierInfo> _magnifierInfo =
|
final ValueNotifier<MagnifierInfo> _magnifierInfo =
|
||||||
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
|
ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
|
||||||
|
|
||||||
@ -863,7 +950,7 @@ class SelectionOverlay {
|
|||||||
/// [MagnifierController.shown].
|
/// [MagnifierController.shown].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
void showMagnifier(MagnifierInfo initalMagnifierInfo) {
|
void showMagnifier(MagnifierInfo initalMagnifierInfo) {
|
||||||
if (_toolbar != null) {
|
if (_toolbar != null || _contextMenuControllerIsShown) {
|
||||||
hideToolbar();
|
hideToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -892,12 +979,11 @@ class SelectionOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// {@template flutter.widgets.SelectionOverlay.hideMagnifier}
|
/// {@template flutter.widgets.SelectionOverlay.hideMagnifier}
|
||||||
/// Hide the current magnifier, optionally immediately showing
|
/// Hide the current magnifier.
|
||||||
/// the toolbar.
|
|
||||||
///
|
///
|
||||||
/// This does nothing if there is no magnifier.
|
/// This does nothing if there is no magnifier.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
void hideMagnifier({required bool shouldShowToolbar}) {
|
void hideMagnifier() {
|
||||||
// This cannot be a check on `MagnifierController.shown`, since
|
// This cannot be a check on `MagnifierController.shown`, since
|
||||||
// it's possible that the magnifier is still in the overlay, but
|
// it's possible that the magnifier is still in the overlay, but
|
||||||
// not shown in cases where the magnifier hides itself.
|
// not shown in cases where the magnifier hides itself.
|
||||||
@ -906,10 +992,6 @@ class SelectionOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_magnifierController.hide();
|
_magnifierController.hide();
|
||||||
|
|
||||||
if (shouldShowToolbar) {
|
|
||||||
showToolbar();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of start selection handle.
|
/// The type of start selection handle.
|
||||||
@ -1046,7 +1128,11 @@ class SelectionOverlay {
|
|||||||
/// The delegate for manipulating the current selection in the owning
|
/// The delegate for manipulating the current selection in the owning
|
||||||
/// text field.
|
/// text field.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final TextSelectionDelegate selectionDelegate;
|
@Deprecated(
|
||||||
|
'Use `contextMenuBuilder` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
final TextSelectionDelegate? selectionDelegate;
|
||||||
|
|
||||||
/// Determines the way that drag start behavior is handled.
|
/// Determines the way that drag start behavior is handled.
|
||||||
///
|
///
|
||||||
@ -1097,6 +1183,10 @@ class SelectionOverlay {
|
|||||||
///
|
///
|
||||||
/// This is useful for displaying toolbars at the mouse right-click locations
|
/// This is useful for displaying toolbars at the mouse right-click locations
|
||||||
/// in desktop devices.
|
/// in desktop devices.
|
||||||
|
@Deprecated(
|
||||||
|
'Use the `contextMenuBuilder` parameter in `showToolbar` instead. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
Offset? get toolbarLocation => _toolbarLocation;
|
Offset? get toolbarLocation => _toolbarLocation;
|
||||||
Offset? _toolbarLocation;
|
Offset? _toolbarLocation;
|
||||||
set toolbarLocation(Offset? value) {
|
set toolbarLocation(Offset? value) {
|
||||||
@ -1117,6 +1207,11 @@ class SelectionOverlay {
|
|||||||
/// A copy/paste toolbar.
|
/// A copy/paste toolbar.
|
||||||
OverlayEntry? _toolbar;
|
OverlayEntry? _toolbar;
|
||||||
|
|
||||||
|
// Manages the context menu. Not necessarily visible when non-null.
|
||||||
|
final ContextMenuController _contextMenuController = ContextMenuController();
|
||||||
|
|
||||||
|
bool get _contextMenuControllerIsShown => _contextMenuController.isShown;
|
||||||
|
|
||||||
/// {@template flutter.widgets.SelectionOverlay.showHandles}
|
/// {@template flutter.widgets.SelectionOverlay.showHandles}
|
||||||
/// Builds the handles by inserting them into the [context]'s overlay.
|
/// Builds the handles by inserting them into the [context]'s overlay.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
@ -1147,12 +1242,34 @@ class SelectionOverlay {
|
|||||||
/// {@template flutter.widgets.SelectionOverlay.showToolbar}
|
/// {@template flutter.widgets.SelectionOverlay.showToolbar}
|
||||||
/// Shows the toolbar by inserting it into the [context]'s overlay.
|
/// Shows the toolbar by inserting it into the [context]'s overlay.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
void showToolbar() {
|
void showToolbar({
|
||||||
if (_toolbar != null) {
|
BuildContext? context,
|
||||||
|
WidgetBuilder? contextMenuBuilder,
|
||||||
|
}) {
|
||||||
|
if (contextMenuBuilder == null) {
|
||||||
|
if (_toolbar != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_toolbar = OverlayEntry(builder: _buildToolbar);
|
||||||
|
Overlay.of(this.context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insert(_toolbar!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_toolbar = OverlayEntry(builder: _buildToolbar);
|
|
||||||
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insert(_toolbar!);
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
|
||||||
|
_contextMenuController.show(
|
||||||
|
context: context,
|
||||||
|
contextMenuBuilder: (BuildContext context) {
|
||||||
|
return _SelectionToolbarWrapper(
|
||||||
|
layerLink: toolbarLayerLink,
|
||||||
|
offset: -renderBox.localToGlobal(Offset.zero),
|
||||||
|
child: contextMenuBuilder(context),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _buildScheduled = false;
|
bool _buildScheduled = false;
|
||||||
@ -1174,6 +1291,9 @@ class SelectionOverlay {
|
|||||||
_handles![1].markNeedsBuild();
|
_handles![1].markNeedsBuild();
|
||||||
}
|
}
|
||||||
_toolbar?.markNeedsBuild();
|
_toolbar?.markNeedsBuild();
|
||||||
|
if (_contextMenuController.isShown) {
|
||||||
|
_contextMenuController.markNeedsBuild();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (_handles != null) {
|
if (_handles != null) {
|
||||||
@ -1181,6 +1301,9 @@ class SelectionOverlay {
|
|||||||
_handles![1].markNeedsBuild();
|
_handles![1].markNeedsBuild();
|
||||||
}
|
}
|
||||||
_toolbar?.markNeedsBuild();
|
_toolbar?.markNeedsBuild();
|
||||||
|
if (_contextMenuController.isShown) {
|
||||||
|
_contextMenuController.markNeedsBuild();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1194,7 +1317,7 @@ class SelectionOverlay {
|
|||||||
_handles![1].remove();
|
_handles![1].remove();
|
||||||
_handles = null;
|
_handles = null;
|
||||||
}
|
}
|
||||||
if (_toolbar != null) {
|
if (_toolbar != null || _contextMenuControllerIsShown) {
|
||||||
hideToolbar();
|
hideToolbar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1205,6 +1328,7 @@ class SelectionOverlay {
|
|||||||
/// To hide the whole overlay, see [hide].
|
/// To hide the whole overlay, see [hide].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
void hideToolbar() {
|
void hideToolbar() {
|
||||||
|
_contextMenuController.remove();
|
||||||
if (_toolbar == null) {
|
if (_toolbar == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1272,10 +1396,12 @@ class SelectionOverlay {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the toolbar via TextSelectionControls.
|
||||||
Widget _buildToolbar(BuildContext context) {
|
Widget _buildToolbar(BuildContext context) {
|
||||||
if (selectionControls == null) {
|
if (selectionControls == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
assert(selectionDelegate != null, 'If not using contextMenuBuilder, must pass selectionDelegate.');
|
||||||
|
|
||||||
final RenderBox renderBox = this.context.findRenderObject()! as RenderBox;
|
final RenderBox renderBox = this.context.findRenderObject()! as RenderBox;
|
||||||
|
|
||||||
@ -1299,21 +1425,23 @@ class SelectionOverlay {
|
|||||||
selectionEndpoints.first.point.dy - lineHeightAtStart,
|
selectionEndpoints.first.point.dy - lineHeightAtStart,
|
||||||
);
|
);
|
||||||
|
|
||||||
return TextFieldTapRegion(
|
return _SelectionToolbarWrapper(
|
||||||
child: Directionality(
|
visibility: toolbarVisible,
|
||||||
textDirection: Directionality.of(this.context),
|
layerLink: toolbarLayerLink,
|
||||||
child: _SelectionToolbarOverlay(
|
offset: -editingRegion.topLeft,
|
||||||
preferredLineHeight: lineHeightAtStart,
|
child: Builder(
|
||||||
toolbarLocation: toolbarLocation,
|
builder: (BuildContext context) {
|
||||||
layerLink: toolbarLayerLink,
|
return selectionControls!.buildToolbar(
|
||||||
editingRegion: editingRegion,
|
context,
|
||||||
selectionControls: selectionControls,
|
editingRegion,
|
||||||
midpoint: midpoint,
|
lineHeightAtStart,
|
||||||
selectionEndpoints: selectionEndpoints,
|
midpoint,
|
||||||
visibility: toolbarVisible,
|
selectionEndpoints,
|
||||||
selectionDelegate: selectionDelegate,
|
selectionDelegate!,
|
||||||
clipboardStatus: clipboardStatus,
|
clipboardStatus,
|
||||||
),
|
toolbarLocation,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1337,38 +1465,32 @@ class SelectionOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This widget represents a selection toolbar.
|
// TODO(justinmc): Currently this fades in but not out on all platforms. It
|
||||||
class _SelectionToolbarOverlay extends StatefulWidget {
|
// should follow the correct fading behavior for the current platform, then be
|
||||||
/// Creates a toolbar overlay.
|
// made public and de-duplicated with widgets/selectable_region.dart.
|
||||||
const _SelectionToolbarOverlay({
|
// https://github.com/flutter/flutter/issues/107732
|
||||||
required this.preferredLineHeight,
|
// Wrap the given child in the widgets common to both contextMenuBuilder and
|
||||||
required this.toolbarLocation,
|
// TextSelectionControls.buildToolbar.
|
||||||
required this.layerLink,
|
class _SelectionToolbarWrapper extends StatefulWidget {
|
||||||
required this.editingRegion,
|
const _SelectionToolbarWrapper({
|
||||||
required this.selectionControls,
|
|
||||||
this.visibility,
|
this.visibility,
|
||||||
required this.midpoint,
|
required this.layerLink,
|
||||||
required this.selectionEndpoints,
|
required this.offset,
|
||||||
required this.selectionDelegate,
|
required this.child,
|
||||||
required this.clipboardStatus,
|
}) : assert(layerLink != null),
|
||||||
});
|
assert(offset != null),
|
||||||
|
assert(child != null);
|
||||||
|
|
||||||
final double preferredLineHeight;
|
final Widget child;
|
||||||
final Offset? toolbarLocation;
|
final Offset offset;
|
||||||
final LayerLink layerLink;
|
final LayerLink layerLink;
|
||||||
final Rect editingRegion;
|
|
||||||
final TextSelectionControls? selectionControls;
|
|
||||||
final ValueListenable<bool>? visibility;
|
final ValueListenable<bool>? visibility;
|
||||||
final Offset midpoint;
|
|
||||||
final List<TextSelectionPoint> selectionEndpoints;
|
|
||||||
final TextSelectionDelegate? selectionDelegate;
|
|
||||||
final ClipboardStatusNotifier? clipboardStatus;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SelectionToolbarOverlayState createState() => _SelectionToolbarOverlayState();
|
State<_SelectionToolbarWrapper> createState() => _SelectionToolbarWrapperState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SelectionToolbarOverlayState extends State<_SelectionToolbarOverlay> with SingleTickerProviderStateMixin {
|
class _SelectionToolbarWrapperState extends State<_SelectionToolbarWrapper> with SingleTickerProviderStateMixin {
|
||||||
late AnimationController _controller;
|
late AnimationController _controller;
|
||||||
Animation<double> get _opacity => _controller.view;
|
Animation<double> get _opacity => _controller.view;
|
||||||
|
|
||||||
@ -1383,7 +1505,7 @@ class _SelectionToolbarOverlayState extends State<_SelectionToolbarOverlay> with
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(_SelectionToolbarOverlay oldWidget) {
|
void didUpdateWidget(_SelectionToolbarWrapper oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.visibility == widget.visibility) {
|
if (oldWidget.visibility == widget.visibility) {
|
||||||
return;
|
return;
|
||||||
@ -1410,25 +1532,17 @@ class _SelectionToolbarOverlayState extends State<_SelectionToolbarOverlay> with
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FadeTransition(
|
return TextFieldTapRegion(
|
||||||
opacity: _opacity,
|
child: Directionality(
|
||||||
child: CompositedTransformFollower(
|
textDirection: Directionality.of(this.context),
|
||||||
link: widget.layerLink,
|
child: FadeTransition(
|
||||||
showWhenUnlinked: false,
|
opacity: _opacity,
|
||||||
offset: -widget.editingRegion.topLeft,
|
child: CompositedTransformFollower(
|
||||||
child: Builder(
|
link: widget.layerLink,
|
||||||
builder: (BuildContext context) {
|
showWhenUnlinked: false,
|
||||||
return widget.selectionControls!.buildToolbar(
|
offset: widget.offset,
|
||||||
context,
|
child: widget.child,
|
||||||
widget.editingRegion,
|
),
|
||||||
widget.preferredLineHeight,
|
|
||||||
widget.midpoint,
|
|
||||||
widget.selectionEndpoints,
|
|
||||||
widget.selectionDelegate!,
|
|
||||||
widget.clipboardStatus,
|
|
||||||
widget.toolbarLocation,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1464,7 +1578,6 @@ class _SelectionHandleOverlay extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<_SelectionHandleOverlay> createState() => _SelectionHandleOverlayState();
|
State<_SelectionHandleOverlay> createState() => _SelectionHandleOverlayState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with SingleTickerProviderStateMixin {
|
class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with SingleTickerProviderStateMixin {
|
||||||
@ -1813,6 +1926,9 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
// trigger the selection overlay.
|
// trigger the selection overlay.
|
||||||
// For backwards-compatibility, we treat a null kind the same as touch.
|
// For backwards-compatibility, we treat a null kind the same as touch.
|
||||||
final PointerDeviceKind? kind = details.kind;
|
final PointerDeviceKind? kind = details.kind;
|
||||||
|
// TODO(justinmc): Should a desktop platform show its selection toolbar when
|
||||||
|
// receiving a tap event? Say a Windows device with a touchscreen.
|
||||||
|
// https://github.com/flutter/flutter/issues/106586
|
||||||
_shouldShowSelectionToolbar = kind == null
|
_shouldShowSelectionToolbar = kind == null
|
||||||
|| kind == PointerDeviceKind.touch
|
|| kind == PointerDeviceKind.touch
|
||||||
|| kind == PointerDeviceKind.stylus;
|
|| kind == PointerDeviceKind.stylus;
|
||||||
@ -2122,7 +2238,7 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
switch (defaultTargetPlatform) {
|
switch (defaultTargetPlatform) {
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
editableText.hideMagnifier(shouldShowToolbar: false);
|
editableText.hideMagnifier();
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
@ -2781,3 +2897,47 @@ enum ClipboardStatus {
|
|||||||
/// The content on the clipboard is not pastable, such as when it is empty.
|
/// The content on the clipboard is not pastable, such as when it is empty.
|
||||||
notPasteable,
|
notPasteable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [TextSelectionControls] that specifically do not manage the toolbar in order
|
||||||
|
/// to leave that to [EditableText.contextMenuBuilder].
|
||||||
|
@Deprecated(
|
||||||
|
'Use `TextSelectionControls`. '
|
||||||
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||||
|
)
|
||||||
|
mixin TextSelectionHandleControls on TextSelectionControls {
|
||||||
|
@override
|
||||||
|
Widget buildToolbar(
|
||||||
|
BuildContext context,
|
||||||
|
Rect globalEditableRegion,
|
||||||
|
double textLineHeight,
|
||||||
|
Offset selectionMidpoint,
|
||||||
|
List<TextSelectionPoint> endpoints,
|
||||||
|
TextSelectionDelegate delegate,
|
||||||
|
ValueNotifier<ClipboardStatus>? clipboardStatus,
|
||||||
|
Offset? lastSecondaryTapDownPosition,
|
||||||
|
) => const SizedBox.shrink();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canCut(TextSelectionDelegate delegate) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canCopy(TextSelectionDelegate delegate) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canPaste(TextSelectionDelegate delegate) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canSelectAll(TextSelectionDelegate delegate) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleCut(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleCopy(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> handlePaste(TextSelectionDelegate delegate) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleSelectAll(TextSelectionDelegate delegate) {}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
// 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/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
/// The position information for a text selection toolbar.
|
||||||
|
///
|
||||||
|
/// Typically, a menu will attempt to position itself at [primaryAnchor], and
|
||||||
|
/// if that's not possible, then it will use [secondaryAnchor] instead, if it
|
||||||
|
/// exists.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AdaptiveTextSelectionToolbar.anchors], which is of this type.
|
||||||
|
@immutable
|
||||||
|
class TextSelectionToolbarAnchors {
|
||||||
|
/// Creates an instance of [TextSelectionToolbarAnchors] directly from the
|
||||||
|
/// anchor points.
|
||||||
|
const TextSelectionToolbarAnchors({
|
||||||
|
required this.primaryAnchor,
|
||||||
|
this.secondaryAnchor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates an instance of [TextSelectionToolbarAnchors] for some selection.
|
||||||
|
factory TextSelectionToolbarAnchors.fromSelection({
|
||||||
|
required RenderBox renderBox,
|
||||||
|
required double startGlyphHeight,
|
||||||
|
required double endGlyphHeight,
|
||||||
|
required List<TextSelectionPoint> selectionEndpoints,
|
||||||
|
}) {
|
||||||
|
final Rect editingRegion = Rect.fromPoints(
|
||||||
|
renderBox.localToGlobal(Offset.zero),
|
||||||
|
renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero)),
|
||||||
|
);
|
||||||
|
final bool isMultiline = selectionEndpoints.last.point.dy - selectionEndpoints.first.point.dy >
|
||||||
|
endGlyphHeight / 2;
|
||||||
|
|
||||||
|
final Rect selectionRect = Rect.fromLTRB(
|
||||||
|
isMultiline
|
||||||
|
? editingRegion.left
|
||||||
|
: editingRegion.left + selectionEndpoints.first.point.dx,
|
||||||
|
editingRegion.top + selectionEndpoints.first.point.dy - startGlyphHeight,
|
||||||
|
isMultiline
|
||||||
|
? editingRegion.right
|
||||||
|
: editingRegion.left + selectionEndpoints.last.point.dx,
|
||||||
|
editingRegion.top + selectionEndpoints.last.point.dy,
|
||||||
|
);
|
||||||
|
|
||||||
|
return TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: Offset(
|
||||||
|
selectionRect.left + selectionRect.width / 2,
|
||||||
|
clampDouble(selectionRect.top, editingRegion.top, editingRegion.bottom),
|
||||||
|
),
|
||||||
|
secondaryAnchor: Offset(
|
||||||
|
selectionRect.left + selectionRect.width / 2,
|
||||||
|
clampDouble(selectionRect.bottom, editingRegion.top, editingRegion.bottom),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The location that the toolbar should attempt to position itself at.
|
||||||
|
///
|
||||||
|
/// If the toolbar doesn't fit at this location, use [secondaryAnchor] if it
|
||||||
|
/// exists.
|
||||||
|
final Offset primaryAnchor;
|
||||||
|
|
||||||
|
/// The fallback position that should be used if [primaryAnchor] doesn't work.
|
||||||
|
final Offset? secondaryAnchor;
|
||||||
|
}
|
@ -35,6 +35,8 @@ export 'src/widgets/binding.dart';
|
|||||||
export 'src/widgets/bottom_navigation_bar_item.dart';
|
export 'src/widgets/bottom_navigation_bar_item.dart';
|
||||||
export 'src/widgets/color_filter.dart';
|
export 'src/widgets/color_filter.dart';
|
||||||
export 'src/widgets/container.dart';
|
export 'src/widgets/container.dart';
|
||||||
|
export 'src/widgets/context_menu_button_item.dart';
|
||||||
|
export 'src/widgets/context_menu_controller.dart';
|
||||||
export 'src/widgets/debug.dart';
|
export 'src/widgets/debug.dart';
|
||||||
export 'src/widgets/default_selection_style.dart';
|
export 'src/widgets/default_selection_style.dart';
|
||||||
export 'src/widgets/default_text_editing_shortcuts.dart';
|
export 'src/widgets/default_text_editing_shortcuts.dart';
|
||||||
@ -137,6 +139,7 @@ export 'src/widgets/tap_region.dart';
|
|||||||
export 'src/widgets/text.dart';
|
export 'src/widgets/text.dart';
|
||||||
export 'src/widgets/text_editing_intents.dart';
|
export 'src/widgets/text_editing_intents.dart';
|
||||||
export 'src/widgets/text_selection.dart';
|
export 'src/widgets/text_selection.dart';
|
||||||
|
export 'src/widgets/text_selection_toolbar_anchors.dart';
|
||||||
export 'src/widgets/text_selection_toolbar_layout_delegate.dart';
|
export 'src/widgets/text_selection_toolbar_layout_delegate.dart';
|
||||||
export 'src/widgets/texture.dart';
|
export 'src/widgets/texture.dart';
|
||||||
export 'src/widgets/ticker_provider.dart';
|
export 'src/widgets/ticker_provider.dart';
|
||||||
|
@ -0,0 +1,245 @@
|
|||||||
|
// 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/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../widgets/clipboard_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.platform,
|
||||||
|
mockClipboard.handleMethodCall,
|
||||||
|
);
|
||||||
|
// Fill the clipboard so that the Paste option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.platform,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Builds the right toolbar on each platform, including web, and shows buttonItems', (WidgetTester tester) async {
|
||||||
|
const String buttonText = 'Click me';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoAdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: const TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: Offset.zero,
|
||||||
|
),
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: buttonText,
|
||||||
|
onPressed: () {
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(buttonText), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbar), findsNothing);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbar), findsOneWidget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
skip: isBrowser, // [intended] see https://github.com/flutter/flutter/issues/108382
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Can build children directly as well', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoAdaptiveTextSelectionToolbar(
|
||||||
|
anchors: const TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: Offset.zero,
|
||||||
|
),
|
||||||
|
children: <Widget>[
|
||||||
|
Container(key: key),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: isBrowser, // [intended] see https://github.com/flutter/flutter/issues/108382
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Can build from EditableTextState', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(CupertinoApp(
|
||||||
|
home: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: EditableText(
|
||||||
|
controller: TextEditingController(),
|
||||||
|
backgroundCursorColor: const Color(0xff00ffff),
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
style: const TextStyle(),
|
||||||
|
cursorColor: const Color(0xff00ffff),
|
||||||
|
selectionControls: cupertinoTextSelectionHandleControls,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) {
|
||||||
|
return CupertinoAdaptiveTextSelectionToolbar.editableText(
|
||||||
|
key: key,
|
||||||
|
editableTextState: editableTextState,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsNothing);
|
||||||
|
|
||||||
|
// Long-press to bring up the context menu.
|
||||||
|
final Finder textFinder = find.byType(EditableText);
|
||||||
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
expect(find.text('Select all'), findsNothing);
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Can build for editable text from raw parameters', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoAdaptiveTextSelectionToolbar.editable(
|
||||||
|
key: key,
|
||||||
|
anchors: const TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: Offset.zero,
|
||||||
|
),
|
||||||
|
clipboardStatus: ClipboardStatus.pasteable,
|
||||||
|
onCopy: () {},
|
||||||
|
onCut: () {},
|
||||||
|
onPaste: () {},
|
||||||
|
onSelectAll: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsOneWidget);
|
||||||
|
expect(find.text('Cut'), findsOneWidget);
|
||||||
|
expect(find.text('Select All'), findsOneWidget);
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(4));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(4));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Builds the correct button per-platform', (WidgetTester tester) async {
|
||||||
|
const String buttonText = 'Click me';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons(
|
||||||
|
context,
|
||||||
|
<ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: buttonText,
|
||||||
|
onPressed: () {
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(buttonText), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
// 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/cupertino.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('can press', (WidgetTester tester) async {
|
||||||
|
bool pressed = false;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoDesktopTextSelectionToolbarButton(
|
||||||
|
child: const Text('Tap me'),
|
||||||
|
onPressed: () {
|
||||||
|
pressed = true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pressed, false);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(CupertinoDesktopTextSelectionToolbarButton));
|
||||||
|
expect(pressed, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('pressedOpacity defaults to 0.1', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoDesktopTextSelectionToolbarButton(
|
||||||
|
child: const Text('Tap me'),
|
||||||
|
onPressed: () { },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Original at full opacity.
|
||||||
|
FadeTransition opacity = tester.widget(find.descendant(
|
||||||
|
of: find.byType(CupertinoDesktopTextSelectionToolbarButton),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
));
|
||||||
|
expect(opacity.opacity.value, 1.0);
|
||||||
|
|
||||||
|
// Make a "down" gesture on the button.
|
||||||
|
final Offset center = tester.getCenter(find.byType(CupertinoDesktopTextSelectionToolbarButton));
|
||||||
|
final TestGesture gesture = await tester.startGesture(center);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Opacity reduces during the down gesture.
|
||||||
|
opacity = tester.widget(find.descendant(
|
||||||
|
of: find.byType(CupertinoDesktopTextSelectionToolbarButton),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
));
|
||||||
|
expect(opacity.opacity.value, 0.7);
|
||||||
|
|
||||||
|
// Release the down gesture.
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Opacity is back to normal.
|
||||||
|
opacity = tester.widget(find.descendant(
|
||||||
|
of: find.byType(CupertinoDesktopTextSelectionToolbarButton),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
));
|
||||||
|
expect(opacity.opacity.value, 1.0);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
// 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/cupertino.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('positions itself at the anchor', (WidgetTester tester) async {
|
||||||
|
// An arbitrary point on the screen to position at.
|
||||||
|
const Offset anchor = Offset(30.0, 40.0);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoDesktopTextSelectionToolbar(
|
||||||
|
anchor: anchor,
|
||||||
|
children: <Widget>[
|
||||||
|
CupertinoDesktopTextSelectionToolbarButton(
|
||||||
|
child: const Text('Tap me'),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.getTopLeft(find.byType(CupertinoDesktopTextSelectionToolbarButton)),
|
||||||
|
// Greater than due to padding internal to the toolbar.
|
||||||
|
greaterThan(anchor),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -1578,30 +1578,30 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Long press to put the cursor after the "w".
|
// Long press to put the cursor after the "w".
|
||||||
const int index = 3;
|
const int index = 3;
|
||||||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection.collapsed(offset: index),
|
const TextSelection.collapsed(offset: index),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Double tap on the same location to select the word around the cursor.
|
// Double tap on the same location to select the word around the cursor.
|
||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Selected text shows 'Copy'.
|
// Selected text shows 'Copy'.
|
||||||
expect(find.text('Paste'), findsNothing);
|
expect(find.text('Paste'), findsNothing);
|
||||||
expect(find.text('Copy'), findsOneWidget);
|
expect(find.text('Copy'), findsOneWidget);
|
||||||
expect(find.text('Cut'), findsNothing);
|
expect(find.text('Cut'), findsNothing);
|
||||||
expect(find.text('Select All'), findsNothing);
|
expect(find.text('Select All'), findsNothing);
|
||||||
},
|
},
|
||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
|
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
|
||||||
@ -1633,6 +1633,15 @@ void main() {
|
|||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tap elsewhere to hide the context menu so that subsequent taps don't
|
||||||
|
// collide with it.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
controller.selection,
|
||||||
|
const TextSelection.collapsed(offset: 35, affinity: TextAffinity.upstream),
|
||||||
|
);
|
||||||
|
|
||||||
// Double tap on the same location to select the word around the cursor.
|
// Double tap on the same location to select the word around the cursor.
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
@ -1966,7 +1975,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'double tap selects word and first tap of double tap moves cursor for non-Apple platforms',
|
'double tap selects word for non-Apple platforms',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
@ -1990,6 +1999,15 @@ void main() {
|
|||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tap elsewhere to hide the context menu so that subsequent taps don't
|
||||||
|
// collide with it.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, controller.text.length));
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
controller.selection,
|
||||||
|
const TextSelection.collapsed(offset: 35, affinity: TextAffinity.upstream),
|
||||||
|
);
|
||||||
|
|
||||||
// Double tap in the middle of 'Peel' to select the word.
|
// Double tap in the middle of 'Peel' to select the word.
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
@ -2001,7 +2019,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Selected text shows 3 toolbar buttons.
|
// Selected text shows 3 toolbar buttons.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
||||||
|
|
||||||
// Tap somewhere else to move the cursor.
|
// Tap somewhere else to move the cursor.
|
||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
@ -2012,7 +2030,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'double tap selects word and first tap of double tap moves cursor for Apple platforms',
|
'double tap selects word for Apple platforms',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
@ -2036,8 +2054,10 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: index),
|
const TextSelection.collapsed(offset: index),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Double tap on the same location to select the word around the cursor.
|
// Double tap to select the word around the cursor. Move slightly left of
|
||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
// the previous tap in order to avoid hitting the text selection toolbar
|
||||||
|
// on Mac.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index) - const Offset(1.0, 0.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@ -2046,8 +2066,22 @@ void main() {
|
|||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Selected text shows 3 toolbar buttons.
|
if (isContextMenuProvidedByPlatform) {
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
} else {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(CupertinoButton), findsNWidgets(4));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -2184,8 +2218,24 @@ void main() {
|
|||||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Selected text shows 3 toolbar buttons.
|
final Matcher matchToolbarButtons;
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
if (isContextMenuProvidedByPlatform) {
|
||||||
|
matchToolbarButtons = findsNothing;
|
||||||
|
} else {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
matchToolbarButtons = findsNWidgets(3);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
matchToolbarButtons = findsNWidgets(4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(find.byType(CupertinoButton), matchToolbarButtons);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -2195,7 +2245,7 @@ void main() {
|
|||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||||
);
|
);
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
expect(find.byType(CupertinoButton), matchToolbarButtons);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2348,8 +2398,10 @@ void main() {
|
|||||||
expect(controller.value.selection.baseOffset, 5);
|
expect(controller.value.selection.baseOffset, 5);
|
||||||
expect(controller.value.selection.extentOffset, 6);
|
expect(controller.value.selection.extentOffset, 6);
|
||||||
|
|
||||||
// Put the cursor at the end of the field.
|
// Tap at the end of the text to move the selection to the end. On some
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
// platforms, the context menu "Cut" button blocks this tap, so move it out
|
||||||
|
// of the way by an Offset.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, 10) + const Offset(200.0, 0.0));
|
||||||
expect(controller.value.selection, isNotNull);
|
expect(controller.value.selection, isNotNull);
|
||||||
expect(controller.value.selection.baseOffset, 10);
|
expect(controller.value.selection.baseOffset, 10);
|
||||||
expect(controller.value.selection.extentOffset, 10);
|
expect(controller.value.selection.extentOffset, 10);
|
||||||
@ -2646,8 +2698,8 @@ void main() {
|
|||||||
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
|
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Non-Collapsed toolbar shows 3 buttons.
|
// Non-Collapsed toolbar shows 4 buttons.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
||||||
},
|
},
|
||||||
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -2680,7 +2732,14 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Collapsed toolbar shows 2 buttons.
|
// Collapsed toolbar shows 2 buttons.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
expect(
|
||||||
|
find.byType(CupertinoButton),
|
||||||
|
isContextMenuProvidedByPlatform
|
||||||
|
? findsNothing
|
||||||
|
: defaultTargetPlatform == TargetPlatform.iOS
|
||||||
|
? findsNWidgets(2)
|
||||||
|
: findsNWidgets(1),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -2706,7 +2765,21 @@ void main() {
|
|||||||
await tester.longPressAt(ePos);
|
await tester.longPressAt(ePos);
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
await tester.tapAt(ePos);
|
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||||||
|
if (kIsWeb) {
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
} else {
|
||||||
|
expect(find.byType(CupertinoButton), findsNWidgets(isTargetPlatformMobile ? 2 : 1));
|
||||||
|
}
|
||||||
|
expect(controller.selection.isCollapsed, isTrue);
|
||||||
|
expect(controller.selection.baseOffset, 6);
|
||||||
|
|
||||||
|
// Tap in a slightly different position to avoid hitting the context menu
|
||||||
|
// on desktop.
|
||||||
|
final Offset secondTapPos = isTargetPlatformMobile
|
||||||
|
? ePos
|
||||||
|
: ePos + const Offset(-1.0, 0.0);
|
||||||
|
await tester.tapAt(secondTapPos);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// The cursor does not move and the toolbar is toggled.
|
// The cursor does not move and the toolbar is toggled.
|
||||||
@ -2776,7 +2849,7 @@ void main() {
|
|||||||
const TextSelection(baseOffset: 0, extentOffset: 23, affinity: TextAffinity.upstream),
|
const TextSelection(baseOffset: 0, extentOffset: 23, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
// The toolbar now shows up.
|
// The toolbar now shows up.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
|
||||||
},
|
},
|
||||||
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -2840,7 +2913,14 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
|
const TextSelection.collapsed(offset: 9, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
// The toolbar now shows up.
|
// The toolbar now shows up.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
expect(
|
||||||
|
find.byType(CupertinoButton),
|
||||||
|
isContextMenuProvidedByPlatform
|
||||||
|
? findsNothing
|
||||||
|
: defaultTargetPlatform == TargetPlatform.iOS
|
||||||
|
? findsNWidgets(2)
|
||||||
|
: findsNWidgets(1),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -3001,7 +3081,16 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
// The toolbar now shows up.
|
// The toolbar now shows up.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
final int toolbarButtons;
|
||||||
|
if (isContextMenuProvidedByPlatform) {
|
||||||
|
toolbarButtons = 0;
|
||||||
|
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||||
|
toolbarButtons = 2;
|
||||||
|
} else {
|
||||||
|
// MacOS has no 'Select all' button.
|
||||||
|
toolbarButtons = 1;
|
||||||
|
}
|
||||||
|
expect(find.byType(CupertinoButton), findsNWidgets(toolbarButtons));
|
||||||
|
|
||||||
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||||||
@ -3059,7 +3148,16 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Long press toolbar.
|
// Long press toolbar.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
final int toolbarButtons;
|
||||||
|
if (isContextMenuProvidedByPlatform) {
|
||||||
|
toolbarButtons = 0;
|
||||||
|
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||||
|
toolbarButtons = 2;
|
||||||
|
} else {
|
||||||
|
// MacOS has no 'Select all' button.
|
||||||
|
toolbarButtons = 1;
|
||||||
|
}
|
||||||
|
expect(find.byType(CupertinoButton), findsNWidgets(toolbarButtons));
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
@ -3081,17 +3179,30 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
|
// Use a position higher than wPos to avoid tapping the context menu on
|
||||||
|
// desktop.
|
||||||
|
final Offset pPos = textOffsetToPosition(tester, 9) + const Offset(0.0, -20.0); // Index of 'P|eel'
|
||||||
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
|
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
|
||||||
|
|
||||||
await tester.longPressAt(wPos);
|
await tester.longPressAt(wPos);
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
expect(controller.selection.isCollapsed, isTrue);
|
||||||
|
expect(controller.selection.baseOffset, 3);
|
||||||
|
if (isContextMenuProvidedByPlatform) {
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
} else {
|
||||||
|
expect(find.byType(CupertinoButton), isTargetPlatformMobile ? findsNWidgets(2) : findsNWidgets(1));
|
||||||
|
}
|
||||||
|
|
||||||
await tester.tapAt(pPos);
|
await tester.tapAt(pPos);
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
// First tap moved the cursor.
|
// First tap moved the cursor.
|
||||||
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
expect(controller.selection.isCollapsed, isTrue);
|
expect(controller.selection.isCollapsed, isTrue);
|
||||||
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
|
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
|
||||||
|
|
||||||
await tester.tapAt(pPos);
|
await tester.tapAt(pPos);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@ -3208,7 +3319,24 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
// Shows toolbar.
|
// Shows toolbar.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
|
final Matcher matchToolbarButtons;
|
||||||
|
if (isContextMenuProvidedByPlatform) {
|
||||||
|
matchToolbarButtons = findsNothing;
|
||||||
|
} else {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
matchToolbarButtons = findsNWidgets(3);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
matchToolbarButtons = findsNWidgets(4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(find.byType(CupertinoButton), matchToolbarButtons);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('force press on unsupported devices falls back to tap', (WidgetTester tester) async {
|
testWidgets('force press on unsupported devices falls back to tap', (WidgetTester tester) async {
|
||||||
@ -3295,6 +3423,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(endpoints.length, 2);
|
expect(endpoints.length, 2);
|
||||||
|
|
||||||
|
// On Mac, the toolbar blocks the drag on the right handle, so hide it.
|
||||||
|
final EditableTextState editableTextState = tester.state(find.byType(EditableText));
|
||||||
|
editableTextState.hideToolbar(false);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Drag the right handle until there's only 1 char selected.
|
// Drag the right handle until there's only 1 char selected.
|
||||||
// We use a small offset because the endpoint is on the very corner
|
// We use a small offset because the endpoint is on the very corner
|
||||||
// of the handle.
|
// of the handle.
|
||||||
@ -6269,6 +6402,9 @@ void main() {
|
|||||||
key: key1,
|
key: key1,
|
||||||
focusNode: focusNode1,
|
focusNode: focusNode1,
|
||||||
),
|
),
|
||||||
|
// This spacer prevents the context menu in one field from
|
||||||
|
// overlapping with the other field.
|
||||||
|
const SizedBox(height: 100.0),
|
||||||
CupertinoTextField(
|
CupertinoTextField(
|
||||||
key: key2,
|
key: key2,
|
||||||
focusNode: focusNode2,
|
focusNode: focusNode2,
|
||||||
@ -6406,6 +6542,75 @@ void main() {
|
|||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('context menu', () {
|
||||||
|
testWidgets('builds CupertinoAdaptiveTextSelectionToolbar by default', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(text: '');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
CupertinoTextField(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Long-press to bring up the context menu.
|
||||||
|
final Finder textFinder = find.byType(EditableText);
|
||||||
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
final TextEditingController controller = TextEditingController(text: '');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
CupertinoTextField(
|
||||||
|
controller: controller,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) {
|
||||||
|
return Placeholder(key: key);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsNothing);
|
||||||
|
|
||||||
|
// Long-press to bring up the context menu.
|
||||||
|
final Finder textFinder = find.byType(EditableText);
|
||||||
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('magnifier', () {
|
group('magnifier', () {
|
||||||
late ValueNotifier<MagnifierInfo> magnifierInfo;
|
late ValueNotifier<MagnifierInfo> magnifierInfo;
|
||||||
final Widget fakeMagnifier = Container(key: UniqueKey());
|
final Widget fakeMagnifier = Container(key: UniqueKey());
|
||||||
@ -6500,7 +6705,6 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const String testValue = 'abc def ghi';
|
const String testValue = 'abc def ghi';
|
||||||
await tester.enterText(find.byType(CupertinoTextField), testValue);
|
await tester.enterText(find.byType(CupertinoTextField), testValue);
|
||||||
|
|
||||||
@ -6662,6 +6866,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||||
|
});
|
||||||
|
|
||||||
group('TapRegion integration', () {
|
group('TapRegion integration', () {
|
||||||
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
|
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
|
||||||
@ -6776,34 +6981,33 @@ void main() {
|
|||||||
skip: kIsWeb, // [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
|
skip: kIsWeb, // [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets("Tapping on border doesn't lose focus",
|
testWidgets("Tapping on border doesn't lose focus",
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
CupertinoApp(
|
CupertinoApp(
|
||||||
home: Center(
|
home: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
child: CupertinoTextField(
|
child: CupertinoTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
await tester.pump();
|
);
|
||||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
await tester.pump();
|
||||||
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||||
|
|
||||||
final Rect borderBox = tester.getRect(find.byType(CupertinoTextField));
|
final Rect borderBox = tester.getRect(find.byType(CupertinoTextField));
|
||||||
// Tap just inside the border, but not inside the EditableText.
|
// Tap just inside the border, but not inside the EditableText.
|
||||||
await tester.tapAt(borderBox.topLeft + const Offset(1, 1));
|
await tester.tapAt(borderBox.topLeft + const Offset(1, 1));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||||
}, variant: TargetPlatformVariant.all());
|
}, variant: TargetPlatformVariant.all());
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Can drag handles to change selection correctly in multiline', (WidgetTester tester) async {
|
testWidgets('Can drag handles to change selection correctly in multiline', (WidgetTester tester) async {
|
||||||
|
@ -23,7 +23,7 @@ class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionContro
|
|||||||
Offset selectionMidpoint,
|
Offset selectionMidpoint,
|
||||||
List<TextSelectionPoint> endpoints,
|
List<TextSelectionPoint> endpoints,
|
||||||
TextSelectionDelegate delegate,
|
TextSelectionDelegate delegate,
|
||||||
ClipboardStatusNotifier? clipboardStatus,
|
ValueNotifier<ClipboardStatus>? clipboardStatus,
|
||||||
Offset? lastSecondaryTapDownPosition,
|
Offset? lastSecondaryTapDownPosition,
|
||||||
) {
|
) {
|
||||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||||
|
@ -0,0 +1,393 @@
|
|||||||
|
// 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/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../widgets/clipboard_utils.dart';
|
||||||
|
import '../widgets/editable_text_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.platform,
|
||||||
|
mockClipboard.handleMethodCall,
|
||||||
|
);
|
||||||
|
// Fill the clipboard so that the Paste option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Builds the right toolbar on each platform, including web, and shows buttonItems', (WidgetTester tester) async {
|
||||||
|
const String buttonText = 'Click me';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: const TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: Offset.zero,
|
||||||
|
),
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: buttonText,
|
||||||
|
onPressed: () {
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(buttonText), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
expect(find.byType(TextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbar), findsNothing);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(TextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbar), findsNothing);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
expect(find.byType(TextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbar), findsOneWidget);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(TextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbar), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbar), findsNothing);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
skip: isBrowser, // [intended] see https://github.com/flutter/flutter/issues/108382
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Can build children directly as well', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: AdaptiveTextSelectionToolbar(
|
||||||
|
anchors: const TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: Offset.zero,
|
||||||
|
),
|
||||||
|
children: <Widget>[
|
||||||
|
Container(key: key),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can build from EditableTextState', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: EditableText(
|
||||||
|
controller: TextEditingController(),
|
||||||
|
backgroundCursorColor: const Color(0xff00ffff),
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
style: const TextStyle(),
|
||||||
|
cursorColor: const Color(0xff00ffff),
|
||||||
|
selectionControls: materialTextSelectionHandleControls,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) {
|
||||||
|
return AdaptiveTextSelectionToolbar.editableText(
|
||||||
|
key: key,
|
||||||
|
editableTextState: editableTextState,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsNothing);
|
||||||
|
|
||||||
|
// Long-press to bring up the context menu.
|
||||||
|
final Finder textFinder = find.byType(EditableText);
|
||||||
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
expect(find.text('Select all'), findsNothing);
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Can build for editable text from raw parameters', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: AdaptiveTextSelectionToolbar.editable(
|
||||||
|
key: key,
|
||||||
|
anchors: const TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: Offset.zero,
|
||||||
|
),
|
||||||
|
clipboardStatus: ClipboardStatus.pasteable,
|
||||||
|
onCopy: () {},
|
||||||
|
onCut: () {},
|
||||||
|
onPaste: () {},
|
||||||
|
onSelectAll: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsOneWidget);
|
||||||
|
expect(find.text('Cut'), findsOneWidget);
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
expect(find.text('Select all'), findsOneWidget);
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsNWidgets(4));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.text('Select All'), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(4));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.text('Select all'), findsOneWidget);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsNWidgets(4));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
expect(find.text('Select All'), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNWidgets(4));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
group('buttonItems', () {
|
||||||
|
testWidgets('getEditableTextButtonItems builds the correct button items per-platform', (WidgetTester tester) async {
|
||||||
|
// Fill the clipboard so that the Paste option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
|
|
||||||
|
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: EditableText(
|
||||||
|
controller: controller,
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
style: const TextStyle(),
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
selectionControls: materialTextSelectionHandleControls,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) {
|
||||||
|
buttonTypes = editableTextState.contextMenuButtonItems
|
||||||
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
||||||
|
.toSet();
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
|
// With no text in the field.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.showToolbar(), true);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(buttonTypes, isNot(contains(ContextMenuButtonType.cut)));
|
||||||
|
expect(buttonTypes, isNot(contains(ContextMenuButtonType.copy)));
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.paste));
|
||||||
|
expect(buttonTypes, isNot(contains(ContextMenuButtonType.selectAll)));
|
||||||
|
|
||||||
|
// With text but no selection.
|
||||||
|
controller.text = 'lorem ipsum';
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(buttonTypes, isNot(contains(ContextMenuButtonType.cut)));
|
||||||
|
expect(buttonTypes, isNot(contains(ContextMenuButtonType.copy)));
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.paste));
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
expect(buttonTypes, isNot(contains(ContextMenuButtonType.selectAll)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With text and selection.
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
selection: const TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: 'lorem'.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.cut));
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.paste));
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||||
|
break;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
expect(buttonTypes, isNot(contains(ContextMenuButtonType.selectAll)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
skip: kIsWeb, // [intended]
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('getAdaptiveButtons builds the correct button widgets per-platform', (WidgetTester tester) async {
|
||||||
|
const String buttonText = 'Click me';
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: buttonText,
|
||||||
|
onPressed: () {
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return ListView(
|
||||||
|
children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
|
||||||
|
context,
|
||||||
|
buttonItems,
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(buttonText), findsOneWidget);
|
||||||
|
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.android:
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
expect(find.byType(TextSelectionToolbarTextButton), findsNothing);
|
||||||
|
expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
|
||||||
|
expect(find.byType(DesktopTextSelectionToolbarButton), findsOneWidget);
|
||||||
|
expect(find.byType(CupertinoDesktopTextSelectionToolbarButton), findsNothing);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// 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_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('can press', (WidgetTester tester) async {
|
||||||
|
bool pressed = false;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: DesktopTextSelectionToolbarButton(
|
||||||
|
child: const Text('Tap me'),
|
||||||
|
onPressed: () {
|
||||||
|
pressed = true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pressed, false);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(DesktopTextSelectionToolbarButton));
|
||||||
|
expect(pressed, true);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
// 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_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('positions itself at the anchor', (WidgetTester tester) async {
|
||||||
|
// An arbitrary point on the screen to position at.
|
||||||
|
const Offset anchor = Offset(30.0, 40.0);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: DesktopTextSelectionToolbar(
|
||||||
|
anchor: anchor,
|
||||||
|
children: <Widget>[
|
||||||
|
DesktopTextSelectionToolbarButton(
|
||||||
|
child: const Text('Tap me'),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.getTopLeft(find.byType(DesktopTextSelectionToolbarButton)),
|
||||||
|
anchor,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -27,21 +27,82 @@ void main() {
|
|||||||
switch (defaultTargetPlatform) {
|
switch (defaultTargetPlatform) {
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
expect(region.selectionControls, materialTextSelectionControls);
|
expect(region.selectionControls, materialTextSelectionHandleControls);
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
expect(region.selectionControls, cupertinoTextSelectionControls);
|
expect(region.selectionControls, cupertinoTextSelectionHandleControls);
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
case TargetPlatform.windows:
|
case TargetPlatform.windows:
|
||||||
expect(region.selectionControls, desktopTextSelectionControls);
|
expect(region.selectionControls, desktopTextSelectionHandleControls);
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.macOS:
|
case TargetPlatform.macOS:
|
||||||
expect(region.selectionControls, cupertinoDesktopTextSelectionControls);
|
expect(region.selectionControls, cupertinoDesktopTextSelectionHandleControls);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, variant: TargetPlatformVariant.all());
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
|
testWidgets('builds the default context menu by default', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: SelectionArea(
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
child: const Text('How are you?'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Show the toolbar by longpressing.
|
||||||
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||||
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 6)); // at the 'r'
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
// `are` is selected.
|
||||||
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended]
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('builds a custom context menu if provided', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: SelectionArea(
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
SelectableRegionState selectableRegionState,
|
||||||
|
) {
|
||||||
|
return Placeholder(key: key);
|
||||||
|
},
|
||||||
|
child: const Text('How are you?'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byKey(key), findsNothing);
|
||||||
|
|
||||||
|
// Show the toolbar by longpressing.
|
||||||
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||||
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 6)); // at the 'r'
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
// `are` is selected.
|
||||||
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended]
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('onSelectionChange is called when the selection changes', (WidgetTester tester) async {
|
testWidgets('onSelectionChange is called when the selection changes', (WidgetTester tester) async {
|
||||||
SelectedContent? content;
|
SelectedContent? content;
|
||||||
|
|
||||||
|
@ -24,15 +24,12 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../widgets/clipboard_utils.dart';
|
import '../widgets/clipboard_utils.dart';
|
||||||
import '../widgets/editable_text_utils.dart' show OverflowWidgetTextEditingController, findRenderEditable, globalize, textOffsetToPosition;
|
import '../widgets/editable_text_utils.dart';
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
import 'feedback_tester.dart';
|
import 'feedback_tester.dart';
|
||||||
|
|
||||||
typedef FormatEditUpdateCallback = void Function(TextEditingValue, TextEditingValue);
|
typedef FormatEditUpdateCallback = void Function(TextEditingValue, TextEditingValue);
|
||||||
|
|
||||||
// On web, the context menu (aka toolbar) is provided by the browser.
|
|
||||||
const bool isContextMenuProvidedByPlatform = isBrowser;
|
|
||||||
|
|
||||||
// On web, key events in text fields are handled by the browser.
|
// On web, key events in text fields are handled by the browser.
|
||||||
const bool areKeyEventsHandledByPlatform = isBrowser;
|
const bool areKeyEventsHandledByPlatform = isBrowser;
|
||||||
|
|
||||||
@ -1160,8 +1157,9 @@ void main() {
|
|||||||
expect(controller.selection.baseOffset, 0);
|
expect(controller.selection.baseOffset, 0);
|
||||||
expect(controller.selection.extentOffset, 7);
|
expect(controller.selection.extentOffset, 7);
|
||||||
|
|
||||||
// Use toolbar to select all text.
|
// Select all text. Use the toolbar if possible. iOS only shows the toolbar
|
||||||
if (isContextMenuProvidedByPlatform) {
|
// when the selection is collapsed.
|
||||||
|
if (isContextMenuProvidedByPlatform || defaultTargetPlatform == TargetPlatform.iOS) {
|
||||||
controller.selection = TextSelection(baseOffset: 0, extentOffset: controller.text.length);
|
controller.selection = TextSelection(baseOffset: 0, extentOffset: controller.text.length);
|
||||||
expect(controller.selection.extentOffset, controller.text.length);
|
expect(controller.selection.extentOffset, controller.text.length);
|
||||||
} else {
|
} else {
|
||||||
@ -2914,6 +2912,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// Toolbar should fade in. Starting at 0% opacity.
|
// Toolbar should fade in. Starting at 0% opacity.
|
||||||
|
expect(find.text('Select all'), findsOneWidget);
|
||||||
final Element target = tester.element(find.text('Select all'));
|
final Element target = tester.element(find.text('Select all'));
|
||||||
final FadeTransition opacity = target.findAncestorWidgetOfExactType<FadeTransition>()!;
|
final FadeTransition opacity = target.findAncestorWidgetOfExactType<FadeTransition>()!;
|
||||||
expect(opacity.opacity.value, equals(0.0));
|
expect(opacity.opacity.value, equals(0.0));
|
||||||
@ -8474,7 +8473,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Collapsed toolbar shows 2 buttons.
|
// Collapsed toolbar shows 2 buttons.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1;
|
||||||
|
expect(
|
||||||
|
find.byType(CupertinoButton),
|
||||||
|
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -8535,7 +8538,14 @@ void main() {
|
|||||||
|
|
||||||
await tester.longPressAt(ePos);
|
await tester.longPressAt(ePos);
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
await tester.tapAt(ePos);
|
|
||||||
|
// Tap slightly behind the previous tap to avoid tapping the context menu
|
||||||
|
// on desktop.
|
||||||
|
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
|
||||||
|
final Offset secondTapPos = isTargetPlatformMobile
|
||||||
|
? ePos
|
||||||
|
: ePos + const Offset(-1.0, 0.0);
|
||||||
|
await tester.tapAt(secondTapPos);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// The cursor does not move and the toolbar is toggled.
|
// The cursor does not move and the toolbar is toggled.
|
||||||
@ -8689,7 +8699,11 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 9),
|
const TextSelection.collapsed(offset: 9),
|
||||||
);
|
);
|
||||||
// The toolbar now shows up.
|
// The toolbar now shows up.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1;
|
||||||
|
expect(
|
||||||
|
find.byType(CupertinoButton),
|
||||||
|
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -8852,7 +8866,11 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
// The toolbar now shows up.
|
// The toolbar now shows up.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1;
|
||||||
|
expect(
|
||||||
|
find.byType(CupertinoButton),
|
||||||
|
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons),
|
||||||
|
);
|
||||||
|
|
||||||
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
lastCharEndpoint = renderEditable.getEndpointsForSelection(
|
||||||
const TextSelection.collapsed(offset: 66), // Last character's position.
|
const TextSelection.collapsed(offset: 66), // Last character's position.
|
||||||
@ -9038,7 +9056,6 @@ void main() {
|
|||||||
// Start long pressing on the first line.
|
// Start long pressing on the first line.
|
||||||
final TestGesture gesture =
|
final TestGesture gesture =
|
||||||
await tester.startGesture(textOffsetToPosition(tester, 19));
|
await tester.startGesture(textOffsetToPosition(tester, 19));
|
||||||
// TODO(justinmc): Make sure you've got all things torn down.
|
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
@ -9281,7 +9298,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Long press toolbar.
|
// Long press toolbar.
|
||||||
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(2));
|
final int buttons = defaultTargetPlatform == TargetPlatform.iOS ? 2 : 1;
|
||||||
|
expect(
|
||||||
|
find.byType(CupertinoButton),
|
||||||
|
isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(buttons),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
);
|
);
|
||||||
@ -9307,7 +9328,9 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
|
// The second tap is slightly higher to avoid tapping the context menu on
|
||||||
|
// desktop.
|
||||||
|
final Offset pPos = textOffsetToPosition(tester, 9) + const Offset(0.0, -20.0); // Index of 'P|eel'
|
||||||
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
|
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
|
||||||
|
|
||||||
await tester.longPressAt(wPos);
|
await tester.longPressAt(wPos);
|
||||||
@ -9662,8 +9685,10 @@ void main() {
|
|||||||
expect(controller.value.selection.baseOffset, 5);
|
expect(controller.value.selection.baseOffset, 5);
|
||||||
expect(controller.value.selection.extentOffset, 6);
|
expect(controller.value.selection.extentOffset, 6);
|
||||||
|
|
||||||
// Put the cursor at the end of the field.
|
// Tap at the end of the text to move the selection to the end. On some
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
// platforms, the context menu "Cut" button blocks this tap, so move it out
|
||||||
|
// of the way by an Offset.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, 10) + const Offset(200.0, 0.0));
|
||||||
expect(controller.value.selection, isNotNull);
|
expect(controller.value.selection, isNotNull);
|
||||||
expect(controller.value.selection.baseOffset, 10);
|
expect(controller.value.selection.baseOffset, 10);
|
||||||
expect(controller.value.selection.extentOffset, 10);
|
expect(controller.value.selection.extentOffset, 10);
|
||||||
@ -10679,10 +10704,12 @@ void main() {
|
|||||||
// Tap the handle to show the toolbar.
|
// Tap the handle to show the toolbar.
|
||||||
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
|
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
|
||||||
await tester.tapAt(handlePos, pointer: 7);
|
await tester.tapAt(handlePos, pointer: 7);
|
||||||
|
await tester.pump();
|
||||||
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
|
expect(editableText.selectionOverlay!.toolbarIsVisible, isContextMenuProvidedByPlatform ? isFalse : isTrue);
|
||||||
|
|
||||||
// Tap the handle again to hide the toolbar.
|
// Tap the handle again to hide the toolbar.
|
||||||
await tester.tapAt(handlePos, pointer: 7);
|
await tester.tapAt(handlePos, pointer: 7);
|
||||||
|
await tester.pump();
|
||||||
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -11405,7 +11432,8 @@ void main() {
|
|||||||
expect(find.text(selectAll), findsNothing);
|
expect(find.text(selectAll), findsNothing);
|
||||||
expect(find.text('Copy'), findsNothing);
|
expect(find.text('Copy'), findsNothing);
|
||||||
},
|
},
|
||||||
variant: TargetPlatformVariant.desktop(),
|
// All desktop platforms except MacOS, which has no select all button.
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.windows }),
|
||||||
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
|
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -12455,6 +12483,7 @@ void main() {
|
|||||||
key: key1,
|
key: key1,
|
||||||
focusNode: focusNode1,
|
focusNode: focusNode1,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 100.0),
|
||||||
TextField(
|
TextField(
|
||||||
key: key2,
|
key: key2,
|
||||||
focusNode: focusNode2,
|
focusNode: focusNode2,
|
||||||
@ -12594,6 +12623,77 @@ void main() {
|
|||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('context menu', () {
|
||||||
|
testWidgets('builds AdaptiveTextSelectionToolbar by default', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(text: '');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
// Long-press to bring up the context menu.
|
||||||
|
final Finder textFinder = find.byType(EditableText);
|
||||||
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
final TextEditingController controller = TextEditingController(text: '');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) {
|
||||||
|
return Placeholder(key: key);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsNothing);
|
||||||
|
|
||||||
|
// Long-press to bring up the context menu.
|
||||||
|
final Finder textFinder = find.byType(EditableText);
|
||||||
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('magnifier builder', () {
|
group('magnifier builder', () {
|
||||||
testWidgets('should build custom magnifier if given',
|
testWidgets('should build custom magnifier if given',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
@ -12816,6 +12916,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
expect(find.byKey(fakeMagnifier.key!), findsNothing);
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
|
||||||
|
});
|
||||||
|
|
||||||
group('TapRegion integration', () {
|
group('TapRegion integration', () {
|
||||||
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
|
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
|
||||||
@ -12966,6 +13067,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||||
|
|
||||||
@ -13033,10 +13135,9 @@ void main() {
|
|||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
expect(focusNode.hasPrimaryFocus, isFalse);
|
expect(focusNode.hasPrimaryFocus, isFalse);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, variant: TargetPlatformVariant.all());
|
}, variant: TargetPlatformVariant.all());
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,7 +611,6 @@ void main() {
|
|||||||
expect(find.text('Select all'), findsNothing);
|
expect(find.text('Select all'), findsNothing);
|
||||||
expect(find.byType(IconButton), findsNothing);
|
expect(find.byType(IconButton), findsNothing);
|
||||||
|
|
||||||
|
|
||||||
// The menu appears at the top of the visible selection.
|
// The menu appears at the top of the visible selection.
|
||||||
final Offset selectionOffset = tester
|
final Offset selectionOffset = tester
|
||||||
.getTopLeft(find.byType(TextSelectionToolbarTextButton).first);
|
.getTopLeft(find.byType(TextSelectionToolbarTextButton).first);
|
||||||
|
@ -8,11 +8,12 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
|
|
||||||
import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
|
import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
|
||||||
|
|
||||||
|
const double _kHandleSize = 22.0;
|
||||||
|
const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
|
||||||
|
const double _kToolbarContentDistance = 8.0;
|
||||||
|
|
||||||
// A custom text selection menu that just displays a single custom button.
|
// A custom text selection menu that just displays a single custom button.
|
||||||
class _CustomMaterialTextSelectionControls extends MaterialTextSelectionControls {
|
class _CustomMaterialTextSelectionControls extends MaterialTextSelectionControls {
|
||||||
static const double _kToolbarContentDistanceBelow = 20.0;
|
|
||||||
static const double _kToolbarContentDistance = 8.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildToolbar(
|
Widget buildToolbar(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -154,23 +155,23 @@ void main() {
|
|||||||
// When the toolbar doesn't fit above aboveAnchor, it positions itself below
|
// When the toolbar doesn't fit above aboveAnchor, it positions itself below
|
||||||
// belowAnchor.
|
// belowAnchor.
|
||||||
double toolbarY = tester.getTopLeft(findToolbar()).dy;
|
double toolbarY = tester.getTopLeft(findToolbar()).dy;
|
||||||
expect(toolbarY, equals(anchorBelowY));
|
expect(toolbarY, equals(anchorBelowY + _kToolbarContentDistanceBelow));
|
||||||
|
|
||||||
// Even when it barely doesn't fit.
|
// Even when it barely doesn't fit.
|
||||||
setState(() {
|
|
||||||
anchorAboveY = 50.0;
|
|
||||||
});
|
|
||||||
await tester.pump();
|
|
||||||
toolbarY = tester.getTopLeft(findToolbar()).dy;
|
|
||||||
expect(toolbarY, equals(anchorBelowY));
|
|
||||||
|
|
||||||
// When it does fit above aboveAnchor, it positions itself there.
|
|
||||||
setState(() {
|
setState(() {
|
||||||
anchorAboveY = 60.0;
|
anchorAboveY = 60.0;
|
||||||
});
|
});
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
toolbarY = tester.getTopLeft(findToolbar()).dy;
|
toolbarY = tester.getTopLeft(findToolbar()).dy;
|
||||||
expect(toolbarY, equals(anchorAboveY - height));
|
expect(toolbarY, equals(anchorBelowY + _kToolbarContentDistanceBelow));
|
||||||
|
|
||||||
|
// When it does fit above aboveAnchor, it positions itself there.
|
||||||
|
setState(() {
|
||||||
|
anchorAboveY = 70.0;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
toolbarY = tester.getTopLeft(findToolbar()).dy;
|
||||||
|
expect(toolbarY, equals(anchorAboveY - height - _kToolbarContentDistance));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('can create and use a custom toolbar', (WidgetTester tester) async {
|
testWidgets('can create and use a custom toolbar', (WidgetTester tester) async {
|
||||||
|
263
packages/flutter/test/widgets/context_menu_controller_test.dart
Normal file
263
packages/flutter/test/widgets/context_menu_controller_test.dart
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
// 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.
|
||||||
|
);
|
||||||
|
}
|
@ -1834,7 +1834,7 @@ void main() {
|
|||||||
backgroundCursorColor: Colors.grey,
|
backgroundCursorColor: Colors.grey,
|
||||||
controller: TextEditingController(text: 'blah blah'),
|
controller: TextEditingController(text: 'blah blah'),
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
toolbarOptions: const ToolbarOptions(),
|
toolbarOptions: ToolbarOptions.empty,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
cursorColor: cursorColor,
|
cursorColor: cursorColor,
|
||||||
selectionControls: cupertinoTextSelectionControls,
|
selectionControls: cupertinoTextSelectionControls,
|
||||||
@ -3293,7 +3293,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
controller.selection =
|
controller.selection =
|
||||||
TextSelection.collapsed(offset:controller.text.length);
|
TextSelection.collapsed(offset: controller.text.length);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// At end, can only go backwards.
|
// At end, can only go backwards.
|
||||||
@ -5306,7 +5306,7 @@ void main() {
|
|||||||
|
|
||||||
// Find the toolbar fade transition while the toolbar is still visible.
|
// Find the toolbar fade transition while the toolbar is still visible.
|
||||||
final List<FadeTransition> transitionsBefore = find.descendant(
|
final List<FadeTransition> transitionsBefore = find.descendant(
|
||||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionToolbarOverlay'),
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionToolbarWrapper'),
|
||||||
matching: find.byType(FadeTransition),
|
matching: find.byType(FadeTransition),
|
||||||
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
|
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
|
||||||
|
|
||||||
@ -5322,7 +5322,7 @@ void main() {
|
|||||||
|
|
||||||
// Find the toolbar fade transition after the toolbar has been hidden.
|
// Find the toolbar fade transition after the toolbar has been hidden.
|
||||||
final List<FadeTransition> transitionsAfter = find.descendant(
|
final List<FadeTransition> transitionsAfter = find.descendant(
|
||||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionToolbarOverlay'),
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionToolbarWrapper'),
|
||||||
matching: find.byType(FadeTransition),
|
matching: find.byType(FadeTransition),
|
||||||
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
|
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
|
||||||
|
|
||||||
@ -13575,6 +13575,56 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('contextMenuBuilder is used in place of the default text selection toolbar', (WidgetTester tester) async {
|
||||||
|
final GlobalKey key = GlobalKey();
|
||||||
|
final TextEditingController controller = TextEditingController(text: '');
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: EditableText(
|
||||||
|
maxLines: 10,
|
||||||
|
controller: controller,
|
||||||
|
showSelectionHandles: true,
|
||||||
|
autofocus: true,
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
style: Typography.material2018().black.subtitle1!,
|
||||||
|
cursorColor: Colors.blue,
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
selectionControls: materialTextSelectionHandleControls,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
EditableTextState editableTextState,
|
||||||
|
) {
|
||||||
|
return SizedBox(
|
||||||
|
key: key,
|
||||||
|
width: 10.0,
|
||||||
|
height: 10.0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsNothing);
|
||||||
|
|
||||||
|
// Long-press to bring up the context menu.
|
||||||
|
final Finder textFinder = find.byType(EditableText);
|
||||||
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(key), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||||
|
);
|
||||||
|
|
||||||
group('Spell check', () {
|
group('Spell check', () {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Spell check configured properly when spell check disabled by default',
|
'Spell check configured properly when spell check disabled by default',
|
||||||
|
@ -7,6 +7,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// On web, the context menu (aka toolbar) is provided by the browser.
|
||||||
|
const bool isContextMenuProvidedByPlatform = isBrowser;
|
||||||
|
|
||||||
// Returns the first RenderEditable.
|
// Returns the first RenderEditable.
|
||||||
RenderEditable findRenderEditable(WidgetTester tester) {
|
RenderEditable findRenderEditable(WidgetTester tester) {
|
||||||
final RenderObject root = tester.renderObject(find.byType(EditableText));
|
final RenderObject root = tester.renderObject(find.byType(EditableText));
|
||||||
|
@ -1209,6 +1209,44 @@ void main() {
|
|||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('builds the correct button items', (WidgetTester tester) async {
|
||||||
|
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: SelectableRegion(
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
selectionControls: materialTextSelectionHandleControls,
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
SelectableRegionState selectableRegionState,
|
||||||
|
) {
|
||||||
|
buttonTypes = selectableRegionState.contextMenuButtonItems
|
||||||
|
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
|
||||||
|
.toSet();
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
child: const Text('How are you?'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
|
||||||
|
|
||||||
|
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||||
|
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 6)); // at the 'r'
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
// `are` is selected.
|
||||||
|
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.copy));
|
||||||
|
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
skip: kIsWeb, // [intended]
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('onSelectionChange is called when the selection changes', (WidgetTester tester) async {
|
testWidgets('onSelectionChange is called when the selection changes', (WidgetTester tester) async {
|
||||||
SelectedContent? content;
|
SelectedContent? content;
|
||||||
|
|
||||||
|
@ -3353,6 +3353,12 @@ void main() {
|
|||||||
await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
|
await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
// Hide the toolbar so it doesn't interfere with taps on the text.
|
||||||
|
final EditableTextState editableTextState =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
editableTextState.hideToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
|
await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
@ -3829,6 +3835,12 @@ void main() {
|
|||||||
await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
|
await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
// Hide the toolbar so it doesn't interfere with taps on the text.
|
||||||
|
final EditableTextState editableTextState =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
editableTextState.hideToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
|
await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
@ -3901,6 +3913,12 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(find.byType(CupertinoButton), findsNWidgets(1));
|
expect(find.byType(CupertinoButton), findsNWidgets(1));
|
||||||
|
|
||||||
|
// Hide the toolbar so it doesn't interfere with taps on the text.
|
||||||
|
final EditableTextState editableTextState =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
editableTextState.hideToolbar();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
|
await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
// First tap moved the cursor.
|
// First tap moved the cursor.
|
||||||
@ -4411,7 +4429,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final EditableTextState state =
|
final EditableTextState state =
|
||||||
tester.state<EditableTextState>(find.byType(EditableText));
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
final RenderEditable renderEditable = state.renderEditable;
|
final RenderEditable renderEditable = state.renderEditable;
|
||||||
|
|
||||||
await tester.tapAt(const Offset(20, 10));
|
await tester.tapAt(const Offset(20, 10));
|
||||||
@ -4971,7 +4989,11 @@ void main() {
|
|||||||
expect(selection!.baseOffset, 5);
|
expect(selection!.baseOffset, 5);
|
||||||
expect(selection!.extentOffset, 6);
|
expect(selection!.extentOffset, 6);
|
||||||
|
|
||||||
// Put the cursor at the end of the field.
|
// Tap at the beginning of the text to hide the toolbar, then at the end to
|
||||||
|
// move the cursor to the end. On some platforms, the context menu would
|
||||||
|
// otherwise block a tap on the end of the field.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||||||
expect(selection, isNotNull);
|
expect(selection, isNotNull);
|
||||||
expect(selection!.baseOffset, 10);
|
expect(selection!.baseOffset, 10);
|
||||||
|
Loading…
Reference in New Issue
Block a user