[Simplistic_Editor] Use new context menu API (#1733)

pull/1737/head
Renzo Olivares 2 years ago committed by GitHub
parent b752cf10ff
commit e09fada0a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,11 +11,32 @@ class BasicTextField extends StatefulWidget {
required this.controller,
required this.style,
required this.focusNode,
this.contextMenuBuilder = _defaultContextMenuBuilder,
});
final TextEditingController controller;
final TextStyle style;
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
State<BasicTextField> createState() => _BasicTextFieldState();
@ -86,21 +107,31 @@ class _BasicTextFieldState extends State<BasicTextField> {
@override
Widget build(BuildContext context) {
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:
_textSelectionControls = cupertinoTextSelectionControls;
// ignore: deprecated_member_use
_textSelectionControls = cupertinoTextSelectionHandleControls;
break;
case TargetPlatform.macOS:
_textSelectionControls = cupertinoDesktopTextSelectionControls;
// ignore: deprecated_member_use
_textSelectionControls = cupertinoDesktopTextSelectionHandleControls;
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
_textSelectionControls = materialTextSelectionControls;
// ignore: deprecated_member_use
_textSelectionControls = materialTextSelectionHandleControls;
break;
case TargetPlatform.linux:
_textSelectionControls = desktopTextSelectionControls;
// ignore: deprecated_member_use
_textSelectionControls = desktopTextSelectionHandleControls;
break;
case TargetPlatform.windows:
_textSelectionControls = desktopTextSelectionControls;
// ignore: deprecated_member_use
_textSelectionControls = desktopTextSelectionHandleControls;
break;
}
@ -109,8 +140,17 @@ class _BasicTextFieldState extends State<BasicTextField> {
behavior: HitTestBehavior.translucent,
onPanStart: (dragStartDetails) => _onDragStart(dragStartDetails),
onPanUpdate: (dragUpdateDetails) => _onDragUpdate(dragUpdateDetails),
onSecondaryTapDown: (secondaryTapDownDetails) {
_renderEditable.selectWordsInRange(
from: secondaryTapDownDetails.globalPosition,
cause: SelectionChangedCause.tap);
_renderEditable.handleSecondaryTapDown(secondaryTapDownDetails);
_textInputClient!.hideToolbar();
_textInputClient!.showToolbar();
},
onTap: () {
_textInputClient!.requestKeyboard();
_textInputClient!.hideToolbar();
},
onTapDown: (tapDownDetails) {
_renderEditable.handleTapDown(tapDownDetails);
@ -160,6 +200,7 @@ class _BasicTextFieldState extends State<BasicTextField> {
selectionControls: _textSelectionControls,
onSelectionChanged: _handleSelectionChanged,
showSelectionHandles: _showSelectionHandles,
contextMenuBuilder: widget.contextMenuBuilder,
),
),
),

@ -15,6 +15,18 @@ import 'replacements.dart';
typedef SelectionChangedCallback = void Function(
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
/// send/receive information from the framework to the platform's text input plugin
/// and vice-versa.
@ -25,6 +37,7 @@ class BasicTextInputClient extends StatefulWidget {
required this.style,
required this.focusNode,
this.selectionControls,
this.contextMenuBuilder,
required this.onSelectionChanged,
required this.showSelectionHandles,
});
@ -35,6 +48,7 @@ class BasicTextInputClient extends StatefulWidget {
final TextSelectionControls? selectionControls;
final bool showSelectionHandles;
final SelectionChangedCallback onSelectionChanged;
final BasicTextFieldContextMenuBuilder? contextMenuBuilder;
@override
State<BasicTextInputClient> createState() => BasicTextInputClientState();
@ -50,6 +64,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
@override
void initState() {
super.initState();
_clipboardStatus?.addListener(_onChangedClipboardStatus);
widget.focusNode.addListener(_handleFocusChanged);
widget.controller.addListener(_didChangeTextEditingValue);
}
@ -63,18 +78,11 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
@override
void dispose() {
widget.controller.removeListener(_didChangeTextEditingValue);
_clipboardStatus?.removeListener(_onChangedClipboardStatus);
_clipboardStatus?.dispose();
super.dispose();
}
@override
void didChangeInputControl(
TextInputControl? oldControl, TextInputControl? newControl) {
if (_hasFocus && _hasInputConnection) {
oldControl?.hide();
newControl?.show();
}
}
/// [DeltaTextInputClient] method implementations.
@override
void connectionClosed() {
@ -94,6 +102,15 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
@override
TextEditingValue? get currentTextEditingValue => _value;
@override
void didChangeInputControl(
TextInputControl? oldControl, TextInputControl? newControl) {
if (_hasFocus && _hasInputConnection) {
oldControl?.hide();
newControl?.show();
}
}
@override
void insertTextPlaceholder(Size size) {
// Will not implement. This method is used for Scribble support.
@ -296,27 +313,15 @@ 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);
void _onChangedClipboardStatus() {
setState(() {
// Inform the widget that the value of clipboardStatus has changed.
});
}
/// Keyboard text editing actions.
// The Handling of the default text editing shortcuts with deltas
// needs to be in the framework somehow. This should go through some kind of
// needs to be in the framework somehow. This should go through some kind of
// generic "replace" method like in EditableText.
// EditableText converts intents like DeleteCharacterIntent to a generic
// ReplaceTextIntent. I wonder if that could be done at a higher level, so
@ -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.
void _didChangeTextEditingValue() {
_updateRemoteTextEditingValueIfNeeded();
@ -455,28 +478,6 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
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
// to avoid sending duplicate update messages to the engine.
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
// 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
@ -542,6 +544,20 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
// 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
void copySelection(SelectionChangedCause cause) {
final TextSelection copyRange = textEditingValue.selection;
@ -599,17 +615,6 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
_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
Future<void> pasteText(SelectionChangedCause cause) async {
final TextSelection pasteRange = textEditingValue.selection;
@ -649,6 +654,18 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
),
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
@ -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.
final LayerLink _startHandleLayerLink = LayerLink();
final LayerLink _endHandleLayerLink = LayerLink();
@ -711,14 +739,14 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
void _handleSelectionChanged(
TextSelection selection, SelectionChangedCause? cause) {
// 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.
if (!widget.controller.isSelectionWithinTextBounds(selection)) return;
widget.controller.selection = selection;
// 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
// it's not focused already.
switch (cause) {
@ -738,28 +766,12 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
}
break;
}
if (widget.selectionControls == null) {
if (widget.selectionControls == null && widget.contextMenuBuilder == null) {
_selectionOverlay?.dispose();
_selectionOverlay = null;
} else {
if (_selectionOverlay == null) {
_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();
},
magnifierConfiguration: TextMagnifierConfiguration.disabled,
);
_selectionOverlay = _createSelectionOverlay();
} else {
_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
Widget build(BuildContext context) {
return Actions(
@ -1018,3 +1160,18 @@ class _Editable extends MultiChildRenderObjectWidget {
..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;
}

Loading…
Cancel
Save