mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Flutter 3.29 beta (#2571)
This commit is contained in:
@@ -47,36 +47,40 @@ class AppStateWidget extends StatefulWidget {
|
||||
class AppStateWidgetState extends State<AppStateWidget> {
|
||||
AppState _data = AppState(
|
||||
replacementsController: ReplacementTextEditingController(
|
||||
text: 'The quick brown fox jumps over the lazy dog.'),
|
||||
text: 'The quick brown fox jumps over the lazy dog.',
|
||||
),
|
||||
textEditingDeltaHistory: <TextEditingDelta>[],
|
||||
toggleButtonsState: <ToggleButtonsState>{},
|
||||
);
|
||||
|
||||
void updateTextEditingDeltaHistory(List<TextEditingDelta> textEditingDeltas) {
|
||||
_data = _data.copyWith(textEditingDeltaHistory: <TextEditingDelta>[
|
||||
..._data.textEditingDeltaHistory,
|
||||
...textEditingDeltas
|
||||
]);
|
||||
_data = _data.copyWith(
|
||||
textEditingDeltaHistory: <TextEditingDelta>[
|
||||
..._data.textEditingDeltaHistory,
|
||||
...textEditingDeltas,
|
||||
],
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void updateToggleButtonsStateOnSelectionChanged(
|
||||
TextSelection selection, ReplacementTextEditingController controller) {
|
||||
TextSelection selection,
|
||||
ReplacementTextEditingController controller,
|
||||
) {
|
||||
// When the selection changes we want to check the replacements at the new
|
||||
// selection. Enable/disable toggle buttons based on the replacements found
|
||||
// at the new selection.
|
||||
final List<TextStyle> replacementStyles =
|
||||
controller.getReplacementsAtSelection(selection);
|
||||
final List<TextStyle> replacementStyles = controller
|
||||
.getReplacementsAtSelection(selection);
|
||||
final Set<ToggleButtonsState> hasChanged = {};
|
||||
|
||||
if (replacementStyles.isEmpty) {
|
||||
_data = _data.copyWith(
|
||||
toggleButtonsState: Set.from(_data.toggleButtonsState)
|
||||
..removeAll({
|
||||
ToggleButtonsState.bold,
|
||||
ToggleButtonsState.italic,
|
||||
ToggleButtonsState.underline,
|
||||
}),
|
||||
toggleButtonsState: Set.from(_data.toggleButtonsState)..removeAll({
|
||||
ToggleButtonsState.bold,
|
||||
ToggleButtonsState.italic,
|
||||
ToggleButtonsState.underline,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,7 +189,9 @@ class AppStateWidgetState extends State<AppStateWidget> {
|
||||
} else {
|
||||
controller.disableExpand(attributeMap[index]!);
|
||||
controller.removeReplacementsAtRange(
|
||||
replacementRange, attributeMap[index]);
|
||||
replacementRange,
|
||||
attributeMap[index],
|
||||
);
|
||||
_data = _data.copyWith(replacementsController: controller);
|
||||
setState(() {});
|
||||
}
|
||||
@@ -193,9 +199,6 @@ class AppStateWidgetState extends State<AppStateWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppStateManager(
|
||||
state: _data,
|
||||
child: widget.child,
|
||||
);
|
||||
return AppStateManager(state: _data, child: widget.child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,9 @@ class _BasicTextFieldState extends State<BasicTextField> {
|
||||
}
|
||||
|
||||
void _handleSelectionChanged(
|
||||
TextSelection selection, SelectionChangedCause? cause) {
|
||||
TextSelection selection,
|
||||
SelectionChangedCause? cause,
|
||||
) {
|
||||
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
||||
if (willShowSelectionHandles != _showSelectionHandles) {
|
||||
setState(() {
|
||||
@@ -96,9 +98,16 @@ class _BasicTextFieldState extends State<BasicTextField> {
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
final Offset startOffset = _renderEditable.maxLines == 1
|
||||
? Offset(_renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
|
||||
: Offset(0.0, _renderEditable.offset.pixels - _dragStartViewportOffset);
|
||||
final Offset startOffset =
|
||||
_renderEditable.maxLines == 1
|
||||
? Offset(
|
||||
_renderEditable.offset.pixels - _dragStartViewportOffset,
|
||||
0.0,
|
||||
)
|
||||
: Offset(
|
||||
0.0,
|
||||
_renderEditable.offset.pixels - _dragStartViewportOffset,
|
||||
);
|
||||
|
||||
_renderEditable.selectPositionAt(
|
||||
from: _startDetails.globalPosition - startOffset,
|
||||
@@ -145,8 +154,9 @@ class _BasicTextFieldState extends State<BasicTextField> {
|
||||
onPanUpdate: (dragUpdateDetails) => _onDragUpdate(dragUpdateDetails),
|
||||
onSecondaryTapDown: (secondaryTapDownDetails) {
|
||||
_renderEditable.selectWordsInRange(
|
||||
from: secondaryTapDownDetails.globalPosition,
|
||||
cause: SelectionChangedCause.tap);
|
||||
from: secondaryTapDownDetails.globalPosition,
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
_renderEditable.handleSecondaryTapDown(secondaryTapDownDetails);
|
||||
_textInputClient!.hideToolbar();
|
||||
_textInputClient!.showToolbar();
|
||||
@@ -172,19 +182,20 @@ class _BasicTextFieldState extends State<BasicTextField> {
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
_renderEditable.selectWordsInRange(
|
||||
from: longPressMoveUpdateDetails.globalPosition -
|
||||
from:
|
||||
longPressMoveUpdateDetails.globalPosition -
|
||||
longPressMoveUpdateDetails.offsetFromOrigin,
|
||||
to: longPressMoveUpdateDetails.globalPosition,
|
||||
cause: SelectionChangedCause.longPress,
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPressEnd: (longPressEndDetails) =>
|
||||
_textInputClient!.showToolbar(),
|
||||
onHorizontalDragStart: (dragStartDetails) =>
|
||||
_onDragStart(dragStartDetails),
|
||||
onHorizontalDragUpdate: (dragUpdateDetails) =>
|
||||
_onDragUpdate(dragUpdateDetails),
|
||||
onLongPressEnd:
|
||||
(longPressEndDetails) => _textInputClient!.showToolbar(),
|
||||
onHorizontalDragStart:
|
||||
(dragStartDetails) => _onDragStart(dragStartDetails),
|
||||
onHorizontalDragUpdate:
|
||||
(dragUpdateDetails) => _onDragUpdate(dragUpdateDetails),
|
||||
child: SizedBox(
|
||||
height: double.infinity,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
|
||||
@@ -12,24 +12,25 @@ import 'replacements.dart';
|
||||
|
||||
/// Signature for the callback that reports when the user changes the selection
|
||||
/// (including the cursor location).
|
||||
typedef SelectionChangedCallback = void Function(
|
||||
TextSelection selection, SelectionChangedCause? cause);
|
||||
typedef SelectionChangedCallback =
|
||||
void Function(TextSelection selection, SelectionChangedCause? cause);
|
||||
|
||||
/// Signature for a widget builder that builds a context menu for the given
|
||||
/// editable field.
|
||||
typedef BasicTextFieldContextMenuBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
ClipboardStatus clipboardStatus,
|
||||
VoidCallback? onCopy,
|
||||
VoidCallback? onCut,
|
||||
VoidCallback? onPaste,
|
||||
VoidCallback? onSelectAll,
|
||||
VoidCallback? onLookUp,
|
||||
VoidCallback? onLiveTextInput,
|
||||
VoidCallback? onSearchWeb,
|
||||
VoidCallback? onShare,
|
||||
TextSelectionToolbarAnchors anchors,
|
||||
);
|
||||
typedef BasicTextFieldContextMenuBuilder =
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
ClipboardStatus clipboardStatus,
|
||||
VoidCallback? onCopy,
|
||||
VoidCallback? onCut,
|
||||
VoidCallback? onPaste,
|
||||
VoidCallback? onSelectAll,
|
||||
VoidCallback? onLookUp,
|
||||
VoidCallback? onLiveTextInput,
|
||||
VoidCallback? onSearchWeb,
|
||||
VoidCallback? onShare,
|
||||
TextSelectionToolbarAnchors anchors,
|
||||
);
|
||||
|
||||
/// A basic text input client. An implementation of [DeltaTextInputClient] meant to
|
||||
/// send/receive information from the framework to the platform's text input plugin
|
||||
@@ -112,7 +113,9 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
|
||||
@override
|
||||
void didChangeInputControl(
|
||||
TextInputControl? oldControl, TextInputControl? newControl) {
|
||||
TextInputControl? oldControl,
|
||||
TextInputControl? newControl,
|
||||
) {
|
||||
if (_hasFocus && _hasInputConnection) {
|
||||
oldControl?.hide();
|
||||
newControl?.show();
|
||||
@@ -172,7 +175,9 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValue(TextEditingValue value) {/* Not using */}
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
/* Not using */
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
|
||||
@@ -193,7 +198,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
|
||||
final bool selectionChanged =
|
||||
_value.selection.start != value.selection.start ||
|
||||
_value.selection.end != value.selection.end;
|
||||
_value.selection.end != value.selection.end;
|
||||
manager.updateTextEditingDeltaHistory(textEditingDeltas);
|
||||
|
||||
_value = value;
|
||||
@@ -206,8 +211,10 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
}
|
||||
|
||||
if (selectionChanged) {
|
||||
manager.updateToggleButtonsStateOnSelectionChanged(value.selection,
|
||||
widget.controller as ReplacementTextEditingController);
|
||||
manager.updateToggleButtonsStateOnSelectionChanged(
|
||||
value.selection,
|
||||
widget.controller as ReplacementTextEditingController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,11 +309,14 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
if (_hasFocus) {
|
||||
if (!_value.selection.isValid) {
|
||||
// Place cursor at the end if the selection is invalid when we receive focus.
|
||||
final TextSelection validSelection =
|
||||
TextSelection.collapsed(offset: _value.text.length);
|
||||
final TextSelection validSelection = TextSelection.collapsed(
|
||||
offset: _value.text.length,
|
||||
);
|
||||
_handleSelectionChanged(validSelection, null);
|
||||
manager.updateToggleButtonsStateOnSelectionChanged(validSelection,
|
||||
widget.controller as ReplacementTextEditingController);
|
||||
manager.updateToggleButtonsStateOnSelectionChanged(
|
||||
validSelection,
|
||||
widget.controller as ReplacementTextEditingController,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,32 +341,38 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
// These actions have yet to be implemented for this sample.
|
||||
static final Map<Type, Action<Intent>> _unsupportedActions =
|
||||
<Type, Action<Intent>>{
|
||||
DeleteToNextWordBoundaryIntent: DoNothingAction(consumesKey: false),
|
||||
DeleteToLineBreakIntent: DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionToNextWordBoundaryIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionToLineBreakIntent: DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionVerticallyToAdjacentLineIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionVerticallyToAdjacentPageIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionToNextParagraphBoundaryIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionToDocumentBoundaryIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionByPageIntent: DoNothingAction(consumesKey: false),
|
||||
ExpandSelectionToDocumentBoundaryIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExpandSelectionToLineBreakIntent: DoNothingAction(consumesKey: false),
|
||||
ScrollToDocumentBoundaryIntent: DoNothingAction(consumesKey: false),
|
||||
RedoTextIntent: DoNothingAction(consumesKey: false),
|
||||
ReplaceTextIntent: DoNothingAction(consumesKey: false),
|
||||
UndoTextIntent: DoNothingAction(consumesKey: false),
|
||||
UpdateSelectionIntent: DoNothingAction(consumesKey: false),
|
||||
TransposeCharactersIntent: DoNothingAction(consumesKey: false),
|
||||
};
|
||||
DeleteToNextWordBoundaryIntent: DoNothingAction(consumesKey: false),
|
||||
DeleteToLineBreakIntent: DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionToNextWordBoundaryIntent: DoNothingAction(
|
||||
consumesKey: false,
|
||||
),
|
||||
ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent:
|
||||
DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionToLineBreakIntent: DoNothingAction(consumesKey: false),
|
||||
ExtendSelectionVerticallyToAdjacentLineIntent: DoNothingAction(
|
||||
consumesKey: false,
|
||||
),
|
||||
ExtendSelectionVerticallyToAdjacentPageIntent: DoNothingAction(
|
||||
consumesKey: false,
|
||||
),
|
||||
ExtendSelectionToNextParagraphBoundaryIntent: DoNothingAction(
|
||||
consumesKey: false,
|
||||
),
|
||||
ExtendSelectionToDocumentBoundaryIntent: DoNothingAction(
|
||||
consumesKey: false,
|
||||
),
|
||||
ExtendSelectionByPageIntent: DoNothingAction(consumesKey: false),
|
||||
ExpandSelectionToDocumentBoundaryIntent: DoNothingAction(
|
||||
consumesKey: false,
|
||||
),
|
||||
ExpandSelectionToLineBreakIntent: DoNothingAction(consumesKey: false),
|
||||
ScrollToDocumentBoundaryIntent: DoNothingAction(consumesKey: false),
|
||||
RedoTextIntent: DoNothingAction(consumesKey: false),
|
||||
ReplaceTextIntent: DoNothingAction(consumesKey: false),
|
||||
UndoTextIntent: DoNothingAction(consumesKey: false),
|
||||
UpdateSelectionIntent: DoNothingAction(consumesKey: false),
|
||||
TransposeCharactersIntent: DoNothingAction(consumesKey: false),
|
||||
};
|
||||
|
||||
/// Keyboard text editing actions.
|
||||
// The Handling of the default text editing shortcuts with deltas
|
||||
@@ -372,9 +388,10 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
),
|
||||
ExtendSelectionByCharacterIntent:
|
||||
CallbackAction<ExtendSelectionByCharacterIntent>(
|
||||
onInvoke: (intent) =>
|
||||
_extendSelection(intent.forward, intent.collapseSelection),
|
||||
),
|
||||
onInvoke:
|
||||
(intent) =>
|
||||
_extendSelection(intent.forward, intent.collapseSelection),
|
||||
),
|
||||
SelectAllTextIntent: CallbackAction<SelectAllTextIntent>(
|
||||
onInvoke: (intent) => selectAll(intent.cause),
|
||||
),
|
||||
@@ -384,9 +401,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
PasteTextIntent: CallbackAction<PasteTextIntent>(
|
||||
onInvoke: (intent) => pasteText(intent.cause),
|
||||
),
|
||||
DoNothingAndStopPropagationTextIntent: DoNothingAction(
|
||||
consumesKey: false,
|
||||
),
|
||||
DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false),
|
||||
..._unsupportedActions,
|
||||
};
|
||||
|
||||
@@ -447,22 +462,24 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
_selection.isNormalized ? _selection.start : _selection.end;
|
||||
final int lastOffset =
|
||||
_selection.isNormalized ? _selection.end : _selection.start;
|
||||
selection =
|
||||
TextSelection.collapsed(offset: forward ? lastOffset : firstOffset);
|
||||
selection = TextSelection.collapsed(
|
||||
offset: forward ? lastOffset : firstOffset,
|
||||
);
|
||||
} else {
|
||||
if (forward && _selection.baseOffset == _value.text.length) return;
|
||||
if (!forward && _selection.baseOffset == 0) return;
|
||||
final int adjustment = forward
|
||||
? _value.text
|
||||
.substring(_selection.baseOffset)
|
||||
.characters
|
||||
.first
|
||||
.length
|
||||
: -_value.text
|
||||
.substring(0, _selection.baseOffset)
|
||||
.characters
|
||||
.last
|
||||
.length;
|
||||
final int adjustment =
|
||||
forward
|
||||
? _value.text
|
||||
.substring(_selection.baseOffset)
|
||||
.characters
|
||||
.first
|
||||
.length
|
||||
: -_value.text
|
||||
.substring(0, _selection.baseOffset)
|
||||
.characters
|
||||
.last
|
||||
.length;
|
||||
selection = TextSelection.collapsed(
|
||||
offset: _selection.baseOffset + adjustment,
|
||||
);
|
||||
@@ -470,13 +487,18 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
} else {
|
||||
if (forward && _selection.extentOffset == _value.text.length) return;
|
||||
if (!forward && _selection.extentOffset == 0) return;
|
||||
final int adjustment = forward
|
||||
? _value.text.substring(_selection.baseOffset).characters.first.length
|
||||
: -_value.text
|
||||
.substring(0, _selection.baseOffset)
|
||||
.characters
|
||||
.last
|
||||
.length;
|
||||
final int adjustment =
|
||||
forward
|
||||
? _value.text
|
||||
.substring(_selection.baseOffset)
|
||||
.characters
|
||||
.first
|
||||
.length
|
||||
: -_value.text
|
||||
.substring(0, _selection.baseOffset)
|
||||
.characters
|
||||
.last
|
||||
.length;
|
||||
selection = TextSelection(
|
||||
baseOffset: _selection.baseOffset,
|
||||
extentOffset: _selection.extentOffset + adjustment,
|
||||
@@ -494,7 +516,9 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
}
|
||||
|
||||
void _userUpdateTextEditingValueWithDelta(
|
||||
TextEditingDelta textEditingDelta, SelectionChangedCause cause) {
|
||||
TextEditingDelta textEditingDelta,
|
||||
SelectionChangedCause cause,
|
||||
) {
|
||||
TextEditingValue value = _value;
|
||||
|
||||
value = textEditingDelta.apply(value);
|
||||
@@ -538,14 +562,16 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
void _updateComposingRectIfNeeded() {
|
||||
final TextRange composingRange = _value.composing;
|
||||
assert(mounted);
|
||||
Rect? composingRect =
|
||||
renderEditable.getRectForComposingRange(composingRange);
|
||||
Rect? composingRect = renderEditable.getRectForComposingRange(
|
||||
composingRange,
|
||||
);
|
||||
// Send the caret location instead if there's no marked text yet.
|
||||
if (composingRect == null) {
|
||||
assert(!composingRange.isValid || composingRange.isCollapsed);
|
||||
final int offset = composingRange.isValid ? composingRange.start : 0;
|
||||
composingRect =
|
||||
renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
|
||||
composingRect = renderEditable.getLocalRectForCaret(
|
||||
TextPosition(offset: offset),
|
||||
);
|
||||
}
|
||||
_textInputConnection!.setComposingRect(composingRect);
|
||||
}
|
||||
@@ -555,10 +581,12 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
if (selection == null || !selection.isValid || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
final TextPosition currentTextPosition =
|
||||
TextPosition(offset: selection.baseOffset);
|
||||
final Rect caretRect =
|
||||
renderEditable.getLocalRectForCaret(currentTextPosition);
|
||||
final TextPosition currentTextPosition = TextPosition(
|
||||
offset: selection.baseOffset,
|
||||
);
|
||||
final Rect caretRect = renderEditable.getLocalRectForCaret(
|
||||
currentTextPosition,
|
||||
);
|
||||
_textInputConnection!.setCaretRect(caretRect);
|
||||
}
|
||||
|
||||
@@ -574,8 +602,9 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
}
|
||||
_updateComposingRectIfNeeded();
|
||||
_updateCaretRectIfNeeded();
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback(_schedulePeriodicPostFrameCallbacks);
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
_schedulePeriodicPostFrameCallbacks,
|
||||
);
|
||||
}
|
||||
|
||||
/// [TextSelectionDelegate] method implementations.
|
||||
@@ -620,7 +649,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
TextEditingDeltaNonTextUpdate(
|
||||
oldText: textEditingValue.text,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: textEditingValue.selection.end),
|
||||
offset: textEditingValue.selection.end,
|
||||
),
|
||||
composing: TextRange.empty,
|
||||
),
|
||||
cause,
|
||||
@@ -638,8 +668,10 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
|
||||
if (cutRange.isCollapsed) return;
|
||||
Clipboard.setData(ClipboardData(text: cutRange.textInside(text)));
|
||||
final int lastSelectionIndex =
|
||||
math.min(cutRange.baseOffset, cutRange.extentOffset);
|
||||
final int lastSelectionIndex = math.min(
|
||||
cutRange.baseOffset,
|
||||
cutRange.extentOffset,
|
||||
);
|
||||
_userUpdateTextEditingValueWithDelta(
|
||||
TextEditingDeltaReplacement(
|
||||
oldText: textEditingValue.text,
|
||||
@@ -665,7 +697,9 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
// After the paste, the cursor should be collapsed and located after the
|
||||
// pasted content.
|
||||
final int lastSelectionIndex = math.max(
|
||||
pasteRange.baseOffset, pasteRange.baseOffset + data.text!.length);
|
||||
pasteRange.baseOffset,
|
||||
pasteRange.baseOffset + data.text!.length,
|
||||
);
|
||||
|
||||
_userUpdateTextEditingValueWithDelta(
|
||||
TextEditingDeltaReplacement(
|
||||
@@ -683,8 +717,10 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
|
||||
@override
|
||||
void selectAll(SelectionChangedCause cause) {
|
||||
final TextSelection newSelection = _value.selection
|
||||
.copyWith(baseOffset: 0, extentOffset: _value.text.length);
|
||||
final TextSelection newSelection = _value.selection.copyWith(
|
||||
baseOffset: 0,
|
||||
extentOffset: _value.text.length,
|
||||
);
|
||||
_userUpdateTextEditingValueWithDelta(
|
||||
TextEditingDeltaNonTextUpdate(
|
||||
oldText: textEditingValue.text,
|
||||
@@ -712,7 +748,9 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
|
||||
@override
|
||||
void userUpdateTextEditingValue(
|
||||
TextEditingValue value, SelectionChangedCause cause) {
|
||||
TextEditingValue value,
|
||||
SelectionChangedCause cause,
|
||||
) {
|
||||
if (value == _value) return;
|
||||
|
||||
final bool selectionChanged = _value.selection != value.selection;
|
||||
@@ -727,10 +765,10 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
if (selectionChanged && !textChanged) {
|
||||
final TextEditingDeltaNonTextUpdate selectionUpdate =
|
||||
TextEditingDeltaNonTextUpdate(
|
||||
oldText: value.text,
|
||||
selection: value.selection,
|
||||
composing: value.composing,
|
||||
);
|
||||
oldText: value.text,
|
||||
selection: value.selection,
|
||||
composing: value.composing,
|
||||
);
|
||||
if (widget.controller is ReplacementTextEditingController) {
|
||||
(widget.controller as ReplacementTextEditingController)
|
||||
.syncReplacementRanges(selectionUpdate);
|
||||
@@ -741,7 +779,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
|
||||
final bool selectionRangeChanged =
|
||||
_value.selection.start != value.selection.start ||
|
||||
_value.selection.end != value.selection.end;
|
||||
_value.selection.end != value.selection.end;
|
||||
|
||||
_value = value;
|
||||
|
||||
@@ -749,8 +787,10 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
_handleSelectionChanged(_value.selection, cause);
|
||||
|
||||
if (selectionRangeChanged) {
|
||||
manager.updateToggleButtonsStateOnSelectionChanged(_value.selection,
|
||||
widget.controller as ReplacementTextEditingController);
|
||||
manager.updateToggleButtonsStateOnSelectionChanged(
|
||||
_value.selection,
|
||||
widget.controller as ReplacementTextEditingController,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -776,7 +816,9 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
_textKey.currentContext!.findRenderObject()! as RenderEditable;
|
||||
|
||||
void _handleSelectionChanged(
|
||||
TextSelection selection, SelectionChangedCause? cause) {
|
||||
TextSelection selection,
|
||||
SelectionChangedCause? cause,
|
||||
) {
|
||||
// We return early if the selection is not valid. This can happen when the
|
||||
// text of the editable is updated at the same time as the selection is
|
||||
// changed by a gesture event.
|
||||
@@ -820,13 +862,16 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
try {
|
||||
widget.onSelectionChanged.call(selection, cause);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'widgets',
|
||||
context:
|
||||
ErrorDescription('while calling onSelectionChanged for $cause'),
|
||||
));
|
||||
FlutterError.reportError(
|
||||
FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'widgets',
|
||||
context: ErrorDescription(
|
||||
'while calling onSelectionChanged for $cause',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,40 +891,41 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
onSelectionHandleTapped: () {
|
||||
_toggleToolbar();
|
||||
},
|
||||
contextMenuBuilder: widget.contextMenuBuilder == null || kIsWeb
|
||||
? null
|
||||
: (context) {
|
||||
return widget.contextMenuBuilder!(
|
||||
context,
|
||||
_clipboardStatus!.value,
|
||||
copyEnabled
|
||||
? () => copySelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
cutEnabled
|
||||
? () => cutSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
pasteEnabled
|
||||
? () => pasteText(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
selectAllEnabled
|
||||
? () => selectAll(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
lookUpEnabled
|
||||
? () => _lookUpSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
liveTextInputEnabled
|
||||
? () => _startLiveTextInput(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
searchWebEnabled
|
||||
? () =>
|
||||
_searchWebForSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
shareEnabled
|
||||
? () => _shareSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
_contextMenuAnchors,
|
||||
);
|
||||
},
|
||||
contextMenuBuilder:
|
||||
widget.contextMenuBuilder == null || kIsWeb
|
||||
? null
|
||||
: (context) {
|
||||
return widget.contextMenuBuilder!(
|
||||
context,
|
||||
_clipboardStatus!.value,
|
||||
copyEnabled
|
||||
? () => copySelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
cutEnabled
|
||||
? () => cutSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
pasteEnabled
|
||||
? () => pasteText(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
selectAllEnabled
|
||||
? () => selectAll(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
lookUpEnabled
|
||||
? () => _lookUpSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
liveTextInputEnabled
|
||||
? () => _startLiveTextInput(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
searchWebEnabled
|
||||
? () =>
|
||||
_searchWebForSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
shareEnabled
|
||||
? () => _shareSelection(SelectionChangedCause.toolbar)
|
||||
: null,
|
||||
_contextMenuAnchors,
|
||||
);
|
||||
},
|
||||
magnifierConfiguration: TextMagnifierConfiguration.disabled,
|
||||
);
|
||||
|
||||
@@ -935,18 +981,20 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
final String selectedGraphemes = selection.textInside(currText);
|
||||
final int firstSelectedGraphemeExtent =
|
||||
selectedGraphemes.characters.first.length;
|
||||
final Rect? startCharacterRect =
|
||||
renderEditable.getRectForComposingRange(TextRange(
|
||||
start: selection.start,
|
||||
end: selection.start + firstSelectedGraphemeExtent,
|
||||
));
|
||||
final Rect? startCharacterRect = renderEditable.getRectForComposingRange(
|
||||
TextRange(
|
||||
start: selection.start,
|
||||
end: selection.start + firstSelectedGraphemeExtent,
|
||||
),
|
||||
);
|
||||
final int lastSelectedGraphemeExtent =
|
||||
selectedGraphemes.characters.last.length;
|
||||
final Rect? endCharacterRect =
|
||||
renderEditable.getRectForComposingRange(TextRange(
|
||||
start: selection.end - lastSelectedGraphemeExtent,
|
||||
end: selection.end,
|
||||
));
|
||||
final Rect? endCharacterRect = renderEditable.getRectForComposingRange(
|
||||
TextRange(
|
||||
start: selection.end - lastSelectedGraphemeExtent,
|
||||
end: selection.end,
|
||||
),
|
||||
);
|
||||
return _GlyphHeights(
|
||||
start: startCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
||||
end: endCharacterRect?.height ?? renderEditable.preferredLineHeight,
|
||||
@@ -963,8 +1011,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
|
||||
final _GlyphHeights glyphHeights = _getGlyphHeights();
|
||||
final TextSelection selection = textEditingValue.selection;
|
||||
final List<TextSelectionPoint> points =
|
||||
renderEditable.getEndpointsForSelection(selection);
|
||||
final List<TextSelectionPoint> points = renderEditable
|
||||
.getEndpointsForSelection(selection);
|
||||
return TextSelectionToolbarAnchors.fromSelection(
|
||||
renderBox: renderEditable,
|
||||
startGlyphHeight: glyphHeights.start,
|
||||
@@ -1015,15 +1063,13 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
/// Currently this is only implemented for iOS.
|
||||
/// Throws an error if the selection is empty or collapsed.
|
||||
Future<void> _lookUpSelection(SelectionChangedCause cause) async {
|
||||
final String text =
|
||||
textEditingValue.selection.textInside(textEditingValue.text);
|
||||
final String text = textEditingValue.selection.textInside(
|
||||
textEditingValue.text,
|
||||
);
|
||||
if (text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await SystemChannels.platform.invokeMethod(
|
||||
'LookUp.invoke',
|
||||
text,
|
||||
);
|
||||
await SystemChannels.platform.invokeMethod('LookUp.invoke', text);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1042,13 +1088,11 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
///
|
||||
/// Currently this is only implemented for iOS.
|
||||
Future<void> _searchWebForSelection(SelectionChangedCause cause) async {
|
||||
final String text =
|
||||
textEditingValue.selection.textInside(textEditingValue.text);
|
||||
final String text = textEditingValue.selection.textInside(
|
||||
textEditingValue.text,
|
||||
);
|
||||
if (text.isNotEmpty) {
|
||||
await SystemChannels.platform.invokeMethod(
|
||||
'SearchWeb.invoke',
|
||||
text,
|
||||
);
|
||||
await SystemChannels.platform.invokeMethod('SearchWeb.invoke', text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1075,13 +1119,11 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
|
||||
///
|
||||
/// Currently this is only implemented for iOS and Android.
|
||||
Future<void> _shareSelection(SelectionChangedCause cause) async {
|
||||
final String text =
|
||||
textEditingValue.selection.textInside(textEditingValue.text);
|
||||
final String text = textEditingValue.selection.textInside(
|
||||
textEditingValue.text,
|
||||
);
|
||||
if (text.isNotEmpty) {
|
||||
await SystemChannels.platform.invokeMethod(
|
||||
'Share.invoke',
|
||||
text,
|
||||
);
|
||||
await SystemChannels.platform.invokeMethod('Share.invoke', text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1322,10 +1364,7 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
/// The start and end glyph heights of some range of text.
|
||||
@immutable
|
||||
class _GlyphHeights {
|
||||
const _GlyphHeights({
|
||||
required this.start,
|
||||
required this.end,
|
||||
});
|
||||
const _GlyphHeights({required this.start, required this.end});
|
||||
|
||||
/// The glyph height of the first line.
|
||||
final double start;
|
||||
|
||||
@@ -4,11 +4,7 @@ import 'app_state.dart';
|
||||
import 'app_state_manager.dart';
|
||||
|
||||
/// The toggle buttons that can be selected.
|
||||
enum ToggleButtonsState {
|
||||
bold,
|
||||
italic,
|
||||
underline,
|
||||
}
|
||||
enum ToggleButtonsState { bold, italic, underline }
|
||||
|
||||
class FormattingToolbar extends StatelessWidget {
|
||||
const FormattingToolbar({super.key});
|
||||
@@ -25,15 +21,20 @@ class FormattingToolbar extends StatelessWidget {
|
||||
ToggleButtons(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
||||
isSelected: [
|
||||
manager.appState.toggleButtonsState
|
||||
.contains(ToggleButtonsState.bold),
|
||||
manager.appState.toggleButtonsState
|
||||
.contains(ToggleButtonsState.italic),
|
||||
manager.appState.toggleButtonsState
|
||||
.contains(ToggleButtonsState.underline),
|
||||
manager.appState.toggleButtonsState.contains(
|
||||
ToggleButtonsState.bold,
|
||||
),
|
||||
manager.appState.toggleButtonsState.contains(
|
||||
ToggleButtonsState.italic,
|
||||
),
|
||||
manager.appState.toggleButtonsState.contains(
|
||||
ToggleButtonsState.underline,
|
||||
),
|
||||
],
|
||||
onPressed: (index) => AppStateWidget.of(context)
|
||||
.updateToggleButtonsStateOnButtonPressed(index),
|
||||
onPressed:
|
||||
(index) => AppStateWidget.of(
|
||||
context,
|
||||
).updateToggleButtonsStateOnButtonPressed(index),
|
||||
children: const [
|
||||
Icon(Icons.format_bold),
|
||||
Icon(Icons.format_italic),
|
||||
|
||||
@@ -20,9 +20,7 @@ class MyApp extends StatelessWidget {
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Simplistic Editor',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
theme: ThemeData(primarySwatch: Colors.blue),
|
||||
home: const MyHomePage(title: 'Simplistic Editor'),
|
||||
),
|
||||
);
|
||||
@@ -56,7 +54,9 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
|
||||
static Route<Object?> _aboutDialogBuilder(
|
||||
BuildContext context, Object? arguments) {
|
||||
BuildContext context,
|
||||
Object? arguments,
|
||||
) {
|
||||
const String aboutContent =
|
||||
'TextEditingDeltas are a new feature in the latest Flutter stable release that give the user'
|
||||
' finer grain control over the changes that occur during text input. There are four types of'
|
||||
@@ -67,10 +67,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
' more powerful rich text editing applications such as this small example. This feature is supported on all platforms.';
|
||||
return DialogRoute<void>(
|
||||
context: context,
|
||||
builder: (context) => const AlertDialog(
|
||||
title: Center(child: Text('About')),
|
||||
content: Text(aboutContent),
|
||||
),
|
||||
builder:
|
||||
(context) => const AlertDialog(
|
||||
title: Center(child: Text('About')),
|
||||
content: Text(aboutContent),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,17 +100,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 35.0),
|
||||
child: BasicTextField(
|
||||
controller: _replacementTextEditingController,
|
||||
style: const TextStyle(
|
||||
fontSize: 18.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: const TextStyle(fontSize: 18.0, color: Colors.black),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(
|
||||
child: TextEditingDeltaHistoryView(),
|
||||
),
|
||||
const Expanded(child: TextEditingDeltaHistoryView()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -70,18 +70,12 @@ class TextEditingInlineSpanReplacement {
|
||||
range.end > deletedRange.start) &&
|
||||
range.end <= deletedRange.end) {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: deletedRange.start,
|
||||
),
|
||||
range: TextRange(start: range.start, end: deletedRange.start),
|
||||
);
|
||||
} else if (range.start < deletedRange.start &&
|
||||
range.end > deletedRange.end) {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: range.end - deletedLength,
|
||||
),
|
||||
range: TextRange(start: range.start, end: range.end - deletedLength),
|
||||
);
|
||||
} else if (range.start >= deletedRange.start &&
|
||||
range.end <= deletedRange.end) {
|
||||
@@ -96,46 +90,29 @@ class TextEditingInlineSpanReplacement {
|
||||
);
|
||||
} else if (range.end <= deletedRange.start &&
|
||||
range.end < deletedRange.end) {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
),
|
||||
);
|
||||
return copy(range: TextRange(start: range.start, end: range.end));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
TextEditingInlineSpanReplacement? onInsertion(
|
||||
TextEditingDeltaInsertion delta) {
|
||||
TextEditingDeltaInsertion delta,
|
||||
) {
|
||||
final int insertionOffset = delta.insertionOffset;
|
||||
final int insertedLength = delta.textInserted.length;
|
||||
|
||||
if (range.end == insertionOffset) {
|
||||
if (expand) {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: range.end + insertedLength,
|
||||
),
|
||||
range: TextRange(start: range.start, end: range.end + insertedLength),
|
||||
);
|
||||
} else {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
),
|
||||
);
|
||||
return copy(range: TextRange(start: range.start, end: range.end));
|
||||
}
|
||||
}
|
||||
if (range.start < insertionOffset && range.end < insertionOffset) {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
),
|
||||
);
|
||||
return copy(range: TextRange(start: range.start, end: range.end));
|
||||
} else if (range.start >= insertionOffset && range.end > insertionOffset) {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
@@ -145,10 +122,7 @@ class TextEditingInlineSpanReplacement {
|
||||
);
|
||||
} else if (range.start < insertionOffset && range.end > insertionOffset) {
|
||||
return copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: range.end + insertedLength,
|
||||
),
|
||||
range: TextRange(start: range.start, end: range.end + insertedLength),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,7 +130,8 @@ class TextEditingInlineSpanReplacement {
|
||||
}
|
||||
|
||||
List<TextEditingInlineSpanReplacement>? onReplacement(
|
||||
TextEditingDeltaReplacement delta) {
|
||||
TextEditingDeltaReplacement delta,
|
||||
) {
|
||||
final TextRange replacedRange = delta.replacedRange;
|
||||
final bool replacementShortenedText =
|
||||
delta.replacementText.length < delta.textReplaced.length;
|
||||
@@ -164,9 +139,10 @@ class TextEditingInlineSpanReplacement {
|
||||
delta.replacementText.length > delta.textReplaced.length;
|
||||
final bool replacementEqualLength =
|
||||
delta.replacementText.length == delta.textReplaced.length;
|
||||
final int changedOffset = replacementShortenedText
|
||||
? delta.textReplaced.length - delta.replacementText.length
|
||||
: delta.replacementText.length - delta.textReplaced.length;
|
||||
final int changedOffset =
|
||||
replacementShortenedText
|
||||
? delta.textReplaced.length - delta.replacementText.length
|
||||
: delta.replacementText.length - delta.textReplaced.length;
|
||||
|
||||
if (range.start >= replacedRange.start &&
|
||||
(range.start < replacedRange.end && range.end > replacedRange.end)) {
|
||||
@@ -190,35 +166,20 @@ class TextEditingInlineSpanReplacement {
|
||||
];
|
||||
} else if (replacementEqualLength) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: replacedRange.end,
|
||||
end: range.end,
|
||||
),
|
||||
),
|
||||
copy(range: TextRange(start: replacedRange.end, end: range.end)),
|
||||
];
|
||||
}
|
||||
} else if ((range.start < replacedRange.start &&
|
||||
range.end > replacedRange.start) &&
|
||||
range.end <= replacedRange.end) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: replacedRange.start,
|
||||
),
|
||||
),
|
||||
copy(range: TextRange(start: range.start, end: replacedRange.start)),
|
||||
];
|
||||
} else if (range.start < replacedRange.start &&
|
||||
range.end > replacedRange.end) {
|
||||
if (replacementShortenedText) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: replacedRange.start,
|
||||
),
|
||||
),
|
||||
copy(range: TextRange(start: range.start, end: replacedRange.start)),
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: replacedRange.end - changedOffset,
|
||||
@@ -228,12 +189,7 @@ class TextEditingInlineSpanReplacement {
|
||||
];
|
||||
} else if (replacementLengthenedText) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: replacedRange.start,
|
||||
),
|
||||
),
|
||||
copy(range: TextRange(start: range.start, end: replacedRange.start)),
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: replacedRange.end + changedOffset,
|
||||
@@ -243,18 +199,8 @@ class TextEditingInlineSpanReplacement {
|
||||
];
|
||||
} else if (replacementEqualLength) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: replacedRange.start,
|
||||
),
|
||||
),
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: replacedRange.end,
|
||||
end: range.end,
|
||||
),
|
||||
),
|
||||
copy(range: TextRange(start: range.start, end: replacedRange.start)),
|
||||
copy(range: TextRange(start: replacedRange.end, end: range.end)),
|
||||
];
|
||||
}
|
||||
} else if (range.start >= replacedRange.start &&
|
||||
@@ -286,21 +232,15 @@ class TextEditingInlineSpanReplacement {
|
||||
}
|
||||
} else if (range.end <= replacedRange.start &&
|
||||
range.end < replacedRange.end) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
),
|
||||
),
|
||||
];
|
||||
return [copy(range: TextRange(start: range.start, end: range.end))];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
TextEditingInlineSpanReplacement? onNonTextUpdate(
|
||||
TextEditingDeltaNonTextUpdate delta) {
|
||||
TextEditingDeltaNonTextUpdate delta,
|
||||
) {
|
||||
if (range.isCollapsed) {
|
||||
if (range.start != delta.selection.start &&
|
||||
range.end != delta.selection.end) {
|
||||
@@ -313,41 +253,21 @@ class TextEditingInlineSpanReplacement {
|
||||
List<TextEditingInlineSpanReplacement>? removeRange(TextRange removalRange) {
|
||||
if (range.start >= removalRange.start &&
|
||||
(range.start < removalRange.end && range.end > removalRange.end)) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: removalRange.end,
|
||||
end: range.end,
|
||||
),
|
||||
),
|
||||
];
|
||||
return [copy(range: TextRange(start: removalRange.end, end: range.end))];
|
||||
} else if ((range.start < removalRange.start &&
|
||||
range.end > removalRange.start) &&
|
||||
range.end <= removalRange.end) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: removalRange.start,
|
||||
),
|
||||
),
|
||||
copy(range: TextRange(start: range.start, end: removalRange.start)),
|
||||
];
|
||||
} else if (range.start < removalRange.start &&
|
||||
range.end > removalRange.end) {
|
||||
return [
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: range.start,
|
||||
end: removalRange.start,
|
||||
),
|
||||
range: TextRange(start: range.start, end: removalRange.start),
|
||||
expand: removalRange.isCollapsed ? false : expand,
|
||||
),
|
||||
copy(
|
||||
range: TextRange(
|
||||
start: removalRange.end,
|
||||
end: range.end,
|
||||
),
|
||||
),
|
||||
copy(range: TextRange(start: removalRange.end, end: range.end)),
|
||||
];
|
||||
} else if (range.start >= removalRange.start &&
|
||||
range.end <= removalRange.end) {
|
||||
@@ -369,7 +289,10 @@ class TextEditingInlineSpanReplacement {
|
||||
/// is updated to the specified value.
|
||||
TextEditingInlineSpanReplacement copy({TextRange? range, bool? expand}) {
|
||||
return TextEditingInlineSpanReplacement(
|
||||
range ?? this.range, generator, expand ?? this.expand);
|
||||
range ?? this.range,
|
||||
generator,
|
||||
expand ?? this.expand,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -397,10 +320,11 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
/// Creates a controller for an editable text field from an initial [TextEditingValue].
|
||||
///
|
||||
/// This constructor treats a null [value] argument as if it were [TextEditingValue.empty].
|
||||
ReplacementTextEditingController.fromValue(super.value,
|
||||
{List<TextEditingInlineSpanReplacement>? replacements,
|
||||
this.composingRegionReplaceable = true})
|
||||
: super.fromValue();
|
||||
ReplacementTextEditingController.fromValue(
|
||||
super.value, {
|
||||
List<TextEditingInlineSpanReplacement>? replacements,
|
||||
this.composingRegionReplaceable = true,
|
||||
}) : super.fromValue();
|
||||
|
||||
/// The [TextEditingInlineSpanReplacement]s that are evaluated on the editing value.
|
||||
///
|
||||
@@ -528,9 +452,9 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
TextStyle? style,
|
||||
required bool withComposing,
|
||||
}) {
|
||||
assert(!value.composing.isValid ||
|
||||
!withComposing ||
|
||||
value.isComposingRangeValid);
|
||||
assert(
|
||||
!value.composing.isValid || !withComposing || value.isComposingRangeValid,
|
||||
);
|
||||
|
||||
// Keep a mapping of TextRanges to the InlineSpan to replace it with.
|
||||
final Map<TextRange, InlineSpan> rangeSpanMapping =
|
||||
@@ -558,15 +482,20 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
if (composingRegionReplaceable &&
|
||||
value.isComposingRangeValid &&
|
||||
withComposing) {
|
||||
_addToMappingWithOverlaps((value, range) {
|
||||
final TextStyle composingStyle = style != null
|
||||
? style.merge(const TextStyle(decoration: TextDecoration.underline))
|
||||
: const TextStyle(decoration: TextDecoration.underline);
|
||||
return TextSpan(
|
||||
style: composingStyle,
|
||||
text: value,
|
||||
);
|
||||
}, value.composing, rangeSpanMapping, value.text);
|
||||
_addToMappingWithOverlaps(
|
||||
(value, range) {
|
||||
final TextStyle composingStyle =
|
||||
style != null
|
||||
? style.merge(
|
||||
const TextStyle(decoration: TextDecoration.underline),
|
||||
)
|
||||
: const TextStyle(decoration: TextDecoration.underline);
|
||||
return TextSpan(style: composingStyle, text: value);
|
||||
},
|
||||
value.composing,
|
||||
rangeSpanMapping,
|
||||
value.text,
|
||||
);
|
||||
}
|
||||
|
||||
// Sort the matches by start index. Since no overlapping exists, this is safe.
|
||||
@@ -579,28 +508,30 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
int previousEndIndex = 0;
|
||||
for (final TextRange range in sortedRanges) {
|
||||
if (range.start > previousEndIndex) {
|
||||
spans.add(TextSpan(
|
||||
text: value.text.substring(previousEndIndex, range.start)));
|
||||
spans.add(
|
||||
TextSpan(text: value.text.substring(previousEndIndex, range.start)),
|
||||
);
|
||||
}
|
||||
spans.add(rangeSpanMapping[range]!);
|
||||
previousEndIndex = range.end;
|
||||
}
|
||||
// Add any trailing text as a regular TextSpan.
|
||||
if (previousEndIndex < value.text.length) {
|
||||
spans.add(TextSpan(
|
||||
text: value.text.substring(previousEndIndex, value.text.length)));
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: value.text.substring(previousEndIndex, value.text.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: spans,
|
||||
);
|
||||
return TextSpan(style: style, children: spans);
|
||||
}
|
||||
|
||||
static void _addToMappingWithOverlaps(
|
||||
InlineSpanGenerator generator,
|
||||
TextRange matchedRange,
|
||||
Map<TextRange, InlineSpan> rangeSpanMapping,
|
||||
String text) {
|
||||
InlineSpanGenerator generator,
|
||||
TextRange matchedRange,
|
||||
Map<TextRange, InlineSpan> rangeSpanMapping,
|
||||
String text,
|
||||
) {
|
||||
// In some cases we should allow for overlap.
|
||||
// For example in the case of two TextSpans matching the same range for replacement,
|
||||
// we should try to merge the styles into one TextStyle and build a new TextSpan.
|
||||
@@ -620,14 +551,14 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
overlappingTriples.add(<dynamic>[
|
||||
matchedRange.start,
|
||||
matchedRange.end,
|
||||
generator(matchedRange.textInside(text), matchedRange).style
|
||||
generator(matchedRange.textInside(text), matchedRange).style,
|
||||
]);
|
||||
|
||||
for (final TextRange overlappingRange in overlapRanges) {
|
||||
overlappingTriples.add(<dynamic>[
|
||||
overlappingRange.start,
|
||||
overlappingRange.end,
|
||||
rangeSpanMapping[overlappingRange]!.style
|
||||
rangeSpanMapping[overlappingRange]!.style,
|
||||
]);
|
||||
rangeSpanMapping.remove(overlappingRange);
|
||||
}
|
||||
@@ -643,8 +574,10 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
if (math.max(tripleA[0] as int, tripleB[0] as int) <=
|
||||
math.min(tripleA[1] as int, tripleB[1] as int) &&
|
||||
tripleA[2] == tripleB[2]) {
|
||||
toRemoveRangesThatHaveBeenMerged
|
||||
.addAll(<dynamic>[tripleA, tripleB]);
|
||||
toRemoveRangesThatHaveBeenMerged.addAll(<dynamic>[
|
||||
tripleA,
|
||||
tripleB,
|
||||
]);
|
||||
tripleA = <dynamic>[
|
||||
math.min(tripleA[0] as int, tripleB[0] as int),
|
||||
math.max(tripleA[1] as int, tripleB[1] as int),
|
||||
@@ -697,8 +630,10 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
styles = styles.difference(end[endPoints[i]]!);
|
||||
styles.addAll(start[endPoints[i]]!);
|
||||
TextStyle? mergedStyles;
|
||||
final TextRange uniqueRange =
|
||||
TextRange(start: endPoints[i], end: otherEndPoints[i]);
|
||||
final TextRange uniqueRange = TextRange(
|
||||
start: endPoints[i],
|
||||
end: otherEndPoints[i],
|
||||
);
|
||||
for (final TextStyle style in styles) {
|
||||
if (mergedStyles == null) {
|
||||
mergedStyles = style;
|
||||
@@ -706,14 +641,18 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
mergedStyles = mergedStyles.merge(style);
|
||||
}
|
||||
}
|
||||
rangeSpanMapping[uniqueRange] =
|
||||
TextSpan(text: uniqueRange.textInside(text), style: mergedStyles);
|
||||
rangeSpanMapping[uniqueRange] = TextSpan(
|
||||
text: uniqueRange.textInside(text),
|
||||
style: mergedStyles,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!overlap) {
|
||||
rangeSpanMapping[matchedRange] =
|
||||
generator(matchedRange.textInside(text), matchedRange);
|
||||
rangeSpanMapping[matchedRange] = generator(
|
||||
matchedRange.textInside(text),
|
||||
matchedRange,
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up collapsed ranges that we don't need to style.
|
||||
@@ -734,9 +673,10 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
|
||||
for (final TextEditingInlineSpanReplacement replacement in replacements!) {
|
||||
if (replacement.range.end == selection.start) {
|
||||
TextStyle? replacementStyle = (replacement.generator(
|
||||
'', const TextRange.collapsed(0)) as TextSpan)
|
||||
.style;
|
||||
TextStyle? replacementStyle =
|
||||
(replacement.generator('', const TextRange.collapsed(0))
|
||||
as TextSpan)
|
||||
.style;
|
||||
if (replacementStyle! == style) {
|
||||
toRemove.add(replacement);
|
||||
toAdd.add(replacement.copy(expand: false));
|
||||
@@ -771,12 +711,14 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
if (selection.end != replacement.range.start) {
|
||||
if (selection.start == replacement.range.end) {
|
||||
if (replacement.expand) {
|
||||
stylesAtSelection
|
||||
.add(replacement.generator('', replacement.range).style!);
|
||||
stylesAtSelection.add(
|
||||
replacement.generator('', replacement.range).style!,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
stylesAtSelection
|
||||
.add(replacement.generator('', replacement.range).style!);
|
||||
stylesAtSelection.add(
|
||||
replacement.generator('', replacement.range).style!,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -785,8 +727,9 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
math.min(replacement.range.end, selection.end)) {
|
||||
if (replacement.range.start <= selection.start &&
|
||||
replacement.range.end >= selection.end) {
|
||||
stylesAtSelection
|
||||
.add(replacement.generator('', replacement.range).style!);
|
||||
stylesAtSelection.add(
|
||||
replacement.generator('', replacement.range).style!,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -801,8 +744,10 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
|
||||
for (int i = 0; i < replacements!.length; i++) {
|
||||
TextEditingInlineSpanReplacement replacement = replacements![i];
|
||||
InlineSpan replacementSpan =
|
||||
replacement.generator('', const TextRange.collapsed(0));
|
||||
InlineSpan replacementSpan = replacement.generator(
|
||||
'',
|
||||
const TextRange.collapsed(0),
|
||||
);
|
||||
TextStyle? replacementStyle = replacementSpan.style;
|
||||
late final TextEditingInlineSpanReplacement? mutatedReplacement;
|
||||
|
||||
@@ -810,8 +755,8 @@ class ReplacementTextEditingController extends TextEditingController {
|
||||
math.min(replacement.range.end, removalRange.end)) &&
|
||||
replacementStyle != null) {
|
||||
if (replacementStyle == attribute!) {
|
||||
List<TextEditingInlineSpanReplacement>? newReplacements =
|
||||
replacement.removeRange(removalRange);
|
||||
List<TextEditingInlineSpanReplacement>? newReplacements = replacement
|
||||
.removeRange(removalRange);
|
||||
|
||||
if (newReplacements != null) {
|
||||
if (newReplacements.length == 1) {
|
||||
|
||||
@@ -7,7 +7,8 @@ class TextEditingDeltaHistoryView extends StatelessWidget {
|
||||
const TextEditingDeltaHistoryView({super.key});
|
||||
|
||||
List<Widget> _buildTextEditingDeltaHistoryViews(
|
||||
List<TextEditingDelta> textEditingDeltas) {
|
||||
List<TextEditingDelta> textEditingDeltas,
|
||||
) {
|
||||
List<Widget> textEditingDeltaViews = [];
|
||||
|
||||
for (final TextEditingDelta delta in textEditingDeltas) {
|
||||
@@ -68,7 +69,8 @@ class TextEditingDeltaHistoryView extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: 'The type of text input that is occurring.'
|
||||
message:
|
||||
'The type of text input that is occurring.'
|
||||
' Check out the documentation for TextEditingDelta for more information.',
|
||||
child: _buildTextEditingDeltaViewHeading('Delta Type'),
|
||||
),
|
||||
@@ -127,7 +129,8 @@ class TextEditingDeltaHistoryView extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 35.0),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildTextEditingDeltaHistoryViews(
|
||||
manager.appState.textEditingDeltaHistory)[index];
|
||||
manager.appState.textEditingDeltaHistory,
|
||||
)[index];
|
||||
},
|
||||
itemCount: manager.appState.textEditingDeltaHistory.length,
|
||||
separatorBuilder: (context, index) {
|
||||
|
||||
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.0
|
||||
sdk: ^3.7.0-0
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
|
||||
@@ -18,11 +18,17 @@ void main() {
|
||||
|
||||
// Elements on Style ToggleButton Toolbar.
|
||||
expect(
|
||||
find.widgetWithIcon(ToggleButtons, Icons.format_bold), findsOneWidget);
|
||||
expect(find.widgetWithIcon(ToggleButtons, Icons.format_italic),
|
||||
findsOneWidget);
|
||||
expect(find.widgetWithIcon(ToggleButtons, Icons.format_underline),
|
||||
findsOneWidget);
|
||||
find.widgetWithIcon(ToggleButtons, Icons.format_bold),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.widgetWithIcon(ToggleButtons, Icons.format_italic),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.widgetWithIcon(ToggleButtons, Icons.format_underline),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Elements on the main screen
|
||||
// Delta labels.
|
||||
@@ -35,24 +41,34 @@ void main() {
|
||||
// Selection delta is generated and delta history is visible.
|
||||
await tester.tap(find.byType(BasicTextInputClient));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(TextEditingDeltaView, "NonTextUpdate"),
|
||||
findsOneWidget);
|
||||
expect(
|
||||
find.widgetWithText(TextEditingDeltaView, "NonTextUpdate"),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Find tooltips.
|
||||
expect(find.byTooltip('The text that is being inserted or deleted'),
|
||||
findsOneWidget);
|
||||
expect(
|
||||
find.byTooltip(
|
||||
'The type of text input that is occurring. Check out the documentation for TextEditingDelta for more information.'),
|
||||
findsOneWidget);
|
||||
find.byTooltip('The text that is being inserted or deleted'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byTooltip(
|
||||
'The offset in the text where the text input is occurring.'),
|
||||
findsOneWidget);
|
||||
find.byTooltip(
|
||||
'The type of text input that is occurring. Check out the documentation for TextEditingDelta for more information.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byTooltip(
|
||||
'The new text selection range after the text input has occurred.'),
|
||||
findsOneWidget);
|
||||
find.byTooltip(
|
||||
'The offset in the text where the text input is occurring.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byTooltip(
|
||||
'The new text selection range after the text input has occurred.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// About Dialog
|
||||
expect(find.widgetWithIcon(IconButton, Icons.info_outline), findsOneWidget);
|
||||
|
||||
Reference in New Issue
Block a user