mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-24 19:16:44 +08:00
feat: at user
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
// 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.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'selectable_text.dart';
|
||||
/// @docImport 'selection_area.dart';
|
||||
/// @docImport 'text_field.dart';
|
||||
library;
|
||||
|
||||
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/rendering.dart';
|
||||
|
||||
/// The default context menu for text selection for the current platform.
|
||||
///
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.contextMenuBuilders}
|
||||
/// Typically, this widget would be passed to `contextMenuBuilder` in a
|
||||
/// supported parent widget, such as:
|
||||
///
|
||||
/// * [EditableText.contextMenuBuilder]
|
||||
/// * [TextField.contextMenuBuilder]
|
||||
/// * [CupertinoTextField.contextMenuBuilder]
|
||||
/// * [SelectionArea.contextMenuBuilder]
|
||||
/// * [SelectableText.contextMenuBuilder]
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableText.getEditableButtonItems], which returns the default
|
||||
/// [ContextMenuButtonItem]s for [EditableText] on the platform.
|
||||
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
|
||||
/// Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar], which does the same thing as this
|
||||
/// widget but only for Cupertino context menus.
|
||||
/// * [TextSelectionToolbar], the default toolbar for Android.
|
||||
/// * [DesktopTextSelectionToolbar], the default toolbar for desktop platforms
|
||||
/// other than MacOS.
|
||||
/// * [CupertinoTextSelectionToolbar], the default toolbar for iOS.
|
||||
/// * [CupertinoDesktopTextSelectionToolbar], the default toolbar for MacOS.
|
||||
class AdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the
|
||||
/// given [children].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// * [AdaptiveTextSelectionToolbar.buttonItems], which takes a list of
|
||||
/// [ContextMenuButtonItem]s instead of [children] widgets.
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// * [AdaptiveTextSelectionToolbar.editable], which builds the default
|
||||
/// children for an editable field.
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// * [AdaptiveTextSelectionToolbar.editableText], which builds the default
|
||||
/// children for an [EditableText].
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
/// * [AdaptiveTextSelectionToolbar.selectable], which builds the default
|
||||
/// children for content that is selectable but not editable.
|
||||
/// {@endtemplate}
|
||||
const AdaptiveTextSelectionToolbar(
|
||||
{super.key, required this.children, required this.anchors})
|
||||
: buttonItems = null;
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] whose children will
|
||||
/// be built from the given [buttonItems].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// * [AdaptiveTextSelectionToolbar.new], which takes the children directly as
|
||||
/// a list of widgets.
|
||||
/// {@endtemplate}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
const AdaptiveTextSelectionToolbar.buttonItems({
|
||||
super.key,
|
||||
required this.buttonItems,
|
||||
required this.anchors,
|
||||
}) : children = null;
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for an editable field.
|
||||
///
|
||||
/// If an on* callback parameter is null, then its corresponding button will
|
||||
/// not be built.
|
||||
///
|
||||
/// These callbacks are called when their corresponding button is activated
|
||||
/// and only then. For example, `onPaste` is called when the user taps the
|
||||
/// "Paste" button in the context menu and not when the user pastes with the
|
||||
/// keyboard.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
AdaptiveTextSelectionToolbar.editable({
|
||||
super.key,
|
||||
required ClipboardStatus clipboardStatus,
|
||||
required VoidCallback? onCopy,
|
||||
required VoidCallback? onCut,
|
||||
required VoidCallback? onPaste,
|
||||
required VoidCallback? onSelectAll,
|
||||
required VoidCallback? onLookUp,
|
||||
required VoidCallback? onSearchWeb,
|
||||
required VoidCallback? onShare,
|
||||
required VoidCallback? onLiveTextInput,
|
||||
required this.anchors,
|
||||
}) : children = null,
|
||||
buttonItems = EditableText.getEditableButtonItems(
|
||||
clipboardStatus: clipboardStatus,
|
||||
onCopy: onCopy,
|
||||
onCut: onCut,
|
||||
onPaste: onPaste,
|
||||
onSelectAll: onSelectAll,
|
||||
onLookUp: onLookUp,
|
||||
onSearchWeb: onSearchWeb,
|
||||
onShare: onShare,
|
||||
onLiveTextInput: onLiveTextInput,
|
||||
);
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for an [EditableText].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
AdaptiveTextSelectionToolbar.editableText({
|
||||
super.key,
|
||||
required EditableTextState editableTextState,
|
||||
}) : children = null,
|
||||
buttonItems = editableTextState.contextMenuButtonItems,
|
||||
anchors = editableTextState.contextMenuAnchors;
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for selectable, but not editable, content.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
AdaptiveTextSelectionToolbar.selectable({
|
||||
super.key,
|
||||
required VoidCallback onCopy,
|
||||
required VoidCallback onSelectAll,
|
||||
required VoidCallback? onShare,
|
||||
required SelectionGeometry selectionGeometry,
|
||||
required this.anchors,
|
||||
}) : children = null,
|
||||
buttonItems = SelectableRegion.getSelectableButtonItems(
|
||||
selectionGeometry: selectionGeometry,
|
||||
onCopy: onCopy,
|
||||
onSelectAll: onSelectAll,
|
||||
onShare: onShare,
|
||||
);
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for a [SelectableRegion].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
AdaptiveTextSelectionToolbar.selectableRegion({
|
||||
super.key,
|
||||
required SelectableRegionState selectableRegionState,
|
||||
}) : children = null,
|
||||
buttonItems = selectableRegionState.contextMenuButtonItems,
|
||||
anchors = selectableRegionState.contextMenuAnchors;
|
||||
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||
/// widgets for the current platform.
|
||||
/// {@endtemplate}
|
||||
final List<ContextMenuButtonItem>? buttonItems;
|
||||
|
||||
/// The children of the toolbar, typically buttons.
|
||||
final List<Widget>? children;
|
||||
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.anchors}
|
||||
/// The location on which to anchor the menu.
|
||||
/// {@endtemplate}
|
||||
final TextSelectionToolbarAnchors anchors;
|
||||
|
||||
/// Returns the default button label String for the button of the given
|
||||
/// [ContextMenuButtonType] on any platform.
|
||||
static String getButtonLabel(
|
||||
BuildContext context, ContextMenuButtonItem buttonItem) {
|
||||
if (buttonItem.label != null) {
|
||||
return buttonItem.label!;
|
||||
}
|
||||
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return CupertinoTextSelectionToolbarButton.getButtonLabel(
|
||||
context, buttonItem);
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final MaterialLocalizations localizations =
|
||||
MaterialLocalizations.of(context);
|
||||
return switch (buttonItem.type) {
|
||||
ContextMenuButtonType.cut => localizations.cutButtonLabel,
|
||||
ContextMenuButtonType.copy => localizations.copyButtonLabel,
|
||||
ContextMenuButtonType.paste => localizations.pasteButtonLabel,
|
||||
ContextMenuButtonType.selectAll => localizations.selectAllButtonLabel,
|
||||
ContextMenuButtonType.delete =>
|
||||
localizations.deleteButtonTooltip.toUpperCase(),
|
||||
ContextMenuButtonType.lookUp => localizations.lookUpButtonLabel,
|
||||
ContextMenuButtonType.searchWeb => localizations.searchWebButtonLabel,
|
||||
ContextMenuButtonType.share => localizations.shareButtonLabel,
|
||||
ContextMenuButtonType.liveTextInput =>
|
||||
localizations.scanTextButtonLabel,
|
||||
ContextMenuButtonType.custom => '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a List of Widgets generated by turning [buttonItems] into the
|
||||
/// default context menu buttons for 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.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates how to use `getAdaptiveButtons` to generate
|
||||
/// default button widgets in a custom toolbar.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the
|
||||
/// Cupertino equivalent of this class and builds only the Cupertino
|
||||
/// buttons.
|
||||
static Iterable<Widget> getAdaptiveButtons(
|
||||
BuildContext context,
|
||||
List<ContextMenuButtonItem> buttonItems,
|
||||
) {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoTextSelectionToolbarButton.buttonItem(
|
||||
buttonItem: buttonItem);
|
||||
});
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.android:
|
||||
final List<Widget> buttons = <Widget>[];
|
||||
for (int i = 0; i < buttonItems.length; i++) {
|
||||
final ContextMenuButtonItem buttonItem = buttonItems[i];
|
||||
buttons.add(
|
||||
TextSelectionToolbarTextButton(
|
||||
padding: TextSelectionToolbarTextButton.getPadding(
|
||||
i, buttonItems.length),
|
||||
onPressed: buttonItem.onPressed,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(getButtonLabel(context, buttonItem)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return buttons;
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return DesktopTextSelectionToolbarButton.text(
|
||||
context: context,
|
||||
onPressed: buttonItem.onPressed,
|
||||
text: getButtonLabel(context, buttonItem),
|
||||
);
|
||||
});
|
||||
case TargetPlatform.macOS:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoDesktopTextSelectionToolbarButton.text(
|
||||
onPressed: buttonItem.onPressed,
|
||||
text: getButtonLabel(context, buttonItem),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there aren't any buttons to build, build an empty toolbar.
|
||||
if ((children != null && children!.isEmpty) ||
|
||||
(buttonItems != null && buttonItems!.isEmpty)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<Widget> resultChildren = children != null
|
||||
? children!
|
||||
: getAdaptiveButtons(context, buttonItems!).toList();
|
||||
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
return CupertinoTextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor == null
|
||||
? anchors.primaryAnchor
|
||||
: anchors.secondaryAnchor!,
|
||||
children: resultChildren,
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
return TextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor == null
|
||||
? anchors.primaryAnchor
|
||||
: anchors.secondaryAnchor!,
|
||||
children: resultChildren,
|
||||
);
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return DesktopTextSelectionToolbar(
|
||||
anchor: anchors.primaryAnchor, children: resultChildren);
|
||||
case TargetPlatform.macOS:
|
||||
return CupertinoDesktopTextSelectionToolbar(
|
||||
anchor: anchors.primaryAnchor,
|
||||
children: resultChildren,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// 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.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/rendering.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 VoidCallback? onLookUp,
|
||||
required VoidCallback? onSearchWeb,
|
||||
required VoidCallback? onShare,
|
||||
required VoidCallback? onLiveTextInput,
|
||||
required this.anchors,
|
||||
}) : children = null,
|
||||
buttonItems = EditableText.getEditableButtonItems(
|
||||
clipboardStatus: clipboardStatus,
|
||||
onCopy: onCopy,
|
||||
onCut: onCut,
|
||||
onPaste: onPaste,
|
||||
onSelectAll: onSelectAll,
|
||||
onLookUp: onLookUp,
|
||||
onSearchWeb: onSearchWeb,
|
||||
onShare: onShare,
|
||||
onLiveTextInput: onLiveTextInput,
|
||||
);
|
||||
|
||||
/// 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,
|
||||
onShare:
|
||||
null, // See https://github.com/flutter/flutter/issues/141775.
|
||||
);
|
||||
|
||||
/// {@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
|
||||
/// 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 [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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SelectionChangedCause, SuggestionSpan;
|
||||
|
||||
/// iOS only shows 3 spell check suggestions in the toolbar.
|
||||
const int _kMaxSuggestions = 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.
|
||||
///
|
||||
/// See also:
|
||||
/// * [SpellCheckSuggestionsToolbar], which is similar but for both the
|
||||
/// Material and Cupertino libraries.
|
||||
class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar].
|
||||
///
|
||||
/// [buttonItems] must not contain more than three items.
|
||||
const CupertinoSpellCheckSuggestionsToolbar({
|
||||
super.key,
|
||||
required this.anchors,
|
||||
required this.buttonItems,
|
||||
}) : assert(buttonItems.length <= _kMaxSuggestions);
|
||||
|
||||
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar] with the default
|
||||
/// children for an [EditableText].
|
||||
///
|
||||
/// See also:
|
||||
/// * [SpellCheckSuggestionsToolbar.editableText], which is similar but
|
||||
/// builds an Android-style toolbar.
|
||||
CupertinoSpellCheckSuggestionsToolbar.editableText({
|
||||
super.key,
|
||||
required EditableTextState editableTextState,
|
||||
}) : buttonItems =
|
||||
buildButtonItems(editableTextState) ?? <ContextMenuButtonItem>[],
|
||||
anchors = editableTextState.contextMenuAnchors;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Must not contain more than three items.
|
||||
///
|
||||
/// 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(
|
||||
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) {
|
||||
assert(debugCheckHasCupertinoLocalizations(editableTextState.context));
|
||||
final CupertinoLocalizations localizations = CupertinoLocalizations.of(
|
||||
editableTextState.context,
|
||||
);
|
||||
return <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
onPressed: null,
|
||||
label: localizations.noSpellCheckReplacementsLabel),
|
||||
];
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
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)
|
||||
.copyWith(
|
||||
selection: TextSelection.collapsed(
|
||||
offset: replacementRange.start + text.length));
|
||||
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: 'SpellCheckSuggestions.bringIntoView');
|
||||
editableTextState.hideToolbar();
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
if (buttonItems.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<Widget> children = _buildToolbarButtons(context);
|
||||
return CupertinoTextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor == null
|
||||
? anchors.primaryAnchor
|
||||
: anchors.secondaryAnchor!,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
1759
lib/common/widgets/text_field/cupertino/cupertino_text_field.dart
Normal file
1759
lib/common/widgets/text_field/cupertino/cupertino_text_field.dart
Normal file
File diff suppressed because it is too large
Load Diff
6609
lib/common/widgets/text_field/editable_text.dart
Normal file
6609
lib/common/widgets/text_field/editable_text.dart
Normal file
File diff suppressed because it is too large
Load Diff
461
lib/common/widgets/text_field/spell_check.dart
Normal file
461
lib/common/widgets/text_field/spell_check.dart
Normal file
@@ -0,0 +1,461 @@
|
||||
// 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.
|
||||
|
||||
/// @docImport 'editable_text.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
|
||||
|
||||
import 'editable_text.dart' show EditableTextContextMenuBuilder;
|
||||
|
||||
/// Controls how spell check is performed for text input.
|
||||
///
|
||||
/// This configuration determines the [SpellCheckService] used to fetch the
|
||||
/// [List<SuggestionSpan>] spell check results and the [TextStyle] used to
|
||||
/// mark misspelled words within text input.
|
||||
@immutable
|
||||
class SpellCheckConfiguration {
|
||||
/// Creates a configuration that specifies the service and suggestions handler
|
||||
/// for spell check.
|
||||
const SpellCheckConfiguration({
|
||||
this.spellCheckService,
|
||||
this.misspelledSelectionColor,
|
||||
this.misspelledTextStyle,
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
}) : _spellCheckEnabled = true;
|
||||
|
||||
/// Creates a configuration that disables spell check.
|
||||
const SpellCheckConfiguration.disabled()
|
||||
: _spellCheckEnabled = false,
|
||||
spellCheckService = null,
|
||||
spellCheckSuggestionsToolbarBuilder = null,
|
||||
misspelledTextStyle = null,
|
||||
misspelledSelectionColor = null;
|
||||
|
||||
/// The service used to fetch spell check results for text input.
|
||||
final SpellCheckService? spellCheckService;
|
||||
|
||||
/// The color the paint the selection highlight when spell check is showing
|
||||
/// suggestions for a misspelled word.
|
||||
///
|
||||
/// For example, on iOS, the selection appears red while the spell check menu
|
||||
/// is showing.
|
||||
final Color? misspelledSelectionColor;
|
||||
|
||||
/// Style used to indicate misspelled words.
|
||||
///
|
||||
/// This is nullable to allow style-specific wrappers of [EditableText]
|
||||
/// to infer this, but this must be specified if this configuration is
|
||||
/// provided directly to [EditableText] or its construction will fail with an
|
||||
/// assertion error.
|
||||
final TextStyle? misspelledTextStyle;
|
||||
|
||||
/// Builds the toolbar used to display spell check suggestions for misspelled
|
||||
/// words.
|
||||
final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder;
|
||||
|
||||
final bool _spellCheckEnabled;
|
||||
|
||||
/// Whether or not the configuration should enable or disable spell check.
|
||||
bool get spellCheckEnabled => _spellCheckEnabled;
|
||||
|
||||
/// Returns a copy of the current [SpellCheckConfiguration] instance with
|
||||
/// specified overrides.
|
||||
SpellCheckConfiguration copyWith({
|
||||
SpellCheckService? spellCheckService,
|
||||
Color? misspelledSelectionColor,
|
||||
TextStyle? misspelledTextStyle,
|
||||
EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder,
|
||||
}) {
|
||||
if (!_spellCheckEnabled) {
|
||||
// A new configuration should be constructed to enable spell check.
|
||||
return const SpellCheckConfiguration.disabled();
|
||||
}
|
||||
|
||||
return SpellCheckConfiguration(
|
||||
spellCheckService: spellCheckService ?? this.spellCheckService,
|
||||
misspelledSelectionColor:
|
||||
misspelledSelectionColor ?? this.misspelledSelectionColor,
|
||||
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder:
|
||||
spellCheckSuggestionsToolbarBuilder ??
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${objectRuntimeType(this, 'SpellCheckConfiguration')}('
|
||||
'${_spellCheckEnabled ? 'enabled' : 'disabled'}, '
|
||||
'service: $spellCheckService, '
|
||||
'text style: $misspelledTextStyle, '
|
||||
'toolbar builder: $spellCheckSuggestionsToolbarBuilder'
|
||||
')';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is SpellCheckConfiguration &&
|
||||
other.spellCheckService == spellCheckService &&
|
||||
other.misspelledTextStyle == misspelledTextStyle &&
|
||||
other.spellCheckSuggestionsToolbarBuilder ==
|
||||
spellCheckSuggestionsToolbarBuilder &&
|
||||
other._spellCheckEnabled == _spellCheckEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
spellCheckService,
|
||||
misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder,
|
||||
_spellCheckEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
// Methods for displaying spell check results:
|
||||
|
||||
/// Adjusts spell check results to correspond to [newText] if the only results
|
||||
/// that the handler has access to are the [results] corresponding to
|
||||
/// [resultsText].
|
||||
///
|
||||
/// Used in the case where the request for the spell check results of the
|
||||
/// [newText] is lagging in order to avoid display of incorrect results.
|
||||
List<SuggestionSpan> _correctSpellCheckResults(
|
||||
String newText,
|
||||
String resultsText,
|
||||
List<SuggestionSpan> results,
|
||||
) {
|
||||
final List<SuggestionSpan> correctedSpellCheckResults = <SuggestionSpan>[];
|
||||
int spanPointer = 0;
|
||||
int offset = 0;
|
||||
|
||||
// Assumes that the order of spans has not been jumbled for optimization
|
||||
// purposes, and will only search since the previously found span.
|
||||
int searchStart = 0;
|
||||
|
||||
while (spanPointer < results.length) {
|
||||
final SuggestionSpan currentSpan = results[spanPointer];
|
||||
final String currentSpanText = resultsText.substring(
|
||||
currentSpan.range.start,
|
||||
currentSpan.range.end,
|
||||
);
|
||||
final int spanLength = currentSpan.range.end - currentSpan.range.start;
|
||||
|
||||
// Try finding SuggestionSpan from resultsText in new text.
|
||||
final String escapedText = RegExp.escape(currentSpanText);
|
||||
final RegExp currentSpanTextRegexp = RegExp('\\b$escapedText\\b');
|
||||
final int foundIndex =
|
||||
newText.substring(searchStart).indexOf(currentSpanTextRegexp);
|
||||
|
||||
// Check whether word was found exactly where expected or elsewhere in the newText.
|
||||
final bool currentSpanFoundExactly =
|
||||
currentSpan.range.start == foundIndex + searchStart;
|
||||
final bool currentSpanFoundExactlyWithOffset =
|
||||
currentSpan.range.start + offset == foundIndex + searchStart;
|
||||
final bool currentSpanFoundElsewhere = foundIndex >= 0;
|
||||
|
||||
if (currentSpanFoundExactly || currentSpanFoundExactlyWithOffset) {
|
||||
// currentSpan was found at the same index in newText and resultsText
|
||||
// or at the same index with the previously calculated adjustment by
|
||||
// the offset value, so apply it to new text by adding it to the list of
|
||||
// corrected results.
|
||||
final SuggestionSpan adjustedSpan = SuggestionSpan(
|
||||
TextRange(
|
||||
start: currentSpan.range.start + offset,
|
||||
end: currentSpan.range.end + offset),
|
||||
currentSpan.suggestions,
|
||||
);
|
||||
|
||||
// Start search for the next misspelled word at the end of currentSpan.
|
||||
searchStart =
|
||||
math.min(currentSpan.range.end + 1 + offset, newText.length);
|
||||
correctedSpellCheckResults.add(adjustedSpan);
|
||||
} else if (currentSpanFoundElsewhere) {
|
||||
// Word was pushed forward but not modified.
|
||||
final int adjustedSpanStart = searchStart + foundIndex;
|
||||
final int adjustedSpanEnd = adjustedSpanStart + spanLength;
|
||||
final SuggestionSpan adjustedSpan = SuggestionSpan(
|
||||
TextRange(start: adjustedSpanStart, end: adjustedSpanEnd),
|
||||
currentSpan.suggestions,
|
||||
);
|
||||
|
||||
// Start search for the next misspelled word at the end of the
|
||||
// adjusted currentSpan.
|
||||
searchStart = math.min(adjustedSpanEnd + 1, newText.length);
|
||||
// Adjust offset to reflect the difference between where currentSpan
|
||||
// was positioned in resultsText versus in newText.
|
||||
offset = adjustedSpanStart - currentSpan.range.start;
|
||||
correctedSpellCheckResults.add(adjustedSpan);
|
||||
}
|
||||
spanPointer++;
|
||||
}
|
||||
return correctedSpellCheckResults;
|
||||
}
|
||||
|
||||
/// Builds the [TextSpan] tree given the current state of the text input and
|
||||
/// spell check results.
|
||||
///
|
||||
/// The [value] is the current [TextEditingValue] requested to be rendered
|
||||
/// by a text input widget. The [composingWithinCurrentTextRange] value
|
||||
/// represents whether or not there is a valid composing region in the
|
||||
/// [value]. The [style] is the [TextStyle] to render the [value]'s text with,
|
||||
/// and the [misspelledTextStyle] is the [TextStyle] to render misspelled
|
||||
/// words within the [value]'s text with. The [spellCheckResults] are the
|
||||
/// results of spell checking the [value]'s text.
|
||||
TextSpan buildTextSpanWithSpellCheckSuggestions(
|
||||
TextEditingValue value,
|
||||
bool composingWithinCurrentTextRange,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledTextStyle,
|
||||
SpellCheckResults spellCheckResults,
|
||||
) {
|
||||
List<SuggestionSpan> spellCheckResultsSpans =
|
||||
spellCheckResults.suggestionSpans;
|
||||
final String spellCheckResultsText = spellCheckResults.spellCheckedText;
|
||||
|
||||
if (spellCheckResultsText != value.text) {
|
||||
spellCheckResultsSpans = _correctSpellCheckResults(
|
||||
value.text,
|
||||
spellCheckResultsText,
|
||||
spellCheckResultsSpans,
|
||||
);
|
||||
}
|
||||
|
||||
// We will draw the TextSpan tree based on the composing region, if it is
|
||||
// available.
|
||||
// TODO(camsim99): The two separate strategies for building TextSpan trees
|
||||
// based on the availability of a composing region should be merged:
|
||||
// https://github.com/flutter/flutter/issues/124142.
|
||||
final bool shouldConsiderComposingRegion =
|
||||
defaultTargetPlatform == TargetPlatform.android;
|
||||
if (shouldConsiderComposingRegion) {
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: _buildSubtreesWithComposingRegion(
|
||||
spellCheckResultsSpans,
|
||||
value,
|
||||
style,
|
||||
misspelledTextStyle,
|
||||
composingWithinCurrentTextRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: _buildSubtreesWithoutComposingRegion(
|
||||
spellCheckResultsSpans,
|
||||
value,
|
||||
style,
|
||||
misspelledTextStyle,
|
||||
value.selection.baseOffset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the [TextSpan] tree for spell check without considering the composing
|
||||
/// region. Instead, uses the cursor to identify the word that's actively being
|
||||
/// edited and shouldn't be spell checked. This is useful for platforms and IMEs
|
||||
/// that don't use the composing region for the active word.
|
||||
List<TextSpan> _buildSubtreesWithoutComposingRegion(
|
||||
List<SuggestionSpan>? spellCheckSuggestions,
|
||||
TextEditingValue value,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledStyle,
|
||||
int cursorIndex,
|
||||
) {
|
||||
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
|
||||
|
||||
int textPointer = 0;
|
||||
int currentSpanPointer = 0;
|
||||
int endIndex;
|
||||
final String text = value.text;
|
||||
final TextStyle misspelledJointStyle =
|
||||
style?.merge(misspelledStyle) ?? misspelledStyle;
|
||||
bool cursorInCurrentSpan = false;
|
||||
|
||||
// Add text interwoven with any misspelled words to the tree.
|
||||
if (spellCheckSuggestions != null) {
|
||||
while (textPointer < text.length &&
|
||||
currentSpanPointer < spellCheckSuggestions.length) {
|
||||
final SuggestionSpan currentSpan =
|
||||
spellCheckSuggestions[currentSpanPointer];
|
||||
|
||||
if (currentSpan.range.start > textPointer) {
|
||||
endIndex = currentSpan.range.start < text.length
|
||||
? currentSpan.range.start
|
||||
: text.length;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
|
||||
);
|
||||
textPointer = endIndex;
|
||||
} else {
|
||||
endIndex = currentSpan.range.end < text.length
|
||||
? currentSpan.range.end
|
||||
: text.length;
|
||||
cursorInCurrentSpan = currentSpan.range.start <= cursorIndex &&
|
||||
currentSpan.range.end >= cursorIndex;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: cursorInCurrentSpan ? style : misspelledJointStyle,
|
||||
text: text.substring(currentSpan.range.start, endIndex),
|
||||
),
|
||||
);
|
||||
|
||||
textPointer = endIndex;
|
||||
currentSpanPointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text to the tree if applicable.
|
||||
if (textPointer < text.length) {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, text.length)),
|
||||
);
|
||||
}
|
||||
|
||||
return textSpanTreeChildren;
|
||||
}
|
||||
|
||||
/// Builds [TextSpan] subtree for text with misspelled words with logic based on
|
||||
/// a valid composing region.
|
||||
List<TextSpan> _buildSubtreesWithComposingRegion(
|
||||
List<SuggestionSpan>? spellCheckSuggestions,
|
||||
TextEditingValue value,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledStyle,
|
||||
bool composingWithinCurrentTextRange,
|
||||
) {
|
||||
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
|
||||
|
||||
int textPointer = 0;
|
||||
int currentSpanPointer = 0;
|
||||
int endIndex;
|
||||
SuggestionSpan currentSpan;
|
||||
final String text = value.text;
|
||||
final TextRange composingRegion = value.composing;
|
||||
final TextStyle composingTextStyle =
|
||||
style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
|
||||
const TextStyle(decoration: TextDecoration.underline);
|
||||
final TextStyle misspelledJointStyle =
|
||||
style?.merge(misspelledStyle) ?? misspelledStyle;
|
||||
bool textPointerWithinComposingRegion = false;
|
||||
bool currentSpanIsComposingRegion = false;
|
||||
|
||||
// Add text interwoven with any misspelled words to the tree.
|
||||
if (spellCheckSuggestions != null) {
|
||||
while (textPointer < text.length &&
|
||||
currentSpanPointer < spellCheckSuggestions.length) {
|
||||
currentSpan = spellCheckSuggestions[currentSpanPointer];
|
||||
|
||||
if (currentSpan.range.start > textPointer) {
|
||||
endIndex = currentSpan.range.start < text.length
|
||||
? currentSpan.range.start
|
||||
: text.length;
|
||||
textPointerWithinComposingRegion =
|
||||
composingRegion.start >= textPointer &&
|
||||
composingRegion.end <= endIndex &&
|
||||
!composingWithinCurrentTextRange;
|
||||
|
||||
if (textPointerWithinComposingRegion) {
|
||||
_addComposingRegionTextSpans(
|
||||
textSpanTreeChildren,
|
||||
text,
|
||||
textPointer,
|
||||
composingRegion,
|
||||
style,
|
||||
composingTextStyle,
|
||||
);
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: style,
|
||||
text: text.substring(composingRegion.end, endIndex)),
|
||||
);
|
||||
} else {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
|
||||
);
|
||||
}
|
||||
|
||||
textPointer = endIndex;
|
||||
} else {
|
||||
endIndex = currentSpan.range.end < text.length
|
||||
? currentSpan.range.end
|
||||
: text.length;
|
||||
currentSpanIsComposingRegion = textPointer >= composingRegion.start &&
|
||||
endIndex <= composingRegion.end &&
|
||||
!composingWithinCurrentTextRange;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: currentSpanIsComposingRegion
|
||||
? composingTextStyle
|
||||
: misspelledJointStyle,
|
||||
text: text.substring(currentSpan.range.start, endIndex),
|
||||
),
|
||||
);
|
||||
|
||||
textPointer = endIndex;
|
||||
currentSpanPointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text to the tree if applicable.
|
||||
if (textPointer < text.length) {
|
||||
if (textPointer < composingRegion.start &&
|
||||
!composingWithinCurrentTextRange) {
|
||||
_addComposingRegionTextSpans(
|
||||
textSpanTreeChildren,
|
||||
text,
|
||||
textPointer,
|
||||
composingRegion,
|
||||
style,
|
||||
composingTextStyle,
|
||||
);
|
||||
|
||||
if (composingRegion.end != text.length) {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: style,
|
||||
text: text.substring(composingRegion.end, text.length)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, text.length)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return textSpanTreeChildren;
|
||||
}
|
||||
|
||||
/// Helper method to create [TextSpan] tree children for specified range of
|
||||
/// text up to and including the composing region.
|
||||
void _addComposingRegionTextSpans(
|
||||
List<TextSpan> treeChildren,
|
||||
String text,
|
||||
int start,
|
||||
TextRange composingRegion,
|
||||
TextStyle? style,
|
||||
TextStyle composingTextStyle,
|
||||
) {
|
||||
treeChildren.add(TextSpan(
|
||||
style: style, text: text.substring(start, composingRegion.start)));
|
||||
treeChildren.add(
|
||||
TextSpan(
|
||||
style: composingTextStyle,
|
||||
text: text.substring(composingRegion.start, composingRegion.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
424
lib/common/widgets/text_field/system_context_menu.dart
Normal file
424
lib/common/widgets/text_field/system_context_menu.dart
Normal file
@@ -0,0 +1,424 @@
|
||||
// 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.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Displays the system context menu on top of the Flutter view.
|
||||
///
|
||||
/// Currently, only supports iOS 16.0 and above and displays nothing on other
|
||||
/// platforms.
|
||||
///
|
||||
/// The context menu is the menu that appears, for example, when doing text
|
||||
/// selection. Flutter typically draws this menu itself, but this class deals
|
||||
/// with the platform-rendered context menu instead.
|
||||
///
|
||||
/// There can only be one system context menu visible at a time. Building this
|
||||
/// widget when the system context menu is already visible will hide the old one
|
||||
/// and display this one. A system context menu that is hidden is informed via
|
||||
/// [onSystemHide].
|
||||
///
|
||||
/// Pass [items] to specify the buttons that will appear in the menu. Any items
|
||||
/// without a title will be given a default title from [WidgetsLocalizations].
|
||||
///
|
||||
/// By default, [items] will be set to the result of [getDefaultItems]. This
|
||||
/// method considers the state of the [EditableTextState] so that, for example,
|
||||
/// it will only include [IOSSystemContextMenuItemCopy] if there is currently a
|
||||
/// selection to copy.
|
||||
///
|
||||
/// To check if the current device supports showing the system context menu,
|
||||
/// call [isSupported].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to create a [TextField] that uses the system context
|
||||
/// menu where supported and does not show a system notification when the user
|
||||
/// presses the "Paste" button.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenuController], which directly controls the hiding and
|
||||
/// showing of the system context menu.
|
||||
class SystemContextMenu extends StatefulWidget {
|
||||
/// Creates an instance of [SystemContextMenu] that points to the given
|
||||
/// [anchor].
|
||||
const SystemContextMenu._({
|
||||
super.key,
|
||||
required this.anchor,
|
||||
required this.items,
|
||||
this.onSystemHide,
|
||||
});
|
||||
|
||||
/// Creates an instance of [SystemContextMenu] for the field indicated by the
|
||||
/// given [EditableTextState].
|
||||
factory SystemContextMenu.editableText({
|
||||
Key? key,
|
||||
required EditableTextState editableTextState,
|
||||
List<IOSSystemContextMenuItem>? items,
|
||||
}) {
|
||||
final (
|
||||
startGlyphHeight: double startGlyphHeight,
|
||||
endGlyphHeight: double endGlyphHeight
|
||||
) = editableTextState.getGlyphHeights();
|
||||
|
||||
return SystemContextMenu._(
|
||||
key: key,
|
||||
anchor: TextSelectionToolbarAnchors.getSelectionRect(
|
||||
editableTextState.renderEditable,
|
||||
startGlyphHeight,
|
||||
endGlyphHeight,
|
||||
editableTextState.renderEditable.getEndpointsForSelection(
|
||||
editableTextState.textEditingValue.selection,
|
||||
),
|
||||
),
|
||||
items: items ?? getDefaultItems(editableTextState),
|
||||
onSystemHide: editableTextState.hideToolbar,
|
||||
);
|
||||
}
|
||||
|
||||
/// The [Rect] that the context menu should point to.
|
||||
final Rect anchor;
|
||||
|
||||
/// A list of the items to be displayed in the system context menu.
|
||||
///
|
||||
/// When passed, items will be shown regardless of the state of text input.
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will produce a copy button
|
||||
/// even when there is no selection to copy. Use [EditableTextState] and/or
|
||||
/// the result of [getDefaultItems] to add and remove items based on the state
|
||||
/// of the input.
|
||||
///
|
||||
/// Defaults to the result of [getDefaultItems].
|
||||
final List<IOSSystemContextMenuItem> items;
|
||||
|
||||
/// Called when the system hides this context menu.
|
||||
///
|
||||
/// For example, tapping outside of the context menu typically causes the
|
||||
/// system to hide the menu.
|
||||
///
|
||||
/// This is not called when showing a new system context menu causes another
|
||||
/// to be hidden.
|
||||
final VoidCallback? onSystemHide;
|
||||
|
||||
/// Whether the current device supports showing the system context menu.
|
||||
///
|
||||
/// Currently, this is only supported on newer versions of iOS.
|
||||
static bool isSupported(BuildContext context) {
|
||||
return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
|
||||
}
|
||||
|
||||
/// The default [items] for the given [EditableTextState].
|
||||
///
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will only be included when the
|
||||
/// field represented by the [EditableTextState] has a selection.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableTextState.contextMenuButtonItems], which provides the default
|
||||
/// [ContextMenuButtonItem]s for the Flutter-rendered context menu.
|
||||
static List<IOSSystemContextMenuItem> getDefaultItems(
|
||||
EditableTextState editableTextState) {
|
||||
return <IOSSystemContextMenuItem>[
|
||||
if (editableTextState.copyEnabled) const IOSSystemContextMenuItemCopy(),
|
||||
if (editableTextState.cutEnabled) const IOSSystemContextMenuItemCut(),
|
||||
if (editableTextState.pasteEnabled) const IOSSystemContextMenuItemPaste(),
|
||||
if (editableTextState.selectAllEnabled)
|
||||
const IOSSystemContextMenuItemSelectAll(),
|
||||
if (editableTextState.lookUpEnabled)
|
||||
const IOSSystemContextMenuItemLookUp(),
|
||||
if (editableTextState.searchWebEnabled)
|
||||
const IOSSystemContextMenuItemSearchWeb(),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
State<SystemContextMenu> createState() => _SystemContextMenuState();
|
||||
}
|
||||
|
||||
class _SystemContextMenuState extends State<SystemContextMenu> {
|
||||
late final SystemContextMenuController _systemContextMenuController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_systemContextMenuController =
|
||||
SystemContextMenuController(onSystemHide: widget.onSystemHide);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_systemContextMenuController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(SystemContextMenu.isSupported(context));
|
||||
|
||||
if (widget.items.isNotEmpty) {
|
||||
final WidgetsLocalizations localizations =
|
||||
WidgetsLocalizations.of(context);
|
||||
final List<IOSSystemContextMenuItemData> itemDatas = widget.items
|
||||
.map((IOSSystemContextMenuItem item) => item.getData(localizations))
|
||||
.toList();
|
||||
_systemContextMenuController.showWithItems(widget.anchor, itemDatas);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a context menu button that will be rendered in the iOS system
|
||||
/// context menu and not by Flutter itself.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemData], which performs a similar role but at the
|
||||
/// method channel level and mirrors the requirements of the method channel
|
||||
/// API.
|
||||
/// * [ContextMenuButtonItem], which performs a similar role for Flutter-drawn
|
||||
/// context menus.
|
||||
@immutable
|
||||
sealed class IOSSystemContextMenuItem {
|
||||
const IOSSystemContextMenuItem();
|
||||
|
||||
/// The text to display to the user.
|
||||
///
|
||||
/// Not exposed for some built-in menu items whose title is always set by the
|
||||
/// platform.
|
||||
String? get title => null;
|
||||
|
||||
/// Returns the representation of this class used by method channels.
|
||||
IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations);
|
||||
|
||||
@override
|
||||
int get hashCode => title.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is IOSSystemContextMenuItem && other.title == title;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// copy button.
|
||||
///
|
||||
/// Should only appear when there is a selection that can be copied.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataCopy], which specifies the data to be sent to
|
||||
/// the platform for this same button.
|
||||
final class IOSSystemContextMenuItemCopy extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemCopy].
|
||||
const IOSSystemContextMenuItemCopy();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataCopy getData(WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataCopy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// cut button.
|
||||
///
|
||||
/// Should only appear when there is a selection that can be cut.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataCut], which specifies the data to be sent to
|
||||
/// the platform for this same button.
|
||||
final class IOSSystemContextMenuItemCut extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemCut].
|
||||
const IOSSystemContextMenuItemCut();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataCut getData(WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataCut();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// paste button.
|
||||
///
|
||||
/// Should only appear when the field can receive pasted content.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataPaste], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemPaste extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemPaste].
|
||||
const IOSSystemContextMenuItemPaste();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataPaste getData(
|
||||
WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataPaste();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// select all button.
|
||||
///
|
||||
/// Should only appear when the field can have its selection changed.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataSelectAll], which specifies the data to be
|
||||
/// sent to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemSelectAll extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemSelectAll].
|
||||
const IOSSystemContextMenuItemSelectAll();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataSelectAll getData(
|
||||
WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataSelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in look up button.
|
||||
///
|
||||
/// Should only appear when content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.lookUpButtonLabel].
|
||||
///
|
||||
/// The action is handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataLookUp], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemLookUp extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemLookUp].
|
||||
const IOSSystemContextMenuItemLookUp({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataLookUp getData(
|
||||
WidgetsLocalizations localizations) {
|
||||
return IOSSystemContextMenuItemDataLookUp(
|
||||
title: title ?? localizations.lookUpButtonLabel);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemLookUp(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in search web button.
|
||||
///
|
||||
/// Should only appear when content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.searchWebButtonLabel].
|
||||
///
|
||||
/// The action is handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataSearchWeb], which specifies the data to be
|
||||
/// sent to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemSearchWeb extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemSearchWeb].
|
||||
const IOSSystemContextMenuItemSearchWeb({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataSearchWeb getData(
|
||||
WidgetsLocalizations localizations) {
|
||||
return IOSSystemContextMenuItemDataSearchWeb(
|
||||
title: title ?? localizations.searchWebButtonLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemSearchWeb(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in share button.
|
||||
///
|
||||
/// Opens the system share dialog.
|
||||
///
|
||||
/// Should only appear when shareable content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.shareButtonLabel].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataShare], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemShare extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemShare].
|
||||
const IOSSystemContextMenuItemShare({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataShare getData(
|
||||
WidgetsLocalizations localizations) {
|
||||
return IOSSystemContextMenuItemDataShare(
|
||||
title: title ?? localizations.shareButtonLabel);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemShare(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(justinmc): Support the "custom" type.
|
||||
// https://github.com/flutter/flutter/issues/103163
|
||||
1962
lib/common/widgets/text_field/text_field.dart
Normal file
1962
lib/common/widgets/text_field/text_field.dart
Normal file
File diff suppressed because it is too large
Load Diff
1473
lib/common/widgets/text_field/text_selection.dart
Normal file
1473
lib/common/widgets/text_field/text_selection.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user