mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 22:09:06 +00:00
[Simplistic_Editor] Use new context menu API (#1733)
This commit is contained in:
@@ -11,11 +11,32 @@ class BasicTextField extends StatefulWidget {
|
|||||||
required this.controller,
|
required this.controller,
|
||||||
required this.style,
|
required this.style,
|
||||||
required this.focusNode,
|
required this.focusNode,
|
||||||
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
final FocusNode focusNode;
|
final FocusNode focusNode;
|
||||||
|
final BasicTextFieldContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
|
static Widget _defaultContextMenuBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
ClipboardStatus clipboardStatus,
|
||||||
|
VoidCallback? onCopy,
|
||||||
|
VoidCallback? onCut,
|
||||||
|
VoidCallback? onPaste,
|
||||||
|
VoidCallback? onSelectAll,
|
||||||
|
TextSelectionToolbarAnchors anchors,
|
||||||
|
) {
|
||||||
|
return AdaptiveTextSelectionToolbar.editable(
|
||||||
|
clipboardStatus: clipboardStatus,
|
||||||
|
onCopy: onCopy,
|
||||||
|
onCut: onCut,
|
||||||
|
onPaste: onPaste,
|
||||||
|
onSelectAll: onSelectAll,
|
||||||
|
anchors: anchors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BasicTextField> createState() => _BasicTextFieldState();
|
State<BasicTextField> createState() => _BasicTextFieldState();
|
||||||
@@ -86,21 +107,31 @@ class _BasicTextFieldState extends State<BasicTextField> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
switch (Theme.of(this.context).platform) {
|
switch (Theme.of(this.context).platform) {
|
||||||
|
// ignore: todo
|
||||||
|
// TODO(Renzo-Olivares): Remove use of deprecated members once
|
||||||
|
// TextSelectionControls.buildToolbar has been deleted.
|
||||||
|
// See https://github.com/flutter/flutter/pull/124611 and
|
||||||
|
// https://github.com/flutter/flutter/pull/124262 for more details.
|
||||||
case TargetPlatform.iOS:
|
case TargetPlatform.iOS:
|
||||||
_textSelectionControls = cupertinoTextSelectionControls;
|
// ignore: deprecated_member_use
|
||||||
|
_textSelectionControls = cupertinoTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.macOS:
|
case TargetPlatform.macOS:
|
||||||
_textSelectionControls = cupertinoDesktopTextSelectionControls;
|
// ignore: deprecated_member_use
|
||||||
|
_textSelectionControls = cupertinoDesktopTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
_textSelectionControls = materialTextSelectionControls;
|
// ignore: deprecated_member_use
|
||||||
|
_textSelectionControls = materialTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.linux:
|
case TargetPlatform.linux:
|
||||||
_textSelectionControls = desktopTextSelectionControls;
|
// ignore: deprecated_member_use
|
||||||
|
_textSelectionControls = desktopTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
case TargetPlatform.windows:
|
case TargetPlatform.windows:
|
||||||
_textSelectionControls = desktopTextSelectionControls;
|
// ignore: deprecated_member_use
|
||||||
|
_textSelectionControls = desktopTextSelectionHandleControls;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +140,17 @@ class _BasicTextFieldState extends State<BasicTextField> {
|
|||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onPanStart: (dragStartDetails) => _onDragStart(dragStartDetails),
|
onPanStart: (dragStartDetails) => _onDragStart(dragStartDetails),
|
||||||
onPanUpdate: (dragUpdateDetails) => _onDragUpdate(dragUpdateDetails),
|
onPanUpdate: (dragUpdateDetails) => _onDragUpdate(dragUpdateDetails),
|
||||||
|
onSecondaryTapDown: (secondaryTapDownDetails) {
|
||||||
|
_renderEditable.selectWordsInRange(
|
||||||
|
from: secondaryTapDownDetails.globalPosition,
|
||||||
|
cause: SelectionChangedCause.tap);
|
||||||
|
_renderEditable.handleSecondaryTapDown(secondaryTapDownDetails);
|
||||||
|
_textInputClient!.hideToolbar();
|
||||||
|
_textInputClient!.showToolbar();
|
||||||
|
},
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_textInputClient!.requestKeyboard();
|
_textInputClient!.requestKeyboard();
|
||||||
|
_textInputClient!.hideToolbar();
|
||||||
},
|
},
|
||||||
onTapDown: (tapDownDetails) {
|
onTapDown: (tapDownDetails) {
|
||||||
_renderEditable.handleTapDown(tapDownDetails);
|
_renderEditable.handleTapDown(tapDownDetails);
|
||||||
@@ -160,6 +200,7 @@ class _BasicTextFieldState extends State<BasicTextField> {
|
|||||||
selectionControls: _textSelectionControls,
|
selectionControls: _textSelectionControls,
|
||||||
onSelectionChanged: _handleSelectionChanged,
|
onSelectionChanged: _handleSelectionChanged,
|
||||||
showSelectionHandles: _showSelectionHandles,
|
showSelectionHandles: _showSelectionHandles,
|
||||||
|
contextMenuBuilder: widget.contextMenuBuilder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,6 +15,18 @@ import 'replacements.dart';
|
|||||||
typedef SelectionChangedCallback = void Function(
|
typedef SelectionChangedCallback = void Function(
|
||||||
TextSelection selection, SelectionChangedCause? cause);
|
TextSelection selection, SelectionChangedCause? cause);
|
||||||
|
|
||||||
|
/// Signature for a widget builder that builds a context menu for the given
|
||||||
|
/// editable field.
|
||||||
|
typedef BasicTextFieldContextMenuBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
ClipboardStatus clipboardStatus,
|
||||||
|
VoidCallback? onCopy,
|
||||||
|
VoidCallback? onCut,
|
||||||
|
VoidCallback? onPaste,
|
||||||
|
VoidCallback? onSelectAll,
|
||||||
|
TextSelectionToolbarAnchors anchors,
|
||||||
|
);
|
||||||
|
|
||||||
/// A basic text input client. An implementation of [DeltaTextInputClient] meant to
|
/// A basic text input client. An implementation of [DeltaTextInputClient] meant to
|
||||||
/// send/receive information from the framework to the platform's text input plugin
|
/// send/receive information from the framework to the platform's text input plugin
|
||||||
/// and vice-versa.
|
/// and vice-versa.
|
||||||
@@ -25,6 +37,7 @@ class BasicTextInputClient extends StatefulWidget {
|
|||||||
required this.style,
|
required this.style,
|
||||||
required this.focusNode,
|
required this.focusNode,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
|
this.contextMenuBuilder,
|
||||||
required this.onSelectionChanged,
|
required this.onSelectionChanged,
|
||||||
required this.showSelectionHandles,
|
required this.showSelectionHandles,
|
||||||
});
|
});
|
||||||
@@ -35,6 +48,7 @@ class BasicTextInputClient extends StatefulWidget {
|
|||||||
final TextSelectionControls? selectionControls;
|
final TextSelectionControls? selectionControls;
|
||||||
final bool showSelectionHandles;
|
final bool showSelectionHandles;
|
||||||
final SelectionChangedCallback onSelectionChanged;
|
final SelectionChangedCallback onSelectionChanged;
|
||||||
|
final BasicTextFieldContextMenuBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BasicTextInputClient> createState() => BasicTextInputClientState();
|
State<BasicTextInputClient> createState() => BasicTextInputClientState();
|
||||||
@@ -50,6 +64,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||||
widget.focusNode.addListener(_handleFocusChanged);
|
widget.focusNode.addListener(_handleFocusChanged);
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
}
|
}
|
||||||
@@ -63,18 +78,11 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.controller.removeListener(_didChangeTextEditingValue);
|
widget.controller.removeListener(_didChangeTextEditingValue);
|
||||||
|
_clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
||||||
|
_clipboardStatus?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeInputControl(
|
|
||||||
TextInputControl? oldControl, TextInputControl? newControl) {
|
|
||||||
if (_hasFocus && _hasInputConnection) {
|
|
||||||
oldControl?.hide();
|
|
||||||
newControl?.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [DeltaTextInputClient] method implementations.
|
/// [DeltaTextInputClient] method implementations.
|
||||||
@override
|
@override
|
||||||
void connectionClosed() {
|
void connectionClosed() {
|
||||||
@@ -94,6 +102,15 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
@override
|
@override
|
||||||
TextEditingValue? get currentTextEditingValue => _value;
|
TextEditingValue? get currentTextEditingValue => _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeInputControl(
|
||||||
|
TextInputControl? oldControl, TextInputControl? newControl) {
|
||||||
|
if (_hasFocus && _hasInputConnection) {
|
||||||
|
oldControl?.hide();
|
||||||
|
newControl?.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void insertTextPlaceholder(Size size) {
|
void insertTextPlaceholder(Size size) {
|
||||||
// Will not implement. This method is used for Scribble support.
|
// Will not implement. This method is used for Scribble support.
|
||||||
@@ -296,22 +313,10 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _userUpdateTextEditingValueWithDelta(
|
void _onChangedClipboardStatus() {
|
||||||
TextEditingDelta textEditingDelta, SelectionChangedCause cause) {
|
setState(() {
|
||||||
TextEditingValue value = _value;
|
// Inform the widget that the value of clipboardStatus has changed.
|
||||||
|
});
|
||||||
value = textEditingDelta.apply(value);
|
|
||||||
|
|
||||||
if (widget.controller is ReplacementTextEditingController) {
|
|
||||||
(widget.controller as ReplacementTextEditingController)
|
|
||||||
.syncReplacementRanges(textEditingDelta);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value != _value) {
|
|
||||||
manager.updateTextEditingDeltaHistory([textEditingDelta]);
|
|
||||||
}
|
|
||||||
|
|
||||||
userUpdateTextEditingValue(value, cause);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keyboard text editing actions.
|
/// Keyboard text editing actions.
|
||||||
@@ -448,6 +453,24 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _userUpdateTextEditingValueWithDelta(
|
||||||
|
TextEditingDelta textEditingDelta, SelectionChangedCause cause) {
|
||||||
|
TextEditingValue value = _value;
|
||||||
|
|
||||||
|
value = textEditingDelta.apply(value);
|
||||||
|
|
||||||
|
if (widget.controller is ReplacementTextEditingController) {
|
||||||
|
(widget.controller as ReplacementTextEditingController)
|
||||||
|
.syncReplacementRanges(textEditingDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != _value) {
|
||||||
|
manager.updateTextEditingDeltaHistory([textEditingDelta]);
|
||||||
|
}
|
||||||
|
|
||||||
|
userUpdateTextEditingValue(value, cause);
|
||||||
|
}
|
||||||
|
|
||||||
/// For updates to text editing value.
|
/// For updates to text editing value.
|
||||||
void _didChangeTextEditingValue() {
|
void _didChangeTextEditingValue() {
|
||||||
_updateRemoteTextEditingValueIfNeeded();
|
_updateRemoteTextEditingValueIfNeeded();
|
||||||
@@ -455,28 +478,6 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleToolbar() {
|
|
||||||
assert(_selectionOverlay != null);
|
|
||||||
if (_selectionOverlay!.toolbarIsVisible) {
|
|
||||||
hideToolbar(false);
|
|
||||||
} else {
|
|
||||||
showToolbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the framework's text editing value changes we should update the text editing
|
|
||||||
// value contained within the selection overlay or we might observe unexpected behavior.
|
|
||||||
void _updateOrDisposeOfSelectionOverlayIfNeeded() {
|
|
||||||
if (_selectionOverlay != null) {
|
|
||||||
if (_hasFocus) {
|
|
||||||
_selectionOverlay!.update(_value);
|
|
||||||
} else {
|
|
||||||
_selectionOverlay!.dispose();
|
|
||||||
_selectionOverlay = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update the platform's text input plugin's text editing value when it has changed
|
// Only update the platform's text input plugin's text editing value when it has changed
|
||||||
// to avoid sending duplicate update messages to the engine.
|
// to avoid sending duplicate update messages to the engine.
|
||||||
void _updateRemoteTextEditingValueIfNeeded() {
|
void _updateRemoteTextEditingValueIfNeeded() {
|
||||||
@@ -488,6 +489,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For correctly positioning the candidate menu on macOS.
|
||||||
// Sends the current composing rect to the iOS text input plugin via the text
|
// Sends the current composing rect to the iOS text input plugin via the text
|
||||||
// input channel. We need to keep sending the information even if no text is
|
// input channel. We need to keep sending the information even if no text is
|
||||||
// currently marked, as the information usually lags behind. The text input
|
// currently marked, as the information usually lags behind. The text input
|
||||||
@@ -542,6 +544,20 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
// Not implemented.
|
// Not implemented.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get cutEnabled => !textEditingValue.selection.isCollapsed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get copyEnabled => !textEditingValue.selection.isCollapsed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get pasteEnabled =>
|
||||||
|
_clipboardStatus == null ||
|
||||||
|
_clipboardStatus!.value == ClipboardStatus.pasteable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get selectAllEnabled => textEditingValue.text.isNotEmpty;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void copySelection(SelectionChangedCause cause) {
|
void copySelection(SelectionChangedCause cause) {
|
||||||
final TextSelection copyRange = textEditingValue.selection;
|
final TextSelection copyRange = textEditingValue.selection;
|
||||||
@@ -599,17 +615,6 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
_clipboardStatus?.update();
|
_clipboardStatus?.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void hideToolbar([bool hideHandles = true]) {
|
|
||||||
if (hideHandles) {
|
|
||||||
// Hide the handles and the toolbar.
|
|
||||||
_selectionOverlay?.hide();
|
|
||||||
} else if (_selectionOverlay?.toolbarIsVisible ?? false) {
|
|
||||||
// Hide only the toolbar but not the handles.
|
|
||||||
_selectionOverlay?.hideToolbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> pasteText(SelectionChangedCause cause) async {
|
Future<void> pasteText(SelectionChangedCause cause) async {
|
||||||
final TextSelection pasteRange = textEditingValue.selection;
|
final TextSelection pasteRange = textEditingValue.selection;
|
||||||
@@ -649,6 +654,18 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
),
|
),
|
||||||
cause,
|
cause,
|
||||||
);
|
);
|
||||||
|
if (cause == SelectionChangedCause.toolbar) {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
break;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
hideToolbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -699,6 +716,17 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void hideToolbar([bool hideHandles = true]) {
|
||||||
|
if (hideHandles) {
|
||||||
|
// Hide the handles and the toolbar.
|
||||||
|
_selectionOverlay?.hide();
|
||||||
|
} else if (_selectionOverlay?.toolbarIsVisible ?? false) {
|
||||||
|
// Hide only the toolbar but not the handles.
|
||||||
|
_selectionOverlay?.hideToolbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// For TextSelection.
|
/// For TextSelection.
|
||||||
final LayerLink _startHandleLayerLink = LayerLink();
|
final LayerLink _startHandleLayerLink = LayerLink();
|
||||||
final LayerLink _endHandleLayerLink = LayerLink();
|
final LayerLink _endHandleLayerLink = LayerLink();
|
||||||
@@ -711,14 +739,14 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
void _handleSelectionChanged(
|
void _handleSelectionChanged(
|
||||||
TextSelection selection, SelectionChangedCause? cause) {
|
TextSelection selection, SelectionChangedCause? cause) {
|
||||||
// We return early if the selection is not valid. This can happen when the
|
// We return early if the selection is not valid. This can happen when the
|
||||||
// text of [EditableText] is updated at the same time as the selection is
|
// text of the editable is updated at the same time as the selection is
|
||||||
// changed by a gesture event.
|
// changed by a gesture event.
|
||||||
if (!widget.controller.isSelectionWithinTextBounds(selection)) return;
|
if (!widget.controller.isSelectionWithinTextBounds(selection)) return;
|
||||||
|
|
||||||
widget.controller.selection = selection;
|
widget.controller.selection = selection;
|
||||||
|
|
||||||
// This will show the keyboard for all selection changes on the
|
// This will show the keyboard for all selection changes on the
|
||||||
// EditableText except for those triggered by a keyboard input.
|
// editable except for those triggered by a keyboard input.
|
||||||
// Typically BasicTextInputClient shouldn't take user keyboard input if
|
// Typically BasicTextInputClient shouldn't take user keyboard input if
|
||||||
// it's not focused already.
|
// it's not focused already.
|
||||||
switch (cause) {
|
switch (cause) {
|
||||||
@@ -738,28 +766,12 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (widget.selectionControls == null) {
|
if (widget.selectionControls == null && widget.contextMenuBuilder == null) {
|
||||||
_selectionOverlay?.dispose();
|
_selectionOverlay?.dispose();
|
||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
} else {
|
} else {
|
||||||
if (_selectionOverlay == null) {
|
if (_selectionOverlay == null) {
|
||||||
_selectionOverlay = TextSelectionOverlay(
|
_selectionOverlay = _createSelectionOverlay();
|
||||||
clipboardStatus: _clipboardStatus,
|
|
||||||
context: context,
|
|
||||||
value: _value,
|
|
||||||
debugRequiredFor: widget,
|
|
||||||
toolbarLayerLink: _toolbarLayerLink,
|
|
||||||
startHandleLayerLink: _startHandleLayerLink,
|
|
||||||
endHandleLayerLink: _endHandleLayerLink,
|
|
||||||
renderObject: renderEditable,
|
|
||||||
selectionControls: widget.selectionControls,
|
|
||||||
selectionDelegate: this,
|
|
||||||
dragStartBehavior: DragStartBehavior.start,
|
|
||||||
onSelectionHandleTapped: () {
|
|
||||||
_toggleToolbar();
|
|
||||||
},
|
|
||||||
magnifierConfiguration: TextMagnifierConfiguration.disabled,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
_selectionOverlay!.update(_value);
|
_selectionOverlay!.update(_value);
|
||||||
}
|
}
|
||||||
@@ -780,6 +792,136 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextSelectionOverlay _createSelectionOverlay() {
|
||||||
|
final TextSelectionOverlay selectionOverlay = TextSelectionOverlay(
|
||||||
|
clipboardStatus: _clipboardStatus,
|
||||||
|
context: context,
|
||||||
|
value: _value,
|
||||||
|
debugRequiredFor: widget,
|
||||||
|
toolbarLayerLink: _toolbarLayerLink,
|
||||||
|
startHandleLayerLink: _startHandleLayerLink,
|
||||||
|
endHandleLayerLink: _endHandleLayerLink,
|
||||||
|
renderObject: renderEditable,
|
||||||
|
selectionControls: widget.selectionControls,
|
||||||
|
selectionDelegate: this,
|
||||||
|
dragStartBehavior: DragStartBehavior.start,
|
||||||
|
onSelectionHandleTapped: () {
|
||||||
|
_toggleToolbar();
|
||||||
|
},
|
||||||
|
contextMenuBuilder: widget.contextMenuBuilder == null || kIsWeb
|
||||||
|
? null
|
||||||
|
: (context) {
|
||||||
|
return widget.contextMenuBuilder!(
|
||||||
|
context,
|
||||||
|
_clipboardStatus!.value,
|
||||||
|
copyEnabled
|
||||||
|
? () => copySelection(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
cutEnabled
|
||||||
|
? () => cutSelection(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
pasteEnabled
|
||||||
|
? () => pasteText(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
selectAllEnabled
|
||||||
|
? () => selectAll(SelectionChangedCause.toolbar)
|
||||||
|
: null,
|
||||||
|
_contextMenuAnchors,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
magnifierConfiguration: TextMagnifierConfiguration.disabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
return selectionOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleToolbar() {
|
||||||
|
final TextSelectionOverlay selectionOverlay =
|
||||||
|
_selectionOverlay ??= _createSelectionOverlay();
|
||||||
|
|
||||||
|
if (selectionOverlay.toolbarIsVisible) {
|
||||||
|
hideToolbar(false);
|
||||||
|
} else {
|
||||||
|
showToolbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the framework's text editing value changes we should update the text editing
|
||||||
|
// value contained within the selection overlay or we might observe unexpected behavior.
|
||||||
|
void _updateOrDisposeOfSelectionOverlayIfNeeded() {
|
||||||
|
if (_selectionOverlay != null) {
|
||||||
|
if (_hasFocus) {
|
||||||
|
_selectionOverlay!.update(_value);
|
||||||
|
} else {
|
||||||
|
_selectionOverlay!.dispose();
|
||||||
|
_selectionOverlay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the line heights at the start and end of the selection for the given
|
||||||
|
/// editable.
|
||||||
|
_GlyphHeights _getGlyphHeights() {
|
||||||
|
final TextSelection selection = textEditingValue.selection;
|
||||||
|
|
||||||
|
// Only calculate handle rects if the text in the previous frame
|
||||||
|
// is the same as the text in the current frame. This is done because
|
||||||
|
// widget.renderObject contains the renderEditable from the previous frame.
|
||||||
|
// If the text changed between the current and previous frames then
|
||||||
|
// widget.renderObject.getRectForComposingRange might fail. In cases where
|
||||||
|
// the current frame is different from the previous we fall back to
|
||||||
|
// renderObject.preferredLineHeight.
|
||||||
|
final InlineSpan span = renderEditable.text!;
|
||||||
|
final String prevText = span.toPlainText();
|
||||||
|
final String currText = textEditingValue.text;
|
||||||
|
if (prevText != currText || !selection.isValid || selection.isCollapsed) {
|
||||||
|
return _GlyphHeights(
|
||||||
|
start: renderEditable.preferredLineHeight,
|
||||||
|
end: renderEditable.preferredLineHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String selectedGraphemes = selection.textInside(currText);
|
||||||
|
final int firstSelectedGraphemeExtent =
|
||||||
|
selectedGraphemes.characters.first.length;
|
||||||
|
final Rect? startCharacterRect =
|
||||||
|
renderEditable.getRectForComposingRange(TextRange(
|
||||||
|
start: selection.start,
|
||||||
|
end: selection.start + firstSelectedGraphemeExtent,
|
||||||
|
));
|
||||||
|
final int lastSelectedGraphemeExtent =
|
||||||
|
selectedGraphemes.characters.last.length;
|
||||||
|
final Rect? endCharacterRect =
|
||||||
|
renderEditable.getRectForComposingRange(TextRange(
|
||||||
|
start: selection.end - lastSelectedGraphemeExtent,
|
||||||
|
end: selection.end,
|
||||||
|
));
|
||||||
|
return _GlyphHeights(
|
||||||
|
start: startCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
||||||
|
end: endCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the anchor points for the default context menu.
|
||||||
|
TextSelectionToolbarAnchors get _contextMenuAnchors {
|
||||||
|
if (renderEditable.lastSecondaryTapDownPosition != null) {
|
||||||
|
return TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: renderEditable.lastSecondaryTapDownPosition!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _GlyphHeights glyphHeights = _getGlyphHeights();
|
||||||
|
final TextSelection selection = textEditingValue.selection;
|
||||||
|
final List<TextSelectionPoint> points =
|
||||||
|
renderEditable.getEndpointsForSelection(selection);
|
||||||
|
return TextSelectionToolbarAnchors.fromSelection(
|
||||||
|
renderBox: renderEditable,
|
||||||
|
startGlyphHeight: glyphHeights.start,
|
||||||
|
endGlyphHeight: glyphHeights.end,
|
||||||
|
selectionEndpoints: points,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Actions(
|
return Actions(
|
||||||
@@ -1018,3 +1160,18 @@ class _Editable extends MultiChildRenderObjectWidget {
|
|||||||
..setPromptRectRange(promptRectRange);
|
..setPromptRectRange(promptRectRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The start and end glyph heights of some range of text.
|
||||||
|
@immutable
|
||||||
|
class _GlyphHeights {
|
||||||
|
const _GlyphHeights({
|
||||||
|
required this.start,
|
||||||
|
required this.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The glyph height of the first line.
|
||||||
|
final double start;
|
||||||
|
|
||||||
|
/// The glyph height of the last line.
|
||||||
|
final double end;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user