blob: b0a2414a5fc9c0b865f7f63bb3e3e5c1b4f01cd4 [file] [log] [blame]
// 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/scheduler.dart';
import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan;
import 'package:flutter/widgets.dart';
import 'text_selection_toolbar.dart';
import 'text_selection_toolbar_button.dart';
/// iOS only shows 3 spell check suggestions in the toolbar.
const int _maxSuggestions = 3;
/// The default spell check suggestions toolbar for iOS.
///
/// Tries to position itself below the [anchors], but if it doesn't fit, then it
/// readjusts to fit above bottom view insets.
class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar].
const CupertinoSpellCheckSuggestionsToolbar({
super.key,
required this.anchors,
required this.buttonItems,
});
/// The location on which to anchor the menu.
final TextSelectionToolbarAnchors anchors;
/// The [ContextMenuButtonItem]s that will be turned into the correct button
/// widgets and displayed in the spell check suggestions toolbar.
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s that are used to build the buttons of the
/// text selection toolbar.
/// * [SpellCheckSuggestionsToolbar.buttonItems], the list of
/// [ContextMenuButtonItem]s used to build the Material style spell check
/// suggestions toolbar.
final List<ContextMenuButtonItem> buttonItems;
/// Builds the button items for the toolbar based on the available
/// spell check suggestions.
static List<ContextMenuButtonItem>? buildButtonItems(
BuildContext context,
EditableTextState editableTextState,
) {
// Determine if composing region is misspelled.
final SuggestionSpan? spanAtCursorIndex =
editableTextState.findSuggestionSpanAtCursorIndex(
editableTextState.currentTextEditingValue.selection.baseOffset,
);
if (spanAtCursorIndex == null) {
return null;
}
if (spanAtCursorIndex.suggestions.isEmpty) {
return <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {},
label: 'No Replacements Found',
)
];
}
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
// Build suggestion buttons.
int suggestionCount = 0;
for (final String suggestion in spanAtCursorIndex.suggestions) {
if (suggestionCount >= _maxSuggestions) {
break;
}
buttonItems.add(ContextMenuButtonItem(
onPressed: () {
if (!editableTextState.mounted) {
return;
}
_replaceText(
editableTextState,
suggestion,
spanAtCursorIndex.range,
);
},
label: suggestion,
));
suggestionCount += 1;
}
return buttonItems;
}
static void _replaceText(EditableTextState editableTextState, String text, TextRange replacementRange) {
// Replacement cannot be performed if the text is read only or obscured.
assert(!editableTextState.widget.readOnly && !editableTextState.widget.obscureText);
final TextEditingValue newValue = editableTextState.textEditingValue.replaced(
replacementRange,
text,
);
editableTextState.userUpdateTextEditingValue(newValue,SelectionChangedCause.toolbar);
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
if (editableTextState.mounted) {
editableTextState.bringIntoView(editableTextState.textEditingValue.selection.extent);
}
});
editableTextState.hideToolbar();
editableTextState.renderEditable.selectWordEdge(cause: SelectionChangedCause.toolbar);
}
/// Builds the toolbar buttons based on the [buttonItems].
List<Widget> _buildToolbarButtons(BuildContext context) {
return buttonItems.map((ContextMenuButtonItem buttonItem) {
return CupertinoTextSelectionToolbarButton.buttonItem(
buttonItem: buttonItem,
);
}).toList();
}
@override
Widget build(BuildContext context) {
final List<Widget> children = _buildToolbarButtons(context);
return CupertinoTextSelectionToolbar(
anchorAbove: anchors.primaryAnchor,
anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!,
children: children,
);
}
}