mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
260 lines
9.2 KiB
Dart
260 lines
9.2 KiB
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:PiliPlus/common/widgets/text_field/editable_text.dart';
|
|
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
|
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/services.dart'
|
|
show SelectionChangedCause, SuggestionSpan;
|
|
|
|
// The default height of the SpellCheckSuggestionsToolbar, which
|
|
// assumes there are the maximum number of spell check suggestions available, 3.
|
|
// Size eyeballed on Pixel 4 emulator running Android API 31.
|
|
const double _kDefaultToolbarHeight = 193.0;
|
|
|
|
/// The maximum number of suggestions in the toolbar is 3, plus a delete button.
|
|
const int _kMaxSuggestions = 3;
|
|
|
|
/// The default spell check suggestions toolbar for Android.
|
|
///
|
|
/// Tries to position itself below the [anchor], but if it doesn't fit, then it
|
|
/// readjusts to fit above bottom view insets.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [CupertinoSpellCheckSuggestionsToolbar], which is similar but builds an
|
|
/// iOS-style spell check toolbar.
|
|
class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
|
/// Constructs a [SpellCheckSuggestionsToolbar].
|
|
///
|
|
/// [buttonItems] must not contain more than four items, generally three
|
|
/// suggestions and one delete button.
|
|
const SpellCheckSuggestionsToolbar(
|
|
{super.key, required this.anchor, required this.buttonItems})
|
|
: assert(buttonItems.length <= _kMaxSuggestions + 1);
|
|
|
|
/// Constructs a [SpellCheckSuggestionsToolbar] with the default children for
|
|
/// an [EditableText].
|
|
///
|
|
/// See also:
|
|
/// * [CupertinoSpellCheckSuggestionsToolbar.editableText], which is similar
|
|
/// but builds an iOS-style toolbar.
|
|
SpellCheckSuggestionsToolbar.editableText({
|
|
super.key,
|
|
required EditableTextState editableTextState,
|
|
}) : buttonItems =
|
|
buildButtonItems(editableTextState) ?? <ContextMenuButtonItem>[],
|
|
anchor = getToolbarAnchor(editableTextState.contextMenuAnchors);
|
|
|
|
/// {@template flutter.material.SpellCheckSuggestionsToolbar.anchor}
|
|
/// The focal point below which the toolbar attempts to position itself.
|
|
/// {@endtemplate}
|
|
final Offset anchor;
|
|
|
|
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
|
/// widgets and displayed in the spell check suggestions toolbar.
|
|
///
|
|
/// Must not contain more than four items, typically three suggestions and a
|
|
/// delete button.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
|
|
/// [ContextMenuButtonItem]s that are used to build the buttons of the
|
|
/// text selection toolbar.
|
|
/// * [CupertinoSpellCheckSuggestionsToolbar.buttonItems], the list of
|
|
/// [ContextMenuButtonItem]s used to build the Cupertino 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(
|
|
EditableTextState editableTextState) {
|
|
// Determine if composing region is misspelled.
|
|
final SuggestionSpan? spanAtCursorIndex =
|
|
editableTextState.findSuggestionSpanAtCursorIndex(
|
|
editableTextState.currentTextEditingValue.selection.baseOffset,
|
|
);
|
|
|
|
if (spanAtCursorIndex == null) {
|
|
return null;
|
|
}
|
|
|
|
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
|
|
|
|
// Build suggestion buttons.
|
|
for (final String suggestion
|
|
in spanAtCursorIndex.suggestions.take(_kMaxSuggestions)) {
|
|
buttonItems.add(
|
|
ContextMenuButtonItem(
|
|
onPressed: () {
|
|
if (!editableTextState.mounted) {
|
|
return;
|
|
}
|
|
_replaceText(
|
|
editableTextState, suggestion, spanAtCursorIndex.range);
|
|
},
|
|
label: suggestion,
|
|
),
|
|
);
|
|
}
|
|
|
|
// Build delete button.
|
|
final ContextMenuButtonItem deleteButton = ContextMenuButtonItem(
|
|
onPressed: () {
|
|
if (!editableTextState.mounted) {
|
|
return;
|
|
}
|
|
_replaceText(editableTextState, '',
|
|
editableTextState.currentTextEditingValue.composing);
|
|
},
|
|
type: ContextMenuButtonType.delete,
|
|
);
|
|
buttonItems.add(deleteButton);
|
|
|
|
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);
|
|
}
|
|
}, debugLabel: 'SpellCheckerSuggestionsToolbar.bringIntoView');
|
|
editableTextState.hideToolbar();
|
|
}
|
|
|
|
/// Determines the Offset that the toolbar will be anchored to.
|
|
static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) {
|
|
// Since this will be positioned below the anchor point, use the secondary
|
|
// anchor by default.
|
|
return anchors.secondaryAnchor == null
|
|
? anchors.primaryAnchor
|
|
: anchors.secondaryAnchor!;
|
|
}
|
|
|
|
/// Builds the toolbar buttons based on the [buttonItems].
|
|
List<Widget> _buildToolbarButtons(BuildContext context) {
|
|
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
|
final TextSelectionToolbarTextButton button =
|
|
TextSelectionToolbarTextButton(
|
|
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
|
|
onPressed: buttonItem.onPressed,
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem),
|
|
style: buttonItem.type == ContextMenuButtonType.delete
|
|
? const TextStyle(color: Colors.blue)
|
|
: null,
|
|
),
|
|
);
|
|
|
|
if (buttonItem.type != ContextMenuButtonType.delete) {
|
|
return button;
|
|
}
|
|
return DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
border: Border(top: BorderSide(color: Colors.grey))),
|
|
child: button,
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (buttonItems.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
// Adjust toolbar height if needed.
|
|
final double spellCheckSuggestionsToolbarHeight =
|
|
_kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length));
|
|
// Incorporate the padding distance between the content and toolbar.
|
|
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
|
final double softKeyboardViewInsetsBottom =
|
|
mediaQueryData.viewInsets.bottom;
|
|
final double paddingAbove = mediaQueryData.padding.top +
|
|
CupertinoTextSelectionToolbar.kToolbarScreenPadding;
|
|
// Makes up for the Padding.
|
|
final Offset localAdjustment = Offset(
|
|
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
|
paddingAbove,
|
|
);
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
|
paddingAbove,
|
|
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
|
CupertinoTextSelectionToolbar.kToolbarScreenPadding +
|
|
softKeyboardViewInsetsBottom,
|
|
),
|
|
child: CustomSingleChildLayout(
|
|
delegate: SpellCheckSuggestionsToolbarLayoutDelegate(
|
|
anchor: anchor - localAdjustment),
|
|
child: AnimatedSize(
|
|
// This duration was eyeballed on a Pixel 2 emulator running Android
|
|
// API 28 for the Material TextSelectionToolbar.
|
|
duration: const Duration(milliseconds: 140),
|
|
child: _SpellCheckSuggestionsToolbarContainer(
|
|
height: spellCheckSuggestionsToolbarHeight,
|
|
children: <Widget>[..._buildToolbarButtons(context)],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The Material-styled toolbar outline for the spell check suggestions
|
|
/// toolbar.
|
|
class _SpellCheckSuggestionsToolbarContainer extends StatelessWidget {
|
|
const _SpellCheckSuggestionsToolbarContainer(
|
|
{required this.height, required this.children});
|
|
|
|
final double height;
|
|
final List<Widget> children;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
// This elevation was eyeballed on a Pixel 4 emulator running Android
|
|
// API 31 for the SpellCheckSuggestionsToolbar.
|
|
elevation: 2.0,
|
|
type: MaterialType.card,
|
|
child: SizedBox(
|
|
// This width was eyeballed on a Pixel 4 emulator running Android
|
|
// API 31 for the SpellCheckSuggestionsToolbar.
|
|
width: 165.0,
|
|
height: height,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: children,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|