Unverified Commit 0b451b6d authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

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.
parent ef1236e0
// 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,
);
}
}
// 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(),
);
},
),
],
),
),
),
);
}
}
// 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);
}
// 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,
),
),
),
],
);
}
}
// 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),
],
),
),
),
),
),
);
}
}
// 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);
});
}
// 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);
});
}
// 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);
});
}
// 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);
});
}
// 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 @@ ...@@ -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'; ...@@ -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; ...@@ -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'; ...@@ -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';
......
// 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 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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,
),
),
);
}
}
// 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,
),
),
),
);
}
}
// 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'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 @@ ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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();
} }
......
...@@ -5,6 +5,7 @@ ...@@ -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; ...@@ -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( ...@@ -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( ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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 ...@@ -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 { ...@@ -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 { ...@@ -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,
......
...@@ -3,20 +3,19 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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,
),
);
}
}
// 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,
),
),
),
);
}
}
// 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'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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> { ...@@ -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'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 @@ ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 { ...@@ -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'; ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 { ...@@ -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, ...@@ -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.
......
// 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';
}
// 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 @@ ...@@ -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.
/// ///
......
// 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'; ...@@ -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'; ...@@ -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';
......
// 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);
});
}
// 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),
);
});
}
...@@ -23,7 +23,7 @@ class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionContro ...@@ -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);
......
...@@ -611,7 +611,6 @@ void main() { ...@@ -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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment