mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-02 00:58:19 +08:00
update richtextfield
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -311,8 +311,7 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
@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)) {
|
||||
if ((children ?? buttonItems)?.isEmpty ?? true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@
|
||||
/// @docImport 'package:flutter/cupertino.dart';
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui'
|
||||
as ui
|
||||
@@ -17,9 +16,9 @@ import 'dart:ui'
|
||||
TextBox;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -153,7 +152,10 @@ class VerticalCaretMovementRun implements Iterator<TextPosition> {
|
||||
final TextPosition closestPosition = _editable._textPainter
|
||||
.getPositionForOffset(newOffset);
|
||||
final MapEntry<Offset, TextPosition> position =
|
||||
MapEntry<Offset, TextPosition>(newOffset, closestPosition);
|
||||
MapEntry<Offset, TextPosition>(
|
||||
newOffset,
|
||||
closestPosition,
|
||||
);
|
||||
_positionCache[lineNumber] = position;
|
||||
return position;
|
||||
}
|
||||
@@ -294,8 +296,8 @@ class RenderEditable extends RenderBox
|
||||
bool paintCursorAboveText = false,
|
||||
Offset cursorOffset = Offset.zero,
|
||||
double devicePixelRatio = 1.0,
|
||||
ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.max,
|
||||
ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.max,
|
||||
bool? enableInteractiveSelection,
|
||||
this.floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5),
|
||||
TextRange? promptRectRange,
|
||||
@@ -1301,7 +1303,7 @@ class RenderEditable extends RenderBox
|
||||
// can be re-used when [assembleSemanticsNode] is called again. This ensures
|
||||
// stable ids for the [SemanticsNode]s of [TextSpan]s across
|
||||
// [assembleSemanticsNode] invocations.
|
||||
LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes;
|
||||
Map<Key, SemanticsNode>? _cachedChildNodes;
|
||||
|
||||
/// Returns a list of rects that bound the given selection, and the text
|
||||
/// direction. The text direction is used by the engine to calculate
|
||||
@@ -1311,7 +1313,11 @@ class RenderEditable extends RenderBox
|
||||
List<TextBox> getBoxesForSelection(TextSelection selection) {
|
||||
_computeTextMetricsIfNeeded();
|
||||
return _textPainter
|
||||
.getBoxesForSelection(selection)
|
||||
.getBoxesForSelection(
|
||||
selection,
|
||||
boxHeightStyle: selectionHeightStyle,
|
||||
boxWidthStyle: selectionWidthStyle,
|
||||
)
|
||||
.map(
|
||||
(TextBox textBox) => TextBox.fromLTRBD(
|
||||
textBox.left + _paintOffset.dx,
|
||||
@@ -1381,6 +1387,7 @@ class RenderEditable extends RenderBox
|
||||
..isMultiline = _isMultiline
|
||||
..textDirection = textDirection
|
||||
..isFocused = hasFocus
|
||||
..isFocusable = true
|
||||
..isTextField = true
|
||||
..isReadOnly = readOnly
|
||||
// This is the default for customer that uses RenderEditable directly.
|
||||
@@ -1437,8 +1444,7 @@ class RenderEditable extends RenderBox
|
||||
int placeholderIndex = 0;
|
||||
int childIndex = 0;
|
||||
RenderBox? child = firstChild;
|
||||
final LinkedHashMap<Key, SemanticsNode> newChildCache =
|
||||
LinkedHashMap<Key, SemanticsNode>();
|
||||
final Map<Key, SemanticsNode> newChildCache = <Key, SemanticsNode>{};
|
||||
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
|
||||
for (final InlineSpanSemanticsInformation info
|
||||
in _cachedCombinedSemanticsInfos!) {
|
||||
@@ -1507,8 +1513,9 @@ class RenderEditable extends RenderBox
|
||||
onDoubleTap: final VoidCallback? handler,
|
||||
):
|
||||
if (handler != null) {
|
||||
configuration.onTap = handler;
|
||||
configuration.isLink = true;
|
||||
configuration
|
||||
..onTap = handler
|
||||
..isLink = true;
|
||||
}
|
||||
case LongPressGestureRecognizer(
|
||||
onLongPress: final GestureLongPressCallback? onLongPress,
|
||||
@@ -2203,17 +2210,14 @@ class RenderEditable extends RenderBox
|
||||
Offset? to,
|
||||
required SelectionChangedCause cause,
|
||||
}) {
|
||||
final localFrom = globalToLocal(from);
|
||||
_computeTextMetricsIfNeeded();
|
||||
final localFrom = globalToLocal(from);
|
||||
final TextPosition fromPosition = _textPainter.getPositionForOffset(
|
||||
localFrom - _paintOffset,
|
||||
);
|
||||
|
||||
final TextPosition? toPosition = to == null
|
||||
? null
|
||||
: _textPainter.getPositionForOffset(
|
||||
globalToLocal(to) - _paintOffset,
|
||||
);
|
||||
: _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset);
|
||||
|
||||
int baseOffset = fromPosition.offset;
|
||||
int extentOffset = toPosition?.offset ?? fromPosition.offset;
|
||||
@@ -2265,7 +2269,6 @@ class RenderEditable extends RenderBox
|
||||
/// beginning and end of a word respectively.
|
||||
///
|
||||
/// {@macro flutter.rendering.RenderEditable.selectPosition}
|
||||
|
||||
void selectWordsInRange({
|
||||
required Offset from,
|
||||
Offset? to,
|
||||
@@ -2278,9 +2281,7 @@ class RenderEditable extends RenderBox
|
||||
final TextSelection fromWord = getWordAtOffset(fromPosition);
|
||||
final TextPosition toPosition = to == null
|
||||
? fromPosition
|
||||
: _textPainter.getPositionForOffset(
|
||||
globalToLocal(to) - _paintOffset,
|
||||
);
|
||||
: _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset);
|
||||
final TextSelection toWord = toPosition == fromPosition
|
||||
? fromWord
|
||||
: getWordAtOffset(toPosition);
|
||||
@@ -2526,9 +2527,7 @@ class RenderEditable extends RenderBox
|
||||
..layout(minWidth: minWidth, maxWidth: maxWidth);
|
||||
final double width = forceLine
|
||||
? constraints.maxWidth
|
||||
: constraints.constrainWidth(
|
||||
_textIntrinsics.size.width + _caretMargin,
|
||||
);
|
||||
: constraints.constrainWidth(_textIntrinsics.size.width + _caretMargin);
|
||||
return Size(
|
||||
width,
|
||||
constraints.constrainHeight(_preferredHeight(constraints.maxWidth)),
|
||||
@@ -2603,8 +2602,9 @@ class RenderEditable extends RenderBox
|
||||
_backgroundRenderObject?.layout(painterConstraints);
|
||||
|
||||
_maxScrollExtent = _getMaxScrollExtent(contentSize);
|
||||
offset.applyViewportDimension(_viewportExtent);
|
||||
offset.applyContentDimensions(0.0, _maxScrollExtent);
|
||||
offset
|
||||
..applyViewportDimension(_viewportExtent)
|
||||
..applyContentDimensions(0.0, _maxScrollExtent);
|
||||
}
|
||||
|
||||
// The relative origin in relation to the distance the user has theoretically
|
||||
@@ -2942,28 +2942,29 @@ class RenderEditable extends RenderBox
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('cursorColor', cursorColor));
|
||||
properties.add(
|
||||
DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines));
|
||||
properties.add(IntProperty('minLines', minLines));
|
||||
properties.add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false),
|
||||
);
|
||||
properties.add(ColorProperty('selectionColor', selectionColor));
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<TextSelection>('selection', selection));
|
||||
properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
|
||||
properties
|
||||
..add(ColorProperty('cursorColor', cursorColor))
|
||||
..add(
|
||||
DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines))
|
||||
..add(IntProperty('minLines', minLines))
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false),
|
||||
)
|
||||
..add(ColorProperty('selectionColor', selectionColor))
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(DiagnosticsProperty<TextSelection>('selection', selection))
|
||||
..add(DiagnosticsProperty<ViewportOffset>('offset', offset));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3154,11 +3155,13 @@ class _TextHighlightPainter extends RenderEditablePainter {
|
||||
|
||||
highlightPaint.color = color;
|
||||
final TextPainter textPainter = renderEditable._textPainter;
|
||||
final List<TextBox> boxes = textPainter.getBoxesForSelection(
|
||||
TextSelection(baseOffset: range.start, extentOffset: range.end),
|
||||
boxHeightStyle: selectionHeightStyle,
|
||||
boxWidthStyle: selectionWidthStyle,
|
||||
);
|
||||
final Set<TextBox> boxes = textPainter
|
||||
.getBoxesForSelection(
|
||||
TextSelection(baseOffset: range.start, extentOffset: range.end),
|
||||
boxHeightStyle: selectionHeightStyle,
|
||||
boxWidthStyle: selectionWidthStyle,
|
||||
)
|
||||
.toSet();
|
||||
|
||||
for (final TextBox box in boxes) {
|
||||
canvas.drawRect(
|
||||
@@ -3215,7 +3218,7 @@ class _CaretPainter extends RenderEditablePainter {
|
||||
Color? get caretColor => _caretColor;
|
||||
Color? _caretColor;
|
||||
set caretColor(Color? value) {
|
||||
if (caretColor?.value == value?.value) {
|
||||
if (caretColor?.toARGB32() == value?.toARGB32()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3246,7 +3249,7 @@ class _CaretPainter extends RenderEditablePainter {
|
||||
Color? get backgroundCursorColor => _backgroundCursorColor;
|
||||
Color? _backgroundCursorColor;
|
||||
set backgroundCursorColor(Color? value) {
|
||||
if (backgroundCursorColor?.value == value?.value) {
|
||||
if (backgroundCursorColor?.toARGB32() == value?.toARGB32()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3318,7 +3321,7 @@ class _CaretPainter extends RenderEditablePainter {
|
||||
paintRegularCursor(canvas, renderEditable, caretColor, caretTextPosition);
|
||||
}
|
||||
|
||||
final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75);
|
||||
final Color? floatingCursorColor = this.caretColor?.withValues(alpha: 0.75);
|
||||
// Floating Cursor.
|
||||
if (floatingCursorRect == null ||
|
||||
floatingCursorColor == null ||
|
||||
|
||||
@@ -20,7 +20,6 @@ import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui hide TextStyle;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart';
|
||||
@@ -30,34 +29,16 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide
|
||||
EditableText,
|
||||
EditableTextState,
|
||||
SpellCheckConfiguration,
|
||||
buildTextSpanWithSpellCheckSuggestions,
|
||||
TextSelectionOverlay,
|
||||
TextSelectionGestureDetectorBuilder;
|
||||
TextSelectionGestureDetectorBuilder,
|
||||
TextSelectionOverlay;
|
||||
import 'package:flutter/rendering.dart'
|
||||
hide RenderEditable, VerticalCaretMovementRun;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
export 'package:flutter/services.dart'
|
||||
show
|
||||
KeyboardInsertedContent,
|
||||
SelectionChangedCause,
|
||||
SmartDashesType,
|
||||
SmartQuotesType,
|
||||
TextEditingValue,
|
||||
TextInputType,
|
||||
TextSelection;
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
// late WidgetTester tester;
|
||||
|
||||
/// Signature for the callback that reports when the user changes the selection
|
||||
/// (including the cursor location).
|
||||
typedef SelectionChangedCallback =
|
||||
void Function(TextSelection selection, SelectionChangedCause? cause);
|
||||
|
||||
/// Signature for a widget builder that builds a context menu for the given
|
||||
/// [EditableTextState].
|
||||
///
|
||||
@@ -135,54 +116,6 @@ class _RenderCompositionCallback extends RenderProxyBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// A controller for an editable text field.
|
||||
///
|
||||
/// Whenever the user modifies a text field with an associated
|
||||
/// [RichTextEditingController], the text field updates [value] and the controller
|
||||
/// notifies its listeners. Listeners can then read the [text] and [selection]
|
||||
/// properties to learn what the user has typed or how the selection has been
|
||||
/// updated.
|
||||
///
|
||||
/// Similarly, if you modify the [text] or [selection] properties, the text
|
||||
/// field will be notified and will update itself appropriately.
|
||||
///
|
||||
/// A [RichTextEditingController] can also be used to provide an initial value for a
|
||||
/// text field. If you build a text field with a controller that already has
|
||||
/// [text], the text field will use that text as its initial value.
|
||||
///
|
||||
/// The [value] (as well as [text] and [selection]) of this controller can be
|
||||
/// updated from within a listener added to this controller. Be aware of
|
||||
/// infinite loops since the listener will also be notified of the changes made
|
||||
/// from within itself. Modifying the composing region from within a listener
|
||||
/// can also have a bad interaction with some input methods. Gboard, for
|
||||
/// example, will try to restore the composing region of the text if it was
|
||||
/// modified programmatically, creating an infinite loop of communications
|
||||
/// between the framework and the input method. Consider using
|
||||
/// [TextInputFormatter]s instead for as-you-type text modification.
|
||||
///
|
||||
/// If both the [text] and [selection] properties need to be changed, set the
|
||||
/// controller's [value] instead. Setting [text] will clear the selection
|
||||
/// and composing range.
|
||||
///
|
||||
/// Remember to [dispose] of the [RichTextEditingController] when it is no longer
|
||||
/// needed. This will ensure we discard any resources used by the object.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example creates a [TextField] with a [RichTextEditingController] whose
|
||||
/// change listener forces the entered text to be lower case and keeps the
|
||||
/// cursor at the end of the input.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextField], which is a Material Design text field that can be controlled
|
||||
/// with a [RichTextEditingController].
|
||||
/// * [EditableText], which is a raw region of editable text that can be
|
||||
/// controlled with a [RichTextEditingController].
|
||||
/// * Learn how to use a [RichTextEditingController] in one of our [cookbook recipes](https://docs.flutter.dev/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller).
|
||||
|
||||
// A time-value pair that represents a key frame in an animation.
|
||||
class _KeyFrame {
|
||||
const _KeyFrame(this.time, this.value);
|
||||
@@ -506,7 +439,7 @@ class EditableText extends StatefulWidget {
|
||||
this.readOnly = false,
|
||||
this.obscuringCharacter = '•',
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
bool? autocorrect,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
this.enableSuggestions = true,
|
||||
@@ -556,12 +489,13 @@ class EditableText extends StatefulWidget {
|
||||
this.cursorOpacityAnimates = false,
|
||||
this.cursorOffset,
|
||||
this.paintCursorAboveText = false,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
ui.BoxHeightStyle? selectionHeightStyle,
|
||||
ui.BoxWidthStyle? selectionWidthStyle,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.keyboardAppearance = Brightness.light,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
bool? enableInteractiveSelection,
|
||||
bool? selectAllOnFocus,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autocorrectionTextRectColor,
|
||||
@@ -586,7 +520,10 @@ class EditableText extends StatefulWidget {
|
||||
this.contextMenuBuilder,
|
||||
this.spellCheckConfiguration,
|
||||
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
|
||||
this.hintLocales,
|
||||
}) : assert(obscuringCharacter.length == 1),
|
||||
autocorrect =
|
||||
autocorrect ?? _inferAutocorrect(autofillHints: autofillHints),
|
||||
smartDashesType =
|
||||
smartDashesType ??
|
||||
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
@@ -608,6 +545,7 @@ class EditableText extends StatefulWidget {
|
||||
),
|
||||
enableInteractiveSelection =
|
||||
enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||
selectAllOnFocus = selectAllOnFocus ?? _defaultSelectAllOnFocus,
|
||||
toolbarOptions =
|
||||
selectionControls is TextSelectionHandleControls &&
|
||||
toolbarOptions == null
|
||||
@@ -647,7 +585,10 @@ class EditableText extends StatefulWidget {
|
||||
...inputFormatters ?? const Iterable<TextInputFormatter>.empty(),
|
||||
]
|
||||
: inputFormatters,
|
||||
showCursor = showCursor ?? !readOnly;
|
||||
showCursor = showCursor ?? !readOnly,
|
||||
selectionHeightStyle =
|
||||
selectionHeightStyle ?? defaultSelectionHeightStyle,
|
||||
selectionWidthStyle = selectionWidthStyle ?? defaultSelectionWidthStyle;
|
||||
|
||||
/// Controls the text being edited.
|
||||
final RichTextEditingController controller;
|
||||
@@ -737,7 +678,7 @@ class EditableText extends StatefulWidget {
|
||||
/// {@template flutter.widgets.editableText.autocorrect}
|
||||
/// Whether to enable autocorrection.
|
||||
///
|
||||
/// Defaults to true.
|
||||
/// False on iOS if [autofillHints] contains password-related hints, otherwise true.
|
||||
/// {@endtemplate}
|
||||
final bool autocorrect;
|
||||
|
||||
@@ -1302,7 +1243,7 @@ class EditableText extends StatefulWidget {
|
||||
/// [TextSelectionGestureDetectorBuilder] to wrap the [EditableText], and set
|
||||
/// [rendererIgnoresPointer] to true.
|
||||
///
|
||||
/// When [rendererIgnoresPointer] is true true, the [RenderEditable] created
|
||||
/// When [rendererIgnoresPointer] is true, the [RenderEditable] created
|
||||
/// by this widget will not handle pointer events.
|
||||
///
|
||||
/// This property is false by default.
|
||||
@@ -1396,8 +1337,9 @@ class EditableText extends StatefulWidget {
|
||||
/// cut/copy/paste menu, and tapping to move the text caret.
|
||||
///
|
||||
/// When this is false, the text selection cannot be adjusted by
|
||||
/// the user, text cannot be copied, and the user cannot paste into
|
||||
/// the text field from the clipboard.
|
||||
/// the user, the cut/copy/paste menu is hidden, and the shortcuts to
|
||||
/// cut/copy/paste text do nothing but stop propagation of the key event
|
||||
/// to other key event handlers in the focus chain.
|
||||
///
|
||||
/// Defaults to true.
|
||||
/// {@endtemplate}
|
||||
@@ -1480,6 +1422,16 @@ class EditableText extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
/// {@template flutter.widgets.editableText.selectAllOnFocus}
|
||||
/// Whether this field should select all text when gaining focus.
|
||||
///
|
||||
/// When false, focusing this text field will leave its
|
||||
/// existing text selection unchanged.
|
||||
///
|
||||
/// Defaults to true on web and desktop platforms, and false on mobile platforms.
|
||||
/// {@endtemplate}
|
||||
final bool selectAllOnFocus;
|
||||
|
||||
/// {@template flutter.widgets.editableText.autofillHints}
|
||||
/// A list of strings that helps the autofill service identify the type of this
|
||||
/// text input.
|
||||
@@ -1714,12 +1666,62 @@ class EditableText extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.magnifier.intro}
|
||||
final TextMagnifierConfiguration magnifierConfiguration;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.hintLocales}
|
||||
final List<Locale>? hintLocales;
|
||||
|
||||
/// The default value for [selectionHeightStyle].
|
||||
///
|
||||
/// On web platforms, this defaults to [ui.BoxHeightStyle.max].
|
||||
///
|
||||
/// On native platforms, this defaults to [ui.BoxHeightStyle.includeLineSpacingMiddle] for all
|
||||
/// platforms.
|
||||
static ui.BoxHeightStyle get defaultSelectionHeightStyle {
|
||||
if (kIsWeb) {
|
||||
return ui.BoxHeightStyle.max;
|
||||
}
|
||||
return ui.BoxHeightStyle.includeLineSpacingMiddle;
|
||||
}
|
||||
|
||||
/// The default value for [selectionWidthStyle].
|
||||
///
|
||||
/// On web platforms, this defaults to [ui.BoxWidthStyle.max] for Apple platforms running
|
||||
/// Safari (webkit) based browsers and [ui.BoxWidthStyle.tight] for all others.
|
||||
///
|
||||
/// On non-web platforms, this defaults to [ui.BoxWidthStyle.max].
|
||||
static ui.BoxWidthStyle get defaultSelectionWidthStyle {
|
||||
// if (kIsWeb) {
|
||||
// if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
// WebBrowserDetection.isSafari) {
|
||||
// // On macOS web, the selection width behavior differs when running on
|
||||
// // Chrom(e|ium) (blink) or Safari (webkit).
|
||||
// return ui.BoxWidthStyle.max;
|
||||
// }
|
||||
// return ui.BoxWidthStyle.tight;
|
||||
// }
|
||||
return ui.BoxWidthStyle.max;
|
||||
}
|
||||
|
||||
/// The default value for [stylusHandwritingEnabled].
|
||||
static const bool defaultStylusHandwritingEnabled = true;
|
||||
|
||||
bool get _userSelectionEnabled =>
|
||||
enableInteractiveSelection && (!readOnly || !obscureText);
|
||||
|
||||
/// The default value for [selectAllOnFocus].
|
||||
static bool get _defaultSelectAllOnFocus {
|
||||
if (kIsWeb) {
|
||||
return true;
|
||||
}
|
||||
return switch (defaultTargetPlatform) {
|
||||
TargetPlatform.android => false,
|
||||
TargetPlatform.iOS => false,
|
||||
TargetPlatform.fuchsia => false,
|
||||
TargetPlatform.linux => true,
|
||||
TargetPlatform.macOS => true,
|
||||
TargetPlatform.windows => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
|
||||
/// platform's default selection menu for an editable field.
|
||||
///
|
||||
@@ -1818,6 +1820,38 @@ class EditableText extends StatefulWidget {
|
||||
return resultButtonItem;
|
||||
}
|
||||
|
||||
// Infer the value of autocorrect from autofillHints.
|
||||
static bool _inferAutocorrect({required Iterable<String>? autofillHints}) {
|
||||
if (autofillHints == null || autofillHints.isEmpty || kIsWeb) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
// username, password and newPassword are password related hint.
|
||||
// newUsername is not supported on iOS.
|
||||
final bool passwordRelatedHint = autofillHints.any(
|
||||
(String hint) =>
|
||||
hint == AutofillHints.username ||
|
||||
hint == AutofillHints.password ||
|
||||
hint == AutofillHints.newPassword,
|
||||
);
|
||||
if (passwordRelatedHint) {
|
||||
// https://github.com/flutter/flutter/issues/134723
|
||||
// Set autocorrect to false to prevent password bar from flashing.
|
||||
return false;
|
||||
}
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Infer the keyboard type of an `EditableText` if it's not specified.
|
||||
static TextInputType _inferKeyboardType({
|
||||
required Iterable<String>? autofillHints,
|
||||
@@ -2000,7 +2034,7 @@ class EditableText extends StatefulWidget {
|
||||
DiagnosticsProperty<bool>(
|
||||
'autocorrect',
|
||||
autocorrect,
|
||||
defaultValue: true,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
@@ -2136,6 +2170,13 @@ class EditableText extends StatefulWidget {
|
||||
? const <String>[]
|
||||
: kDefaultContentInsertionMimeTypes,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<List<Locale>?>(
|
||||
'hintLocales',
|
||||
hintLocales,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2438,6 +2479,7 @@ class EditableTextState extends State<EditableText>
|
||||
if (selection.isCollapsed || widget.obscureText) {
|
||||
return;
|
||||
}
|
||||
// bggRGjQaUbCoE copySelection
|
||||
final String text =
|
||||
widget.controller.getSelectionText(selection) ??
|
||||
selection.textInside(textEditingValue.text);
|
||||
@@ -2455,7 +2497,6 @@ class EditableTextState extends State<EditableText>
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
// Collapse the selection and hide the toolbar and handles.
|
||||
|
||||
userUpdateTextEditingValue(
|
||||
TextEditingValue(
|
||||
text: textEditingValue.text,
|
||||
@@ -2480,6 +2521,7 @@ class EditableTextState extends State<EditableText>
|
||||
if (selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
// bggRGjQaUbCoE cutSelection
|
||||
final String text =
|
||||
widget.controller.getSelectionText(selection) ??
|
||||
selection.textInside(textEditingValue.text);
|
||||
@@ -2528,12 +2570,7 @@ class EditableTextState extends State<EditableText>
|
||||
selection.baseOffset,
|
||||
selection.extentOffset,
|
||||
);
|
||||
// final TextEditingValue collapsedTextEditingValue =
|
||||
// textEditingValue.copyWith(
|
||||
// selection: TextSelection.collapsed(offset: lastSelectionIndex),
|
||||
// );
|
||||
// final newValue = collapsedTextEditingValue.replaced(selection, text);
|
||||
|
||||
// bggRGjQaUbCoE _pasteText
|
||||
widget.controller.syncRichText(
|
||||
selection.isCollapsed
|
||||
? TextEditingDeltaInsertion(
|
||||
@@ -2551,15 +2588,12 @@ class EditableTextState extends State<EditableText>
|
||||
composing: TextRange.empty,
|
||||
),
|
||||
);
|
||||
|
||||
final newValue = _value.copyWith(
|
||||
text: widget.controller.plainText,
|
||||
selection: widget.controller.newSelection,
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
|
||||
userUpdateTextEditingValue(newValue, cause);
|
||||
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
// Schedule a call to bringIntoView() after renderEditable updates.
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -2579,7 +2613,6 @@ class EditableTextState extends State<EditableText>
|
||||
// selecting it.
|
||||
return;
|
||||
}
|
||||
|
||||
userUpdateTextEditingValue(
|
||||
textEditingValue.copyWith(
|
||||
selection: TextSelection(
|
||||
@@ -3520,7 +3553,9 @@ class EditableTextState extends State<EditableText>
|
||||
);
|
||||
case FloatingCursorDragState.End:
|
||||
// Resume cursor blinking.
|
||||
_startCursorBlink();
|
||||
if (_hasFocus) {
|
||||
_startCursorBlink();
|
||||
}
|
||||
// We skip animation if no update has happened.
|
||||
if (_lastTextPosition != null && _lastBoundedOffset != null) {
|
||||
_floatingCursorResetController!.value = 0.0;
|
||||
@@ -4417,15 +4452,6 @@ class EditableTextState extends State<EditableText>
|
||||
final bool textCommitted =
|
||||
!oldValue.composing.isCollapsed && value.composing.isCollapsed;
|
||||
final bool selectionChanged = oldValue.selection != value.selection;
|
||||
// if (!textChanged && selectionChanged) {
|
||||
// value = value.copyWith(
|
||||
// selection: widget.controller.updateSelection(
|
||||
// oldSelection: _value.selection,
|
||||
// newSelection: value.selection,
|
||||
// cause: cause,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
if (textChanged || textCommitted) {
|
||||
// Only apply input formatters if the text has changed (including uncommitted
|
||||
@@ -4689,17 +4715,9 @@ class EditableTextState extends State<EditableText>
|
||||
|
||||
TextSelection? _adjustedSelectionWhenFocused() {
|
||||
TextSelection? selection;
|
||||
final bool isDesktop = switch (defaultTargetPlatform) {
|
||||
TargetPlatform.android ||
|
||||
TargetPlatform.iOS ||
|
||||
TargetPlatform.fuchsia => false,
|
||||
TargetPlatform.macOS ||
|
||||
TargetPlatform.linux ||
|
||||
TargetPlatform.windows => true,
|
||||
};
|
||||
final bool shouldSelectAll =
|
||||
widget.selectAllOnFocus &&
|
||||
widget.selectionEnabled &&
|
||||
(kIsWeb || isDesktop) &&
|
||||
!_isMultiline &&
|
||||
!_nextFocusChangeIsInternal &&
|
||||
!_justResumed;
|
||||
@@ -5043,10 +5061,10 @@ class EditableTextState extends State<EditableText>
|
||||
}
|
||||
|
||||
/// Shows the magnifier at the position given by `positionToShow`,
|
||||
/// if there is no magnifier visible.
|
||||
/// if no magnifier exists.
|
||||
///
|
||||
/// Updates the magnifier to the position given by `positionToShow`,
|
||||
/// if there is a magnifier visible.
|
||||
/// if a magnifier exits.
|
||||
///
|
||||
/// Does nothing if a magnifier couldn't be shown, such as when the selection
|
||||
/// overlay does not currently exist.
|
||||
@@ -5055,22 +5073,20 @@ class EditableTextState extends State<EditableText>
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectionOverlay!.magnifierIsVisible) {
|
||||
if (_selectionOverlay!.magnifierExists) {
|
||||
_selectionOverlay!.updateMagnifier(positionToShow);
|
||||
} else {
|
||||
_selectionOverlay!.showMagnifier(positionToShow);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the magnifier if it is visible.
|
||||
/// Hides the magnifier.
|
||||
void hideMagnifier() {
|
||||
if (_selectionOverlay == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectionOverlay!.magnifierIsVisible) {
|
||||
_selectionOverlay!.hideMagnifier();
|
||||
}
|
||||
_selectionOverlay!.hideMagnifier();
|
||||
}
|
||||
|
||||
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
|
||||
@@ -5161,6 +5177,7 @@ class EditableTextState extends State<EditableText>
|
||||
allowedMimeTypes: widget.contentInsertionConfiguration == null
|
||||
? const <String>[]
|
||||
: widget.contentInsertionConfiguration!.allowedMimeTypes,
|
||||
hintLocales: widget.hintLocales,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5357,10 +5374,7 @@ class EditableTextState extends State<EditableText>
|
||||
|
||||
void _replaceText(ReplaceTextIntent intent) {
|
||||
final TextEditingValue oldValue = _value;
|
||||
// final TextEditingValue newValue = intent.currentTextEditingValue.replaced(
|
||||
// intent.replacementRange,
|
||||
// intent.replacementText,
|
||||
// );
|
||||
// bggRGjQaUbCoE _replaceText
|
||||
widget.controller.syncRichText(
|
||||
intent.replacementText.isEmpty
|
||||
? TextEditingDeltaDeletion(
|
||||
@@ -5387,7 +5401,6 @@ class EditableTextState extends State<EditableText>
|
||||
selection: widget.controller.newSelection,
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
|
||||
userUpdateTextEditingValue(newValue, intent.cause);
|
||||
|
||||
// If there's no change in text and selection (e.g. when selecting and
|
||||
@@ -5509,7 +5522,6 @@ class EditableTextState extends State<EditableText>
|
||||
}
|
||||
|
||||
bringIntoView(nextSelection.extent);
|
||||
|
||||
userUpdateTextEditingValue(
|
||||
_value.copyWith(selection: nextSelection),
|
||||
SelectionChangedCause.keyboard,
|
||||
@@ -5718,7 +5730,8 @@ class EditableTextState extends State<EditableText>
|
||||
),
|
||||
),
|
||||
ScrollToDocumentBoundaryIntent: _makeOverridable(
|
||||
CallbackAction<ScrollToDocumentBoundaryIntent>(
|
||||
_WebComposingDisablingCallbackAction<ScrollToDocumentBoundaryIntent>(
|
||||
this,
|
||||
onInvoke: _scrollToDocumentBoundary,
|
||||
),
|
||||
),
|
||||
@@ -5748,11 +5761,7 @@ class EditableTextState extends State<EditableText>
|
||||
// Copy Paste
|
||||
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
||||
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
|
||||
PasteTextIntent: _makeOverridable(
|
||||
CallbackAction<PasteTextIntent>(
|
||||
onInvoke: (PasteTextIntent intent) => pasteText(intent.cause),
|
||||
),
|
||||
),
|
||||
PasteTextIntent: _makeOverridable(_PasteSelectionAction(this)),
|
||||
|
||||
TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction),
|
||||
EditableTextTapOutsideIntent: _makeOverridable(
|
||||
@@ -5826,7 +5835,13 @@ class EditableTextState extends State<EditableText>
|
||||
? AxisDirection.down
|
||||
: AxisDirection.right,
|
||||
controller: _scrollController,
|
||||
physics: widget.scrollPhysics,
|
||||
// On iOS a single-line TextField should not scroll.
|
||||
physics:
|
||||
widget.scrollPhysics ??
|
||||
(!_isMultiline &&
|
||||
defaultTargetPlatform == TargetPlatform.iOS
|
||||
? const _NeverUserScrollableScrollPhysics()
|
||||
: null),
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
restorationId: widget.restorationId,
|
||||
// If a ScrollBehavior is not provided, only apply scrollbars when
|
||||
@@ -5970,15 +5985,19 @@ class EditableTextState extends State<EditableText>
|
||||
final int placeholderLocation = _value.text.length - _placeholderLocation;
|
||||
if (_isMultiline) {
|
||||
// The zero size placeholder here allows the line to break and keep the caret on the first line.
|
||||
placeholders.add(
|
||||
const _ScribblePlaceholder(child: SizedBox.shrink(), size: Size.zero),
|
||||
);
|
||||
placeholders.add(
|
||||
_ScribblePlaceholder(
|
||||
child: const SizedBox.shrink(),
|
||||
size: Size(renderEditable.size.width, 0.0),
|
||||
),
|
||||
);
|
||||
placeholders
|
||||
..add(
|
||||
const _ScribblePlaceholder(
|
||||
child: SizedBox.shrink(),
|
||||
size: Size.zero,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
_ScribblePlaceholder(
|
||||
child: const SizedBox.shrink(),
|
||||
size: Size(renderEditable.size.width, 0.0),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
placeholders.add(
|
||||
const _ScribblePlaceholder(
|
||||
@@ -6061,8 +6080,8 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
this.cursorRadius,
|
||||
required this.cursorOffset,
|
||||
required this.paintCursorAboveText,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
ui.BoxHeightStyle? selectionHeightStyle,
|
||||
ui.BoxWidthStyle? selectionWidthStyle,
|
||||
this.enableInteractiveSelection = true,
|
||||
required this.textSelectionDelegate,
|
||||
required this.devicePixelRatio,
|
||||
@@ -6070,7 +6089,11 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
this.promptRectColor,
|
||||
required this.clipBehavior,
|
||||
required this.controller,
|
||||
}) : super(
|
||||
}) : selectionHeightStyle =
|
||||
selectionHeightStyle ?? EditableText.defaultSelectionHeightStyle,
|
||||
selectionWidthStyle =
|
||||
selectionWidthStyle ?? EditableText.defaultSelectionWidthStyle,
|
||||
super(
|
||||
children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaler),
|
||||
);
|
||||
|
||||
@@ -6203,6 +6226,20 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NeverUserScrollableScrollPhysics extends ScrollPhysics {
|
||||
/// Creates a scroll physics that prevents scrolling with user input, for example
|
||||
/// by dragging, but still allows for programmatic scrolling.
|
||||
const _NeverUserScrollableScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
_NeverUserScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return _NeverUserScrollableScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
bool get allowUserScrolling => false;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class _ScribbleCacheKey {
|
||||
const _ScribbleCacheKey({
|
||||
@@ -6454,11 +6491,7 @@ class _CodePointBoundary extends TextBoundary {
|
||||
// ------------------------------- Text Actions -------------------------------
|
||||
class _DeleteTextAction<T extends DirectionalTextEditingIntent>
|
||||
extends ContextAction<T> {
|
||||
_DeleteTextAction(
|
||||
this.state,
|
||||
this.getTextBoundary,
|
||||
this._applyTextBoundary,
|
||||
);
|
||||
_DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary);
|
||||
|
||||
final EditableTextState state;
|
||||
final TextBoundary Function() getTextBoundary;
|
||||
@@ -6658,7 +6691,15 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent>
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb &&
|
||||
state.widget.selectionEnabled &&
|
||||
state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state._value.selection.isValid;
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateTextSelectionVerticallyAction<
|
||||
@@ -6745,7 +6786,33 @@ class _UpdateTextSelectionVerticallyAction<
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb &&
|
||||
state.widget.selectionEnabled &&
|
||||
state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state._value.selection.isValid;
|
||||
}
|
||||
}
|
||||
|
||||
class _WebComposingDisablingCallbackAction<T extends Intent>
|
||||
extends CallbackAction<T> {
|
||||
_WebComposingDisablingCallbackAction(this.state, {required super.onInvoke});
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
@override
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb &&
|
||||
state.widget.selectionEnabled &&
|
||||
state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isActionEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
@@ -6755,6 +6822,10 @@ class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
|
||||
@override
|
||||
Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) {
|
||||
if (!state.widget.selectionEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(
|
||||
@@ -6764,9 +6835,6 @@ class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state.widget.selectionEnabled;
|
||||
}
|
||||
|
||||
class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
|
||||
@@ -6776,16 +6844,35 @@ class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
|
||||
|
||||
@override
|
||||
void invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
|
||||
if (!state._value.selection.isValid || state._value.selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.widget.selectionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.collapseSelection) {
|
||||
state.cutSelection(intent.cause);
|
||||
} else {
|
||||
state.copySelection(intent.cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PasteSelectionAction extends ContextAction<PasteTextIntent> {
|
||||
_PasteSelectionAction(this.state);
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
@override
|
||||
bool get isActionEnabled =>
|
||||
state._value.selection.isValid && !state._value.selection.isCollapsed;
|
||||
void invoke(PasteTextIntent intent, [BuildContext? context]) {
|
||||
if (!state.widget.selectionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.pasteText(intent.cause);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [ClipboardStatusNotifier] whose [value] is hardcoded to
|
||||
|
||||
@@ -5,15 +5,11 @@
|
||||
/// @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 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'
|
||||
show EditableTextContextMenuBuilder;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart' show SpellCheckService;
|
||||
|
||||
/// Controls how spell check is performed for text input.
|
||||
///
|
||||
@@ -121,351 +117,3 @@ class SpellCheckConfiguration {
|
||||
_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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// 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/flutter/text_field/adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart'
|
||||
hide EditableText, EditableTextState, AdaptiveTextSelectionToolbar;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SelectionChangedCause, SuggestionSpan;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -80,7 +81,7 @@ class SystemContextMenu extends StatefulWidget {
|
||||
),
|
||||
),
|
||||
items: items ?? getDefaultItems(editableTextState),
|
||||
onSystemHide: editableTextState.hideToolbar,
|
||||
onSystemHide: () => editableTextState.hideToolbar(false),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,6 +97,13 @@ class SystemContextMenu extends StatefulWidget {
|
||||
/// of the input.
|
||||
///
|
||||
/// Defaults to the result of [getDefaultItems].
|
||||
///
|
||||
/// To add custom menu items, pass [IOSSystemContextMenuItemCustom] instances
|
||||
/// in the [items] list. Each custom item requires a title and an onPressed callback.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [IOSSystemContextMenuItemCustom], which creates custom menu items.
|
||||
final List<IOSSystemContextMenuItem> items;
|
||||
|
||||
/// Called when the system hides this context menu.
|
||||
@@ -110,8 +118,30 @@ class SystemContextMenu extends StatefulWidget {
|
||||
/// Whether the current device supports showing the system context menu.
|
||||
///
|
||||
/// Currently, this is only supported on newer versions of iOS.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [isSupportedByField], which uses this method and determines whether an
|
||||
/// individual [EditableTextState] supports the system context menu.
|
||||
static bool isSupported(BuildContext context) {
|
||||
return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
|
||||
return defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
(MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false);
|
||||
}
|
||||
|
||||
/// Whether the given field supports showing the system context menu.
|
||||
///
|
||||
/// Currently [SystemContextMenu] is only supported with an active
|
||||
/// [TextInputConnection]. In cases where this isn't possible, such as in a
|
||||
/// read-only field, fall back to using a Flutter-rendered context menu like
|
||||
/// [AdaptiveTextSelectionToolbar].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [isSupported], which is used by this method and determines whether the
|
||||
/// platform in general supports showing the system context menu.
|
||||
static bool isSupportedByField(EditableTextState editableTextState) {
|
||||
return !editableTextState.widget.readOnly &&
|
||||
isSupported(editableTextState.context);
|
||||
}
|
||||
|
||||
/// The default [items] for the given [EditableTextState].
|
||||
@@ -136,6 +166,8 @@ class SystemContextMenu extends StatefulWidget {
|
||||
const IOSSystemContextMenuItemLookUp(),
|
||||
if (editableTextState.searchWebEnabled)
|
||||
const IOSSystemContextMenuItemSearchWeb(),
|
||||
if (editableTextState.liveTextInputEnabled)
|
||||
const IOSSystemContextMenuItemLiveText(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -177,259 +209,3 @@ class _SystemContextMenuState extends State<SystemContextMenu> {
|
||||
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
|
||||
|
||||
@@ -15,8 +15,8 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_text_field.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/text_field.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart';
|
||||
@@ -26,62 +26,31 @@ import 'package:flutter/cupertino.dart'
|
||||
hide
|
||||
EditableText,
|
||||
EditableTextState,
|
||||
CupertinoSpellCheckSuggestionsToolbar,
|
||||
SystemContextMenu,
|
||||
SpellCheckConfiguration,
|
||||
EditableTextContextMenuBuilder,
|
||||
buildTextSpanWithSpellCheckSuggestions,
|
||||
SystemContextMenu,
|
||||
CupertinoSpellCheckSuggestionsToolbar,
|
||||
SpellCheckConfiguration,
|
||||
CupertinoTextField,
|
||||
TextSelectionGestureDetectorBuilderDelegate,
|
||||
TextSelectionGestureDetectorBuilder,
|
||||
TextSelectionOverlay;
|
||||
TextSelectionOverlay,
|
||||
TextSelectionGestureDetectorBuilderDelegate;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide
|
||||
EditableText,
|
||||
EditableTextState,
|
||||
SpellCheckSuggestionsToolbar,
|
||||
EditableTextContextMenuBuilder,
|
||||
AdaptiveTextSelectionToolbar,
|
||||
SystemContextMenu,
|
||||
SpellCheckSuggestionsToolbar,
|
||||
SpellCheckConfiguration,
|
||||
EditableTextContextMenuBuilder,
|
||||
buildTextSpanWithSpellCheckSuggestions,
|
||||
TextSelectionGestureDetectorBuilderDelegate,
|
||||
TextSelectionGestureDetectorBuilder,
|
||||
TextSelectionOverlay;
|
||||
TextSelectionOverlay,
|
||||
TextSelectionGestureDetectorBuilderDelegate;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
export 'package:flutter/services.dart'
|
||||
show
|
||||
SmartDashesType,
|
||||
SmartQuotesType,
|
||||
TextCapitalization,
|
||||
TextInputAction,
|
||||
TextInputType;
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
// late FocusNode myFocusNode;
|
||||
|
||||
/// Signature for the [RichTextField.buildCounter] callback.
|
||||
typedef InputCounterWidgetBuilder =
|
||||
Widget? Function(
|
||||
/// The build context for the TextField.
|
||||
BuildContext context, {
|
||||
|
||||
/// The length of the string currently in the input.
|
||||
required int currentLength,
|
||||
|
||||
/// The maximum string length that can be entered into the TextField.
|
||||
required int? maxLength,
|
||||
|
||||
/// Whether or not the TextField is currently focused. Mainly provided for
|
||||
/// the [liveRegion] parameter in the [Semantics] widget for accessibility.
|
||||
required bool isFocused,
|
||||
});
|
||||
|
||||
class _TextFieldSelectionGestureDetectorBuilder
|
||||
extends TextSelectionGestureDetectorBuilder {
|
||||
_TextFieldSelectionGestureDetectorBuilder({
|
||||
@@ -209,6 +178,14 @@ class _TextFieldSelectionGestureDetectorBuilder
|
||||
/// [RichTextField] to ensure proper scroll coordination for [RichTextField] and its
|
||||
/// components like [TextSelectionOverlay].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates how to use the [Shortcuts] and [Actions] widgets
|
||||
/// to create a custom `Shift+Enter` keyboard shortcut for inserting a new line
|
||||
/// in a [RichTextField].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_field/text_field.3.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextFormField], which integrates with the [Form] widget.
|
||||
@@ -262,7 +239,8 @@ class RichTextField extends StatefulWidget {
|
||||
///
|
||||
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
|
||||
/// changing the shape of the selection highlighting. These properties default
|
||||
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight], respectively.
|
||||
/// to [EditableText.defaultSelectionHeightStyle] and
|
||||
/// [EditableText.defaultSelectionHeightStyle], respectively.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@@ -293,7 +271,7 @@ class RichTextField extends StatefulWidget {
|
||||
this.statesController,
|
||||
this.obscuringCharacter = '•',
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
this.autocorrect,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
this.enableSuggestions = true,
|
||||
@@ -315,12 +293,13 @@ class RichTextField extends StatefulWidget {
|
||||
this.cursorOpacityAnimates,
|
||||
this.cursorColor,
|
||||
this.cursorErrorColor,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
this.selectionHeightStyle,
|
||||
this.selectionWidthStyle,
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
bool? enableInteractiveSelection,
|
||||
this.selectAllOnFocus,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.onTapAlwaysCalled = false,
|
||||
@@ -346,6 +325,7 @@ class RichTextField extends StatefulWidget {
|
||||
this.canRequestFocus = true,
|
||||
this.spellCheckConfiguration,
|
||||
this.magnifierConfiguration,
|
||||
this.hintLocales,
|
||||
}) : assert(obscuringCharacter.length == 1),
|
||||
smartDashesType =
|
||||
smartDashesType ??
|
||||
@@ -526,7 +506,7 @@ class RichTextField extends StatefulWidget {
|
||||
final bool obscureText;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autocorrect}
|
||||
final bool autocorrect;
|
||||
final bool? autocorrect;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.smartDashesType}
|
||||
final SmartDashesType smartDashesType;
|
||||
@@ -578,6 +558,8 @@ class RichTextField extends StatefulWidget {
|
||||
/// field showing how many characters have been entered. If set to a number
|
||||
/// greater than 0, it will also display the maximum number allowed. If set
|
||||
/// to [RichTextField.noMaxLength] then only the current character count is displayed.
|
||||
/// To remove the counter, set [InputDecoration.counterText] to an empty string or
|
||||
/// return null from [RichTextField.buildCounter] callback.
|
||||
///
|
||||
/// After [maxLength] characters have been input, additional input
|
||||
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||
@@ -642,6 +624,27 @@ class RichTextField extends StatefulWidget {
|
||||
///
|
||||
/// If non-null this property overrides the [decoration]'s
|
||||
/// [InputDecoration.enabled] property.
|
||||
///
|
||||
/// When a text field is disabled, all of its children widgets are also
|
||||
/// disabled, including the [InputDecoration.suffixIcon]. If you need to keep
|
||||
/// the suffix icon interactive while disabling the text field, consider using
|
||||
/// [readOnly] and [enableInteractiveSelection] instead:
|
||||
///
|
||||
/// ```dart
|
||||
/// TextField(
|
||||
/// enabled: true,
|
||||
/// readOnly: true,
|
||||
/// enableInteractiveSelection: false,
|
||||
/// decoration: InputDecoration(
|
||||
/// suffixIcon: IconButton(
|
||||
/// onPressed: () {
|
||||
/// // This will work because the TextField is enabled
|
||||
/// },
|
||||
/// icon: const Icon(Icons.edit_outlined),
|
||||
/// ),
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
final bool? enabled;
|
||||
|
||||
/// Determines whether this widget ignores pointer events.
|
||||
@@ -683,12 +686,12 @@ class RichTextField extends StatefulWidget {
|
||||
/// Controls how tall the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||
final ui.BoxHeightStyle selectionHeightStyle;
|
||||
final ui.BoxHeightStyle? selectionHeightStyle;
|
||||
|
||||
/// Controls how wide the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxWidthStyle] for details on available styles.
|
||||
final ui.BoxWidthStyle selectionWidthStyle;
|
||||
final ui.BoxWidthStyle? selectionWidthStyle;
|
||||
|
||||
/// The appearance of the keyboard.
|
||||
///
|
||||
@@ -703,6 +706,9 @@ class RichTextField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
||||
final bool enableInteractiveSelection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectAllOnFocus}
|
||||
final bool? selectAllOnFocus;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionControls}
|
||||
final TextSelectionControls? selectionControls;
|
||||
|
||||
@@ -837,7 +843,7 @@ class RichTextField extends StatefulWidget {
|
||||
/// offset and - if no [controller] has been provided - the content of the
|
||||
/// text field. If a [controller] has been provided, it is the responsibility
|
||||
/// of the owner of that controller to persist and restore it, e.g. by using
|
||||
/// a [RestorableTextEditingController].
|
||||
/// a [RestorableRichTextEditingController].
|
||||
///
|
||||
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
||||
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
||||
@@ -883,12 +889,14 @@ class RichTextField extends StatefulWidget {
|
||||
/// be possible to move the focus to the text field with tab key.
|
||||
final bool canRequestFocus;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.hintLocales}
|
||||
final List<Locale>? hintLocales;
|
||||
|
||||
static Widget _defaultContextMenuBuilder(
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
SystemContextMenu.isSupported(context)) {
|
||||
if (SystemContextMenu.isSupportedByField(editableTextState)) {
|
||||
return SystemContextMenu.editableText(
|
||||
editableTextState: editableTextState,
|
||||
);
|
||||
@@ -991,7 +999,9 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null))
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<InputDecoration>(
|
||||
'decoration',
|
||||
@@ -1006,7 +1016,9 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: TextInputType.text,
|
||||
),
|
||||
)
|
||||
..add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null))
|
||||
..add(
|
||||
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false),
|
||||
)
|
||||
@@ -1028,7 +1040,7 @@ class RichTextField extends StatefulWidget {
|
||||
DiagnosticsProperty<bool>(
|
||||
'autocorrect',
|
||||
autocorrect,
|
||||
defaultValue: true,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
@@ -1058,7 +1070,9 @@ class RichTextField extends StatefulWidget {
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, defaultValue: 1))
|
||||
..add(IntProperty('minLines', minLines, defaultValue: null))
|
||||
..add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false))
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false),
|
||||
)
|
||||
..add(IntProperty('maxLength', maxLength, defaultValue: null))
|
||||
..add(
|
||||
EnumProperty<MaxLengthEnforcement>(
|
||||
@@ -1102,8 +1116,12 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0))
|
||||
..add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null))
|
||||
..add(
|
||||
DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0),
|
||||
)
|
||||
..add(
|
||||
DoubleProperty('cursorHeight', cursorHeight, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Radius>(
|
||||
'cursorRadius',
|
||||
@@ -1118,7 +1136,9 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(ColorProperty('cursorColor', cursorColor, defaultValue: null))
|
||||
..add(
|
||||
ColorProperty('cursorColor', cursorColor, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
ColorProperty('cursorErrorColor', cursorErrorColor, defaultValue: null),
|
||||
)
|
||||
@@ -1208,6 +1228,13 @@ class RichTextField extends StatefulWidget {
|
||||
? const <String>[]
|
||||
: kDefaultContentInsertionMimeTypes,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<List<Locale>?>(
|
||||
'hintLocales',
|
||||
hintLocales,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1215,9 +1242,7 @@ class RichTextField extends StatefulWidget {
|
||||
class RichTextFieldState extends State<RichTextField>
|
||||
with RestorationMixin
|
||||
implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
|
||||
// RestorableRichTextEditingController? _controller;
|
||||
RichTextEditingController get _effectiveController => widget.controller;
|
||||
// widget.controller ?? _controller!.value;
|
||||
|
||||
FocusNode? _focusNode;
|
||||
FocusNode get _effectiveFocusNode =>
|
||||
@@ -1260,13 +1285,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
bool get _hasIntrinsicError =>
|
||||
widget.maxLength != null &&
|
||||
widget.maxLength! > 0 &&
|
||||
(
|
||||
// widget.controller == null
|
||||
// ? !restorePending &&
|
||||
// _effectiveController.value.text.characters.length >
|
||||
// widget.maxLength!
|
||||
// :
|
||||
_effectiveController.value.text.characters.length > widget.maxLength!);
|
||||
_effectiveController.value.text.characters.length > widget.maxLength!;
|
||||
|
||||
bool get _hasError =>
|
||||
widget.decoration?.errorText != null ||
|
||||
@@ -1288,7 +1307,10 @@ class RichTextFieldState extends State<RichTextField>
|
||||
.applyDefaults(themeData.inputDecorationTheme)
|
||||
.copyWith(
|
||||
enabled: _isEnabled,
|
||||
hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
|
||||
hintMaxLines:
|
||||
widget.decoration?.hintMaxLines ??
|
||||
themeData.inputDecorationTheme.hintMaxLines ??
|
||||
widget.maxLines,
|
||||
);
|
||||
|
||||
// No need to build anything if counter or counterText were given directly.
|
||||
@@ -1368,9 +1390,6 @@ class RichTextFieldState extends State<RichTextField>
|
||||
state: this,
|
||||
controller: widget.controller,
|
||||
);
|
||||
// if (widget.controller == null) {
|
||||
// _createLocalController();
|
||||
// }
|
||||
_effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled;
|
||||
_effectiveFocusNode.addListener(_handleFocusChanged);
|
||||
_initStatesController();
|
||||
@@ -1394,13 +1413,6 @@ class RichTextFieldState extends State<RichTextField>
|
||||
@override
|
||||
void didUpdateWidget(RichTextField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// if (widget.controller == null && oldWidget.controller != null) {
|
||||
// _createLocalController(oldWidget.controller!.value);
|
||||
// } else if (widget.controller != null && oldWidget.controller == null) {
|
||||
// unregisterFromRestoration(_controller!);
|
||||
// _controller!.dispose();
|
||||
// _controller = null;
|
||||
// }
|
||||
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
|
||||
@@ -1434,26 +1446,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
}
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
// if (_controller != null) {
|
||||
// _registerController();
|
||||
// }
|
||||
}
|
||||
|
||||
// void _registerController() {
|
||||
// assert(_controller != null);
|
||||
// registerForRestoration(_controller!, 'controller');
|
||||
// }
|
||||
|
||||
// void _createLocalController([TextEditingValue? value]) {
|
||||
// assert(_controller == null);
|
||||
// _controller = value == null
|
||||
// ? RestorableRichTextEditingController()
|
||||
// : RestorableRichTextEditingController.fromValue(value);
|
||||
// if (!restorePending) {
|
||||
// _registerController();
|
||||
// }
|
||||
// }
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {}
|
||||
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
@@ -1462,7 +1455,6 @@ class RichTextFieldState extends State<RichTextField>
|
||||
void dispose() {
|
||||
_effectiveFocusNode.removeListener(_handleFocusChanged);
|
||||
_focusNode?.dispose();
|
||||
// _controller?.dispose();
|
||||
_statesController.removeListener(_handleStatesControllerChange);
|
||||
_internalStatesController?.dispose();
|
||||
super.dispose();
|
||||
@@ -1476,8 +1468,9 @@ class RichTextFieldState extends State<RichTextField>
|
||||
|
||||
bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
|
||||
// When the text field is activated by something that doesn't trigger the
|
||||
// selection overlay, we shouldn't show the handles either.
|
||||
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
|
||||
// selection toolbar, we shouldn't show the handles either.
|
||||
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar ||
|
||||
!_selectionGestureDetectorBuilder.shouldShowSelectionHandles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1570,7 +1563,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
WidgetStatesController? _internalStatesController;
|
||||
|
||||
void _handleStatesControllerChange() {
|
||||
// Force a rebuild to resolve MaterialStateProperty properties.
|
||||
// Force a rebuild to resolve WidgetStateProperty properties.
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -1581,11 +1574,12 @@ class RichTextFieldState extends State<RichTextField>
|
||||
if (widget.statesController == null) {
|
||||
_internalStatesController = WidgetStatesController();
|
||||
}
|
||||
_statesController.update(WidgetState.disabled, !_isEnabled);
|
||||
_statesController.update(WidgetState.hovered, _isHovering);
|
||||
_statesController.update(WidgetState.focused, _effectiveFocusNode.hasFocus);
|
||||
_statesController.update(WidgetState.error, _hasError);
|
||||
_statesController.addListener(_handleStatesControllerChange);
|
||||
_statesController
|
||||
..update(WidgetState.disabled, !_isEnabled)
|
||||
..update(WidgetState.hovered, _isHovering)
|
||||
..update(WidgetState.focused, _effectiveFocusNode.hasFocus)
|
||||
..update(WidgetState.error, _hasError)
|
||||
..addListener(_handleStatesControllerChange);
|
||||
}
|
||||
|
||||
// AutofillClient implementation start.
|
||||
@@ -1716,7 +1710,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
cupertinoTheme.primaryColor;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
cupertinoTheme.primaryColor.withOpacity(0.40);
|
||||
cupertinoTheme.primaryColor.withValues(alpha: 0.40);
|
||||
cursorRadius ??= const Radius.circular(2.0);
|
||||
cursorOffset = Offset(
|
||||
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
|
||||
@@ -1737,7 +1731,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
cupertinoTheme.primaryColor;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
cupertinoTheme.primaryColor.withOpacity(0.40);
|
||||
cupertinoTheme.primaryColor.withValues(alpha: 0.40);
|
||||
cursorRadius ??= const Radius.circular(2.0);
|
||||
cursorOffset = Offset(
|
||||
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
|
||||
@@ -1767,7 +1761,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withOpacity(0.40);
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
|
||||
case TargetPlatform.linux:
|
||||
forcePressEnabled = false;
|
||||
@@ -1781,7 +1775,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withOpacity(0.40);
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
handleDidGainAccessibilityFocus = () {
|
||||
// Automatically activate the TextField when it receives accessibility focus.
|
||||
if (!_effectiveFocusNode.hasFocus &&
|
||||
@@ -1805,7 +1799,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withOpacity(0.40);
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
handleDidGainAccessibilityFocus = () {
|
||||
// Automatically activate the TextField when it receives accessibility focus.
|
||||
if (!_effectiveFocusNode.hasFocus &&
|
||||
@@ -1876,9 +1870,11 @@ class RichTextFieldState extends State<RichTextField>
|
||||
scrollPadding: widget.scrollPadding,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
selectAllOnFocus: widget.selectAllOnFocus,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
autofillHints: widget.autofillHints,
|
||||
autofillClient: this,
|
||||
autocorrectionTextRectColor: autocorrectionTextRectColor,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
@@ -1892,6 +1888,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
magnifierConfiguration:
|
||||
widget.magnifierConfiguration ??
|
||||
TextMagnifier.adaptiveMagnifierConfiguration,
|
||||
hintLocales: widget.hintLocales,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2026,26 +2023,17 @@ TextStyle _m2CounterErrorStyle(BuildContext context) => Theme.of(
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
TextStyle? _m3StateInputStyle(BuildContext context) =>
|
||||
WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return TextStyle(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.color
|
||||
?.withOpacity(0.38));
|
||||
}
|
||||
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color);
|
||||
});
|
||||
TextStyle? _m3StateInputStyle(BuildContext context) => WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withValues(alpha:0.38));
|
||||
}
|
||||
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color);
|
||||
});
|
||||
|
||||
TextStyle _m3InputStyle(BuildContext context) =>
|
||||
Theme.of(context).textTheme.bodyLarge!;
|
||||
TextStyle _m3InputStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;
|
||||
|
||||
TextStyle _m3CounterErrorStyle(BuildContext context) => Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Theme.of(context).colorScheme.error);
|
||||
TextStyle _m3CounterErrorStyle(BuildContext context) =>
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error);
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - TextField
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
// 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/cupertino.dart';
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' show kMinInteractiveDimension;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart' hide EditableText, EditableTextState;
|
||||
|
||||
/// Delegate interface for the [TextSelectionGestureDetectorBuilder].
|
||||
///
|
||||
/// The interface is usually implemented by the [State] of text field
|
||||
/// implementations wrapping [EditableText], so that they can use a
|
||||
/// [TextSelectionGestureDetectorBuilder] to build a
|
||||
/// [TextSelectionGestureDetector] for their [EditableText]. The delegate
|
||||
/// provides the builder with information about the current state of the text
|
||||
/// field. Based on that information, the builder adds the correct gesture
|
||||
/// handlers to the gesture detector.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextField], which implements this delegate for the Material text field.
|
||||
/// * [CupertinoTextField], which implements this delegate for the Cupertino
|
||||
/// text field.
|
||||
abstract class TextSelectionGestureDetectorBuilderDelegate {
|
||||
/// [GlobalKey] to the [EditableText] for which the
|
||||
/// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
|
||||
@@ -83,6 +104,10 @@ class TextSelectionGestureDetectorBuilder {
|
||||
|
||||
// Hides the magnifier on supported platforms, currently only Android and iOS.
|
||||
void _hideMagnifierIfSupportedByPlatform() {
|
||||
if (!_isEditableTextMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
@@ -181,6 +206,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
offset,
|
||||
);
|
||||
final TextSelection selection = renderEditable.selection!;
|
||||
// bggRGjQaUbCoE on select
|
||||
final TextSelection nextSelection = selection.copyWith(
|
||||
extentOffset: controller.tapOffsetSimple(tappedPosition.offset),
|
||||
);
|
||||
@@ -193,12 +219,23 @@ class TextSelectionGestureDetectorBuilder {
|
||||
|
||||
/// Whether to show the selection toolbar.
|
||||
///
|
||||
/// It is based on the signal source when a [onTapDown] is called. This getter
|
||||
/// will return true if current [onTapDown] event is triggered by a touch or
|
||||
/// a stylus.
|
||||
/// It is based on the signal source when [onTapDown], [onSecondaryTapDown],
|
||||
/// [onDragSelectionStart], or [onForcePressStart] is called. This getter
|
||||
/// will return true if the current [onTapDown], or [onDragSelectionStart] event
|
||||
/// is triggered by a touch or a stylus. It will always return true for the
|
||||
/// current [onSecondaryTapDown] or [onForcePressStart] event.
|
||||
bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar;
|
||||
bool _shouldShowSelectionToolbar = true;
|
||||
|
||||
/// Whether to show the selection handles.
|
||||
///
|
||||
/// It is based on the signal source when [onTapDown], [onSecondaryTapDown],
|
||||
/// [onDragSelectionStart], is called. This getter will return true if the
|
||||
/// current [onTapDown], [onSecondaryTapDown], or [onDragSelectionStart] event
|
||||
/// is triggered by a touch or a stylus.
|
||||
bool get shouldShowSelectionHandles => _shouldShowSelectionHandles;
|
||||
bool _shouldShowSelectionHandles = true;
|
||||
|
||||
/// The [State] of the [EditableText] for which the builder will provide a
|
||||
/// [TextSelectionGestureDetector].
|
||||
@protected
|
||||
@@ -209,6 +246,13 @@ class TextSelectionGestureDetectorBuilder {
|
||||
@protected
|
||||
RenderEditable get renderEditable => editableText.renderEditable;
|
||||
|
||||
/// Returns `true` if a widget with the global key [delegate.editableTextKey]
|
||||
/// is in the tree and the widget is mounted.
|
||||
///
|
||||
/// Otherwise returns `false`.
|
||||
bool get _isEditableTextMounted =>
|
||||
delegate.editableTextKey.currentContext?.mounted ?? false;
|
||||
|
||||
/// Whether the Shift key was pressed when the most recent [PointerDownEvent]
|
||||
/// was tracked by the [BaseTapAndDragGestureRecognizer].
|
||||
bool _isShiftPressed = false;
|
||||
@@ -311,6 +355,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
kind == null ||
|
||||
kind == PointerDeviceKind.touch ||
|
||||
kind == PointerDeviceKind.stylus;
|
||||
_shouldShowSelectionHandles = _shouldShowSelectionToolbar;
|
||||
|
||||
// It is impossible to extend the selection when the shift key is pressed, if the
|
||||
// renderEditable.selection is invalid.
|
||||
@@ -504,6 +549,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
// Precise devices should place the cursor at a precise position if the
|
||||
// word at the text position is not misspelled.
|
||||
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
|
||||
editableText.hideToolbar();
|
||||
case PointerDeviceKind.touch:
|
||||
case PointerDeviceKind.unknown:
|
||||
// If the word that was tapped is misspelled, select the word and show the spell check suggestions
|
||||
@@ -719,22 +765,23 @@ class TextSelectionGestureDetectorBuilder {
|
||||
/// callback.
|
||||
@protected
|
||||
void onSingleLongTapEnd(LongPressEndDetails details) {
|
||||
_hideMagnifierIfSupportedByPlatform();
|
||||
_onSingleLongTapEndOrCancel();
|
||||
if (shouldShowSelectionToolbar) {
|
||||
editableText.showToolbar();
|
||||
}
|
||||
_longPressStartedWithoutFocus = false;
|
||||
_dragStartViewportOffset = 0.0;
|
||||
_dragStartScrollOffset = 0.0;
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
delegate.selectionEnabled &&
|
||||
editableText.textEditingValue.selection.isCollapsed) {
|
||||
// Update the floating cursor.
|
||||
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
|
||||
state: FloatingCursorDragState.End,
|
||||
);
|
||||
editableText.updateFloatingCursor(cursorPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for [TextSelectionGestureDetector.onSingleLongTapCancel].
|
||||
///
|
||||
/// By default, it hides the magnifier and the floating cursor if necessary.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextSelectionGestureDetector.onSingleLongTapCancel], which triggers
|
||||
/// this callback.
|
||||
@protected
|
||||
void onSingleLongTapCancel() {
|
||||
_onSingleLongTapEndOrCancel();
|
||||
}
|
||||
|
||||
/// Handler for [TextSelectionGestureDetector.onSecondaryTap].
|
||||
@@ -785,6 +832,10 @@ class TextSelectionGestureDetectorBuilder {
|
||||
TapDownDetails(globalPosition: details.globalPosition),
|
||||
);
|
||||
_shouldShowSelectionToolbar = true;
|
||||
_shouldShowSelectionHandles =
|
||||
details.kind == null ||
|
||||
details.kind == PointerDeviceKind.touch ||
|
||||
details.kind == PointerDeviceKind.stylus;
|
||||
}
|
||||
|
||||
/// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
|
||||
@@ -806,6 +857,23 @@ class TextSelectionGestureDetectorBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
void _onSingleLongTapEndOrCancel() {
|
||||
_hideMagnifierIfSupportedByPlatform();
|
||||
_longPressStartedWithoutFocus = false;
|
||||
_dragStartViewportOffset = 0.0;
|
||||
_dragStartScrollOffset = 0.0;
|
||||
if (_isEditableTextMounted &&
|
||||
defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
delegate.selectionEnabled &&
|
||||
editableText.textEditingValue.selection.isCollapsed) {
|
||||
// Update the floating cursor.
|
||||
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
|
||||
state: FloatingCursorDragState.End,
|
||||
);
|
||||
editableText.updateFloatingCursor(cursorPoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Selects the set of paragraphs in a document that intersect a given range of
|
||||
// global positions.
|
||||
void _selectParagraphsInRange({
|
||||
@@ -954,6 +1022,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
kind == null ||
|
||||
kind == PointerDeviceKind.touch ||
|
||||
kind == PointerDeviceKind.stylus;
|
||||
_shouldShowSelectionHandles = _shouldShowSelectionToolbar;
|
||||
|
||||
_dragStartSelection = renderEditable.selection;
|
||||
_dragStartScrollOffset = _scrollPosition;
|
||||
@@ -1300,6 +1369,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
onSingleLongTapStart: onSingleLongTapStart,
|
||||
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
|
||||
onSingleLongTapEnd: onSingleLongTapEnd,
|
||||
onSingleLongTapCancel: onSingleLongTapCancel,
|
||||
onDoubleTapDown: onDoubleTapDown,
|
||||
onTripleTapDown: onTripleTapDown,
|
||||
onDragSelectionStart: onDragSelectionStart,
|
||||
@@ -1312,6 +1382,149 @@ class TextSelectionGestureDetectorBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// A gesture detector to respond to non-exclusive event chains for a text field.
|
||||
///
|
||||
/// An ordinary [GestureDetector] configured to handle events like tap and
|
||||
/// double tap will only recognize one or the other. This widget detects both:
|
||||
/// the first tap and then any subsequent taps that occurs within a time limit
|
||||
/// after the first.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextField], a Material text field which uses this gesture detector.
|
||||
/// * [CupertinoTextField], a Cupertino text field which uses this gesture
|
||||
/// detector.
|
||||
class TextSelectionGestureDetector extends StatefulWidget {
|
||||
/// Create a [TextSelectionGestureDetector].
|
||||
///
|
||||
/// Multiple callbacks can be called for one sequence of input gesture.
|
||||
const TextSelectionGestureDetector({
|
||||
super.key,
|
||||
this.onTapTrackStart,
|
||||
this.onTapTrackReset,
|
||||
this.onTapDown,
|
||||
this.onForcePressStart,
|
||||
this.onForcePressEnd,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapDown,
|
||||
this.onSingleTapUp,
|
||||
this.onSingleTapCancel,
|
||||
this.onUserTap,
|
||||
this.onSingleLongTapStart,
|
||||
this.onSingleLongTapMoveUpdate,
|
||||
this.onSingleLongTapEnd,
|
||||
this.onSingleLongTapCancel,
|
||||
this.onDoubleTapDown,
|
||||
this.onTripleTapDown,
|
||||
this.onDragSelectionStart,
|
||||
this.onDragSelectionUpdate,
|
||||
this.onDragSelectionEnd,
|
||||
this.onUserTapAlwaysCalled = false,
|
||||
this.behavior,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart}
|
||||
/// Callback used to indicate that a tap tracking has started upon
|
||||
/// a [PointerDownEvent].
|
||||
/// {@endtemplate}
|
||||
final VoidCallback? onTapTrackStart;
|
||||
|
||||
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset}
|
||||
/// Callback used to indicate that a tap tracking has been reset which
|
||||
/// happens on the next [PointerDownEvent] after the timer between two taps
|
||||
/// elapses, the recognizer loses the arena, the gesture is cancelled or
|
||||
/// the recognizer is disposed of.
|
||||
/// {@endtemplate}
|
||||
final VoidCallback? onTapTrackReset;
|
||||
|
||||
/// Called for every tap down including every tap down that's part of a
|
||||
/// double click or a long press, except touches that include enough movement
|
||||
/// to not qualify as taps (e.g. pans and flings).
|
||||
final GestureTapDragDownCallback? onTapDown;
|
||||
|
||||
/// Called when a pointer has tapped down and the force of the pointer has
|
||||
/// just become greater than [ForcePressGestureRecognizer.startPressure].
|
||||
final GestureForcePressStartCallback? onForcePressStart;
|
||||
|
||||
/// Called when a pointer that had previously triggered [onForcePressStart] is
|
||||
/// lifted off the screen.
|
||||
final GestureForcePressEndCallback? onForcePressEnd;
|
||||
|
||||
/// Called for a tap event with the secondary mouse button.
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
|
||||
/// Called for a tap down event with the secondary mouse button.
|
||||
final GestureTapDownCallback? onSecondaryTapDown;
|
||||
|
||||
/// Called for the first tap in a series of taps, consecutive taps do not call
|
||||
/// this method.
|
||||
///
|
||||
/// For example, if the detector was configured with [onTapDown] and
|
||||
/// [onDoubleTapDown], three quick taps would be recognized as a single tap
|
||||
/// down, followed by a tap up, then a double tap down, followed by a single tap down.
|
||||
final GestureTapDragUpCallback? onSingleTapUp;
|
||||
|
||||
/// Called for each touch that becomes recognized as a gesture that is not a
|
||||
/// short tap, such as a long tap or drag. It is called at the moment when
|
||||
/// another gesture from the touch is recognized.
|
||||
final GestureCancelCallback? onSingleTapCancel;
|
||||
|
||||
/// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
|
||||
/// disabled, which is the default behavior.
|
||||
///
|
||||
/// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
|
||||
/// including consecutive taps.
|
||||
final GestureTapCallback? onUserTap;
|
||||
|
||||
/// Called for a single long tap that's sustained for longer than
|
||||
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
|
||||
/// double-tap-hold, which calls [onDoubleTapDown] instead.
|
||||
final GestureLongPressStartCallback? onSingleLongTapStart;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is dragged.
|
||||
final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is lifted.
|
||||
final GestureLongPressEndCallback? onSingleLongTapEnd;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is canceled.
|
||||
final GestureLongPressCancelCallback? onSingleLongTapCancel;
|
||||
|
||||
/// Called after a momentary hold or a short tap that is close in space and
|
||||
/// time (within [kDoubleTapTimeout]) to a previous short tap.
|
||||
final GestureTapDragDownCallback? onDoubleTapDown;
|
||||
|
||||
/// Called after a momentary hold or a short tap that is close in space and
|
||||
/// time (within [kDoubleTapTimeout]) to a previous double-tap.
|
||||
final GestureTapDragDownCallback? onTripleTapDown;
|
||||
|
||||
/// Called when a mouse starts dragging to select text.
|
||||
final GestureTapDragStartCallback? onDragSelectionStart;
|
||||
|
||||
/// Called repeatedly as a mouse moves while dragging.
|
||||
final GestureTapDragUpdateCallback? onDragSelectionUpdate;
|
||||
|
||||
/// Called when a mouse that was previously dragging is released.
|
||||
final GestureTapDragEndCallback? onDragSelectionEnd;
|
||||
|
||||
/// Whether [onUserTap] will be called for all taps including consecutive taps.
|
||||
///
|
||||
/// Defaults to false, so [onUserTap] is only called for each distinct tap.
|
||||
final bool onUserTapAlwaysCalled;
|
||||
|
||||
/// How this gesture detector should behave during hit testing.
|
||||
///
|
||||
/// This defaults to [HitTestBehavior.deferToChild].
|
||||
final HitTestBehavior? behavior;
|
||||
|
||||
/// Child below this widget.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TextSelectionGestureDetectorState();
|
||||
}
|
||||
|
||||
class _TextSelectionGestureDetectorState
|
||||
extends State<TextSelectionGestureDetector> {
|
||||
// Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
|
||||
@@ -1411,21 +1624,19 @@ class _TextSelectionGestureDetectorState
|
||||
}
|
||||
|
||||
void _handleLongPressStart(LongPressStartDetails details) {
|
||||
if (widget.onSingleLongTapStart != null) {
|
||||
widget.onSingleLongTapStart!(details);
|
||||
}
|
||||
widget.onSingleLongTapStart?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
if (widget.onSingleLongTapMoveUpdate != null) {
|
||||
widget.onSingleLongTapMoveUpdate!(details);
|
||||
}
|
||||
widget.onSingleLongTapMoveUpdate?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressEnd(LongPressEndDetails details) {
|
||||
if (widget.onSingleLongTapEnd != null) {
|
||||
widget.onSingleLongTapEnd!(details);
|
||||
}
|
||||
widget.onSingleLongTapEnd?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressCancel() {
|
||||
widget.onSingleLongTapCancel?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1445,7 +1656,8 @@ class _TextSelectionGestureDetectorState
|
||||
|
||||
if (widget.onSingleLongTapStart != null ||
|
||||
widget.onSingleLongTapMoveUpdate != null ||
|
||||
widget.onSingleLongTapEnd != null) {
|
||||
widget.onSingleLongTapEnd != null ||
|
||||
widget.onSingleLongTapCancel != null) {
|
||||
gestures[LongPressGestureRecognizer] =
|
||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(
|
||||
@@ -1456,7 +1668,8 @@ class _TextSelectionGestureDetectorState
|
||||
instance
|
||||
..onLongPressStart = _handleLongPressStart
|
||||
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
|
||||
..onLongPressEnd = _handleLongPressEnd;
|
||||
..onLongPressEnd = _handleLongPressEnd
|
||||
..onLongPressCancel = _handleLongPressCancel;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1824,8 +2037,11 @@ class TextSelectionOverlay {
|
||||
/// specifically is visible.
|
||||
bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible;
|
||||
|
||||
/// Whether the magnifier is currently visible.
|
||||
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
|
||||
/// {@macro flutter.widgets.SelectionOverlay.magnifierIsVisible}
|
||||
bool get magnifierIsVisible => _selectionOverlay.magnifierIsVisible;
|
||||
|
||||
/// {@macro flutter.widgets.SelectionOverlay.magnifierExists}
|
||||
bool get magnifierExists => _selectionOverlay.magnifierExists;
|
||||
|
||||
/// Whether the spell check menu is currently visible.
|
||||
///
|
||||
@@ -1965,6 +2181,16 @@ class TextSelectionOverlay {
|
||||
late double _endHandleDragTarget;
|
||||
|
||||
// The initial selection when a selection handle drag has started.
|
||||
//
|
||||
// This is used on Apple platforms to:
|
||||
//
|
||||
// 1. Preserve a collapsed selection: if the selection was collapsed when the drag
|
||||
// began, then it should remain collapsed throughout the entire drag.
|
||||
// 2. Anchor the non-dragged end of a non-collapsed selection: On Apple platforms,
|
||||
// the dragged handle always defines the selection's new extent. The drag start
|
||||
// selection provides the original position for the selection's new base. This
|
||||
// allows the selection handles to correctly swap their logical order (invert)
|
||||
// during the drag.
|
||||
TextSelection? _dragStartSelection;
|
||||
|
||||
void _handleSelectionEndHandleDragStart(DragStartDetails details) {
|
||||
@@ -1990,7 +2216,12 @@ class TextSelectionOverlay {
|
||||
final TextPosition position = renderObject.getPositionForPoint(
|
||||
Offset(details.globalPosition.dx, centerOfLineGlobal),
|
||||
);
|
||||
_dragStartSelection ??= _selection;
|
||||
|
||||
// The drag start selection is only utilized on Apple platforms.
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
_dragStartSelection ??= _selection;
|
||||
}
|
||||
|
||||
_selectionOverlay.showMagnifier(
|
||||
_buildMagnifier(
|
||||
@@ -2031,7 +2262,6 @@ class TextSelectionOverlay {
|
||||
if (!renderObject.attached) {
|
||||
return;
|
||||
}
|
||||
assert(_dragStartSelection != null);
|
||||
|
||||
// This is NOT the same as details.localPosition. That is relative to the
|
||||
// selection handle, whereas this is relative to the RenderEditable.
|
||||
@@ -2059,27 +2289,27 @@ class TextSelectionOverlay {
|
||||
// bggRGjQaUbCoE right drag
|
||||
position = controller.dragOffset(position);
|
||||
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
final TextSelection newSelection;
|
||||
switch (defaultTargetPlatform) {
|
||||
// On Apple platforms, dragging the base handle makes it the extent.
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
assert(_dragStartSelection != null);
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
// Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized
|
||||
// always returns true for a TextSelection.
|
||||
final bool dragStartSelectionNormalized =
|
||||
@@ -2095,6 +2325,21 @@ class TextSelectionOverlay {
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
if (_selection.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
newSelection = TextSelection(
|
||||
baseOffset: _selection.baseOffset,
|
||||
extentOffset: position.offset,
|
||||
@@ -2146,7 +2391,12 @@ class TextSelectionOverlay {
|
||||
final TextPosition position = renderObject.getPositionForPoint(
|
||||
Offset(details.globalPosition.dx, centerOfLineGlobal),
|
||||
);
|
||||
_dragStartSelection ??= _selection;
|
||||
|
||||
// The drag start selection is only utilized on Apple platforms.
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
_dragStartSelection ??= _selection;
|
||||
}
|
||||
|
||||
_selectionOverlay.showMagnifier(
|
||||
_buildMagnifier(
|
||||
@@ -2161,7 +2411,6 @@ class TextSelectionOverlay {
|
||||
if (!renderObject.attached) {
|
||||
return;
|
||||
}
|
||||
assert(_dragStartSelection != null);
|
||||
|
||||
// This is NOT the same as details.localPosition. That is relative to the
|
||||
// selection handle, whereas this is relative to the RenderEditable.
|
||||
@@ -2186,27 +2435,27 @@ class TextSelectionOverlay {
|
||||
// bggRGjQaUbCoE single drag, left drag
|
||||
position = controller.dragOffset(position);
|
||||
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
final TextSelection newSelection;
|
||||
switch (defaultTargetPlatform) {
|
||||
// On Apple platforms, dragging the base handle makes it the extent.
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
assert(_dragStartSelection != null);
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
// Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized
|
||||
// always returns true for a TextSelection.
|
||||
final bool dragStartSelectionNormalized =
|
||||
@@ -2222,6 +2471,21 @@ class TextSelectionOverlay {
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
if (_selection.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
newSelection = TextSelection(
|
||||
baseOffset: position.offset,
|
||||
extentOffset: _selection.extentOffset,
|
||||
@@ -2250,19 +2514,26 @@ class TextSelectionOverlay {
|
||||
return;
|
||||
}
|
||||
_dragStartSelection = null;
|
||||
final bool draggingHandles =
|
||||
_selectionOverlay.isDraggingStartHandle ||
|
||||
_selectionOverlay.isDraggingEndHandle;
|
||||
if (selectionControls is! TextSelectionHandleControls) {
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar();
|
||||
if (!draggingHandles) {
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar(
|
||||
context: context,
|
||||
contextMenuBuilder: contextMenuBuilder,
|
||||
);
|
||||
if (!draggingHandles) {
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar(
|
||||
context: context,
|
||||
contextMenuBuilder: contextMenuBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2375,6 +2646,19 @@ class SelectionOverlay {
|
||||
: _toolbar != null || _spellCheckToolbarController.isShown;
|
||||
}
|
||||
|
||||
/// {@template flutter.widgets.SelectionOverlay.magnifierIsVisible}
|
||||
/// Whether the magnifier is currently visible.
|
||||
/// {@endtemplate}
|
||||
bool get magnifierIsVisible => _magnifierController.shown;
|
||||
|
||||
/// {@template flutter.widgets.SelectionOverlay.magnifierExists}
|
||||
/// Whether the magnifier currently exists.
|
||||
///
|
||||
/// This differs from [magnifierIsVisible] in that the magnifier may exist
|
||||
/// in the overlay, but not be shown.
|
||||
/// {@endtemplate}
|
||||
bool get magnifierExists => _magnifierController.overlayEntry != null;
|
||||
|
||||
/// {@template flutter.widgets.SelectionOverlay.showMagnifier}
|
||||
/// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier]
|
||||
/// was called. This is safe to call on platforms not mobile, since
|
||||
@@ -2386,6 +2670,10 @@ class SelectionOverlay {
|
||||
/// [MagnifierController.shown].
|
||||
/// {@endtemplate}
|
||||
void showMagnifier(MagnifierInfo initialMagnifierInfo) {
|
||||
// Do not show the magnifier if one already exists.
|
||||
if (_magnifierController.overlayEntry != null) {
|
||||
return;
|
||||
}
|
||||
if (toolbarIsVisible) {
|
||||
hideToolbar();
|
||||
}
|
||||
@@ -2459,8 +2747,26 @@ class SelectionOverlay {
|
||||
markNeedsBuild();
|
||||
}
|
||||
|
||||
// Whether a drag is in progress on the start handle. This differs from
|
||||
// `_isDraggingStartHandle` in that it is not blocked by `_canDragStartHandle`.
|
||||
bool _startHandleDragInProgress = false;
|
||||
|
||||
/// Whether the selection start handle is currently being dragged.
|
||||
bool get isDraggingStartHandle =>
|
||||
_isDraggingStartHandle || _startHandleDragInProgress;
|
||||
bool _isDraggingStartHandle = false;
|
||||
|
||||
// Whether the start handle can be dragged.
|
||||
//
|
||||
// On Apple and web platforms only one selection handle can be dragged
|
||||
// at a time, so when the end handle is being dragged on these platforms
|
||||
// the the start handle cannot be dragged.
|
||||
bool get _canDragStartHandle =>
|
||||
!_isDraggingEndHandle ||
|
||||
(defaultTargetPlatform != TargetPlatform.iOS &&
|
||||
defaultTargetPlatform != TargetPlatform.macOS &&
|
||||
!kIsWeb);
|
||||
|
||||
/// Whether the start handle is visible.
|
||||
///
|
||||
/// If the value changes, the start handle uses [FadeTransition] to transition
|
||||
@@ -2480,6 +2786,10 @@ class SelectionOverlay {
|
||||
_isDraggingStartHandle = false;
|
||||
return;
|
||||
}
|
||||
_startHandleDragInProgress = true;
|
||||
if (!_canDragStartHandle) {
|
||||
return;
|
||||
}
|
||||
_isDraggingStartHandle = details.kind == PointerDeviceKind.touch;
|
||||
onStartHandleDragStart?.call(details);
|
||||
}
|
||||
@@ -2491,6 +2801,22 @@ class SelectionOverlay {
|
||||
_isDraggingStartHandle = false;
|
||||
return;
|
||||
}
|
||||
if (!_canDragStartHandle) {
|
||||
return;
|
||||
}
|
||||
// The handle drag may have been blocked before on Apple platforms and the web
|
||||
// while the opposite handle was being dragged. Ensure that any logic that was
|
||||
// meant to be run in onStartHandleDragStart is still run.
|
||||
if (!_isDraggingStartHandle) {
|
||||
_isDraggingStartHandle = details.kind == PointerDeviceKind.touch;
|
||||
final DragStartDetails startDetails = DragStartDetails(
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
sourceTimeStamp: details.sourceTimeStamp,
|
||||
kind: details.kind,
|
||||
);
|
||||
onStartHandleDragStart?.call(startDetails);
|
||||
}
|
||||
onStartHandleDragUpdate?.call(details);
|
||||
}
|
||||
|
||||
@@ -2508,6 +2834,10 @@ class SelectionOverlay {
|
||||
if (_handles == null) {
|
||||
return;
|
||||
}
|
||||
_startHandleDragInProgress = false;
|
||||
if (!_canDragStartHandle) {
|
||||
return;
|
||||
}
|
||||
onStartHandleDragEnd?.call(details);
|
||||
}
|
||||
|
||||
@@ -2539,8 +2869,26 @@ class SelectionOverlay {
|
||||
markNeedsBuild();
|
||||
}
|
||||
|
||||
// Whether a drag is in progress on the start handle. This differs from
|
||||
// `_isDraggingEndHandle` in that it is not blocked by `_canDragEndHandle`.
|
||||
bool _endHandleDragInProgress = false;
|
||||
|
||||
/// Whether the selection end handle is currently being dragged.
|
||||
bool get isDraggingEndHandle =>
|
||||
_isDraggingEndHandle || _endHandleDragInProgress;
|
||||
bool _isDraggingEndHandle = false;
|
||||
|
||||
// Whether the end handle can be dragged.
|
||||
//
|
||||
// On Apple and web platforms only one selection handle can be dragged
|
||||
// at a time, so when the start handle is being dragged on these platforms
|
||||
// the the end handle cannot be dragged.
|
||||
bool get _canDragEndHandle =>
|
||||
!_isDraggingStartHandle ||
|
||||
(defaultTargetPlatform != TargetPlatform.iOS &&
|
||||
defaultTargetPlatform != TargetPlatform.macOS &&
|
||||
!kIsWeb);
|
||||
|
||||
/// Whether the end handle is visible.
|
||||
///
|
||||
/// If the value changes, the end handle uses [FadeTransition] to transition
|
||||
@@ -2560,6 +2908,10 @@ class SelectionOverlay {
|
||||
_isDraggingEndHandle = false;
|
||||
return;
|
||||
}
|
||||
_endHandleDragInProgress = true;
|
||||
if (!_canDragEndHandle) {
|
||||
return;
|
||||
}
|
||||
_isDraggingEndHandle = details.kind == PointerDeviceKind.touch;
|
||||
onEndHandleDragStart?.call(details);
|
||||
}
|
||||
@@ -2571,6 +2923,22 @@ class SelectionOverlay {
|
||||
_isDraggingEndHandle = false;
|
||||
return;
|
||||
}
|
||||
if (!_canDragEndHandle) {
|
||||
return;
|
||||
}
|
||||
// The handle drag may have been blocked before on Apple platforms and the web
|
||||
// while the opposite handle was being dragged. Ensure that any logic that was
|
||||
// meant to be run in onStartHandleDragStart is still run.
|
||||
if (!_isDraggingEndHandle) {
|
||||
_isDraggingEndHandle = details.kind == PointerDeviceKind.touch;
|
||||
final DragStartDetails startDetails = DragStartDetails(
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
sourceTimeStamp: details.sourceTimeStamp,
|
||||
kind: details.kind,
|
||||
);
|
||||
onEndHandleDragStart?.call(startDetails);
|
||||
}
|
||||
onEndHandleDragUpdate?.call(details);
|
||||
}
|
||||
|
||||
@@ -2588,6 +2956,10 @@ class SelectionOverlay {
|
||||
if (_handles == null) {
|
||||
return;
|
||||
}
|
||||
_endHandleDragInProgress = false;
|
||||
if (!_canDragEndHandle) {
|
||||
return;
|
||||
}
|
||||
onEndHandleDragEnd?.call(details);
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
characters:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
|
||||
@@ -238,6 +238,7 @@ dependencies:
|
||||
flutter_cache_manager: any
|
||||
http2: any
|
||||
screen_retriever: any
|
||||
characters: any
|
||||
|
||||
dependency_overrides:
|
||||
# screen_brightness: ^2.1.
|
||||
|
||||
Reference in New Issue
Block a user