1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 13:58:47 +00:00
This commit is contained in:
Brett Morgan
2022-05-11 12:48:11 -07:00
committed by GitHub
parent fb00d0a102
commit ccd68f34e2
242 changed files with 1719 additions and 1430 deletions

View File

@@ -22,8 +22,10 @@ class BasicTextField extends StatefulWidget {
}
class _BasicTextFieldState extends State<BasicTextField> {
final GlobalKey<BasicTextInputClientState> textInputClientKey = GlobalKey<BasicTextInputClientState>();
BasicTextInputClientState? get _textInputClient => textInputClientKey.currentState;
final GlobalKey<BasicTextInputClientState> textInputClientKey =
GlobalKey<BasicTextInputClientState>();
BasicTextInputClientState? get _textInputClient =>
textInputClientKey.currentState;
RenderEditable get _renderEditable => _textInputClient!.renderEditable;
// For text selection gestures.
@@ -42,8 +44,8 @@ class _BasicTextFieldState extends State<BasicTextField> {
return false;
}
if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.scribble) {
if (cause == SelectionChangedCause.longPress ||
cause == SelectionChangedCause.scribble) {
return true;
}
@@ -54,7 +56,8 @@ class _BasicTextFieldState extends State<BasicTextField> {
return false;
}
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
void _handleSelectionChanged(
TextSelection selection, SelectionChangedCause? cause) {
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
if (willShowSelectionHandles != _showSelectionHandles) {
setState(() {
@@ -128,16 +131,20 @@ class _BasicTextFieldState extends State<BasicTextField> {
case TargetPlatform.linux:
case TargetPlatform.windows:
_renderEditable.selectWordsInRange(
from: longPressMoveUpdateDetails.globalPosition - longPressMoveUpdateDetails.offsetFromOrigin,
from: longPressMoveUpdateDetails.globalPosition -
longPressMoveUpdateDetails.offsetFromOrigin,
to: longPressMoveUpdateDetails.globalPosition,
cause: SelectionChangedCause.longPress,
);
break;
}
},
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,
@@ -160,4 +167,4 @@ class _BasicTextFieldState extends State<BasicTextField> {
),
);
}
}
}

View File

@@ -11,7 +11,8 @@ import 'toggle_button_state_manager.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);
/// A basic text input client. An implementation of [DeltaTextInputClient] meant to
/// send/receive information from the framework to the platform's text input plugin
@@ -39,11 +40,13 @@ class BasicTextInputClient extends StatefulWidget {
}
class BasicTextInputClientState extends State<BasicTextInputClient>
with TextSelectionDelegate implements DeltaTextInputClient {
with TextSelectionDelegate
implements DeltaTextInputClient {
final GlobalKey _textKey = GlobalKey();
late final ToggleButtonsStateManager toggleButtonStateManager;
late final TextEditingDeltaHistoryManager textEditingDeltaHistoryManager;
final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
final ClipboardStatusNotifier? _clipboardStatus =
kIsWeb ? null : ClipboardStatusNotifier();
@override
void initState() {
@@ -124,7 +127,7 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
}
@override
void updateEditingValue(TextEditingValue value) { /* Not using */}
void updateEditingValue(TextEditingValue value) {/* Not using */}
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
@@ -143,14 +146,18 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
return;
}
final bool selectionChanged = _value.selection.start != value.selection.start || _value.selection.end != value.selection.end;
textEditingDeltaHistoryManager.updateTextEditingDeltaHistoryOnInput(textEditingDeltas);
final bool selectionChanged =
_value.selection.start != value.selection.start ||
_value.selection.end != value.selection.end;
textEditingDeltaHistoryManager
.updateTextEditingDeltaHistoryOnInput(textEditingDeltas);
_value = value;
if (widget.controller is ReplacementTextEditingController) {
for (final TextEditingDelta delta in textEditingDeltas) {
(widget.controller as ReplacementTextEditingController).syncReplacementRanges(delta);
(widget.controller as ReplacementTextEditingController)
.syncReplacementRanges(delta);
}
}
@@ -246,7 +253,8 @@ 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);
toggleButtonStateManager.updateToggleButtonsOnSelection(validSelection);
}
@@ -264,17 +272,20 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
);
}
void _userUpdateTextEditingValueWithDelta(TextEditingDelta textEditingDelta, SelectionChangedCause cause) {
void _userUpdateTextEditingValueWithDelta(
TextEditingDelta textEditingDelta, SelectionChangedCause cause) {
TextEditingValue value = _value;
value = textEditingDelta.apply(value);
if (widget.controller is ReplacementTextEditingController) {
(widget.controller as ReplacementTextEditingController).syncReplacementRanges(textEditingDelta);
(widget.controller as ReplacementTextEditingController)
.syncReplacementRanges(textEditingDelta);
}
if (value != _value) {
textEditingDeltaHistoryManager.updateTextEditingDeltaHistoryOnInput([textEditingDelta]);
textEditingDeltaHistoryManager
.updateTextEditingDeltaHistoryOnInput([textEditingDelta]);
}
userUpdateTextEditingValue(value, cause);
@@ -292,16 +303,18 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
DeleteCharacterIntent: CallbackAction<DeleteCharacterIntent>(
onInvoke: (intent) => _delete(),
),
ExtendSelectionByCharacterIntent: CallbackAction<ExtendSelectionByCharacterIntent>(
onInvoke: (intent) => _extendSelection(intent.forward, intent.collapseSelection),
ExtendSelectionByCharacterIntent:
CallbackAction<ExtendSelectionByCharacterIntent>(
onInvoke: (intent) =>
_extendSelection(intent.forward, intent.collapseSelection),
),
SelectAllTextIntent : CallbackAction<SelectAllTextIntent>(
SelectAllTextIntent: CallbackAction<SelectAllTextIntent>(
onInvoke: (intent) => selectAll(intent.cause),
),
CopySelectionTextIntent : CallbackAction<CopySelectionTextIntent>(
CopySelectionTextIntent: CallbackAction<CopySelectionTextIntent>(
onInvoke: (intent) => copySelection(intent.cause),
),
PasteTextIntent : CallbackAction<PasteTextIntent>(
PasteTextIntent: CallbackAction<PasteTextIntent>(
onInvoke: (intent) => pasteText(intent.cause),
),
};
@@ -311,7 +324,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
late final TextRange deletedRange;
late final TextRange newComposing;
final int deletedLength = _value.text.substring(0, _selection.baseOffset).characters.last.length;
final int deletedLength =
_value.text.substring(0, _selection.baseOffset).characters.last.length;
if (_selection.isCollapsed) {
if (_selection.baseOffset == 0) return;
@@ -323,7 +337,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
deletedRange = _selection;
}
final bool isComposing = _selection.isCollapsed && _value.isComposingRangeValid;
final bool isComposing =
_selection.isCollapsed && _value.isComposingRangeValid;
if (isComposing) {
newComposing = TextRange.collapsed(deletedRange.start);
@@ -347,15 +362,26 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
if (collapseSelection) {
if (!_selection.isCollapsed) {
final int firstOffset = _selection.isNormalized ? _selection.start : _selection.end;
final int lastOffset = _selection.isNormalized ? _selection.end : _selection.start;
selection = TextSelection.collapsed(offset: forward ? lastOffset : firstOffset);
final int firstOffset =
_selection.isNormalized ? _selection.start : _selection.end;
final int lastOffset =
_selection.isNormalized ? _selection.end : _selection.start;
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;
? _value.text
.substring(_selection.baseOffset)
.characters
.first
.length
: -_value.text
.substring(0, _selection.baseOffset)
.characters
.last
.length;
selection = TextSelection.collapsed(
offset: _selection.baseOffset + adjustment,
);
@@ -365,7 +391,11 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
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;
: -_value.text
.substring(0, _selection.baseOffset)
.characters
.last
.length;
selection = TextSelection(
baseOffset: _selection.baseOffset,
extentOffset: _selection.extentOffset + adjustment,
@@ -382,7 +412,6 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
);
}
/// For updates to text editing value.
void _didChangeTextEditingValue() {
_updateRemoteTextEditingValueIfNeeded();
@@ -448,12 +477,13 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
case TargetPlatform.linux:
case TargetPlatform.windows:
_userUpdateTextEditingValueWithDelta(
TextEditingDeltaNonTextUpdate(
oldText: textEditingValue.text,
selection: TextSelection.collapsed(offset: textEditingValue.selection.end),
composing: TextRange.empty,
),
cause,
TextEditingDeltaNonTextUpdate(
oldText: textEditingValue.text,
selection: TextSelection.collapsed(
offset: textEditingValue.selection.end),
composing: TextRange.empty,
),
cause,
);
break;
}
@@ -469,14 +499,15 @@ 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,
replacementText: '',
replacedRange: cutRange,
selection: TextSelection.collapsed(offset: lastSelectionIndex),
composing: TextRange.empty,
oldText: textEditingValue.text,
replacementText: '',
replacedRange: cutRange,
selection: TextSelection.collapsed(offset: lastSelectionIndex),
composing: TextRange.empty,
),
cause,
);
@@ -505,15 +536,16 @@ 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);
final int lastSelectionIndex = math.max(
pasteRange.baseOffset, pasteRange.baseOffset + data.text!.length);
_userUpdateTextEditingValueWithDelta(
TextEditingDeltaReplacement(
oldText: textEditingValue.text,
replacementText: data.text!,
replacedRange: pasteRange,
selection: TextSelection.collapsed(offset: lastSelectionIndex),
composing: TextRange.empty,
oldText: textEditingValue.text,
replacementText: data.text!,
replacedRange: pasteRange,
selection: TextSelection.collapsed(offset: lastSelectionIndex),
composing: TextRange.empty,
),
cause,
);
@@ -523,12 +555,13 @@ 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,
selection: newSelection,
composing: TextRange.empty
oldText: textEditingValue.text,
selection: newSelection,
composing: TextRange.empty,
),
cause,
);
@@ -538,31 +571,38 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
TextEditingValue get textEditingValue => _value;
@override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {
void userUpdateTextEditingValue(
TextEditingValue value, SelectionChangedCause cause) {
if (value == _value) return;
final bool selectionChanged = _value.selection != value.selection;
if (cause == SelectionChangedCause.drag || cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.tap) {
if (cause == SelectionChangedCause.drag ||
cause == SelectionChangedCause.longPress ||
cause == SelectionChangedCause.tap) {
// Here the change is coming from gestures which call on RenderEditable to change the selection.
// Create a TextEditingDeltaNonTextUpdate so we can keep track of the delta history. RenderEditable
// does not report a delta on selection change.
final bool textChanged = _value.text != value.text;
if (selectionChanged && !textChanged) {
final TextEditingDeltaNonTextUpdate selectionUpdate = TextEditingDeltaNonTextUpdate(
oldText: value.text,
selection: value.selection,
composing: value.composing,
final TextEditingDeltaNonTextUpdate selectionUpdate =
TextEditingDeltaNonTextUpdate(
oldText: value.text,
selection: value.selection,
composing: value.composing,
);
if (widget.controller is ReplacementTextEditingController) {
(widget.controller as ReplacementTextEditingController).syncReplacementRanges(selectionUpdate);
(widget.controller as ReplacementTextEditingController)
.syncReplacementRanges(selectionUpdate);
}
textEditingDeltaHistoryManager.updateTextEditingDeltaHistoryOnInput([selectionUpdate]);
textEditingDeltaHistoryManager
.updateTextEditingDeltaHistoryOnInput([selectionUpdate]);
}
}
final bool selectionRangeChanged = _value.selection.start != value.selection.start
|| _value.selection.end != value.selection.end;
final bool selectionRangeChanged =
_value.selection.start != value.selection.start ||
_value.selection.end != value.selection.end;
_value = value;
@@ -570,7 +610,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
_handleSelectionChanged(_value.selection, cause);
if (selectionRangeChanged) {
toggleButtonStateManager.updateToggleButtonsOnSelection(_value.selection);
toggleButtonStateManager
.updateToggleButtonsOnSelection(_value.selection);
}
}
}
@@ -581,9 +622,11 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
final LayerLink _toolbarLayerLink = LayerLink();
TextSelectionOverlay? _selectionOverlay;
RenderEditable get renderEditable => _textKey.currentContext!.findRenderObject()! as RenderEditable;
RenderEditable get renderEditable =>
_textKey.currentContext!.findRenderObject()! as RenderEditable;
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
void _handleSelectionChanged(
TextSelection selection, SelectionChangedCause? cause) {
// We return early if the selection is not valid. This can happen when the
// text of [EditableText] is updated at the same time as the selection is
// changed by a gesture event.
@@ -647,20 +690,27 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSelectionChanged for $cause'),
context:
ErrorDescription('while calling onSelectionChanged for $cause'),
));
}
}
static final Map<ShortcutActivator, Intent> _defaultWebShortcuts = <ShortcutActivator, Intent>{
static final Map<ShortcutActivator, Intent> _defaultWebShortcuts =
<ShortcutActivator, Intent>{
// Activation
const SingleActivator(LogicalKeyboardKey.space): DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.space):
DoNothingAndStopPropagationIntent(),
// Scrolling
const SingleActivator(LogicalKeyboardKey.arrowUp): DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.arrowDown): DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.arrowLeft): DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.arrowRight): DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.arrowUp):
DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.arrowDown):
DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.arrowLeft):
DoNothingAndStopPropagationIntent(),
const SingleActivator(LogicalKeyboardKey.arrowRight):
DoNothingAndStopPropagationIntent(),
};
@override
@@ -684,7 +734,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey[100],
showCursor: ValueNotifier<bool>(_hasFocus),
forceLine: true, // Whether text field will take full line regardless of width.
forceLine:
true, // Whether text field will take full line regardless of width.
readOnly: false, // editable text-field.
hasFocus: _hasFocus,
maxLines: null, // multi-line text-field.
@@ -699,7 +750,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
textHeightBehavior: DefaultTextHeightBehavior.of(context),
textWidthBasis: TextWidthBasis.parent,
obscuringCharacter: '',
obscureText: false, // This is a non-private text field that does not require obfuscation.
obscureText:
false, // This is a non-private text field that does not require obfuscation.
offset: position,
onCaretChanged: null,
rendererIgnoresPointer: true,
@@ -708,7 +760,8 @@ class BasicTextInputClientState extends State<BasicTextInputClient>
cursorRadius: const Radius.circular(2.0),
cursorOffset: Offset.zero,
paintCursorAboveText: false,
enableInteractiveSelection: true, // make true to enable selection on mobile.
enableInteractiveSelection:
true, // make true to enable selection on mobile.
textSelectionDelegate: this,
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
promptRectRange: null,
@@ -900,4 +953,4 @@ class _Editable extends MultiChildRenderObjectWidget {
..clipBehavior = clipBehavior
..setPromptRectRange(promptRectRange);
}
}
}

View File

@@ -38,14 +38,15 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
final ReplacementTextEditingController _replacementTextEditingController =
ReplacementTextEditingController(
ReplacementTextEditingController(
text: 'The quick brown fox jumps over the lazy dog.',
);
final FocusNode _focusNode = FocusNode();
final List<bool> _isSelected = [false, false, false];
final List<TextEditingDelta> _textEditingDeltaHistory = [];
void _updateTextEditingDeltaHistory(List<TextEditingDelta> textEditingDeltas) {
void _updateTextEditingDeltaHistory(
List<TextEditingDelta> textEditingDeltas) {
for (final TextEditingDelta delta in textEditingDeltas) {
_textEditingDeltaHistory.add(delta);
}
@@ -53,7 +54,8 @@ class _MyHomePageState extends State<MyHomePage> {
setState(() {});
}
List<Widget> _buildTextEditingDeltaHistoryViews(List<TextEditingDelta> textEditingDeltas) {
List<Widget> _buildTextEditingDeltaHistoryViews(
List<TextEditingDelta> textEditingDeltas) {
List<Widget> textEditingDeltaViews = [];
for (final TextEditingDelta delta in textEditingDeltas) {
@@ -61,7 +63,8 @@ class _MyHomePageState extends State<MyHomePage> {
if (delta is TextEditingDeltaInsertion) {
deltaView = TextEditingDeltaView(
deltaType: delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaType:
delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaText: delta.textInserted,
deltaRange: TextRange.collapsed(delta.insertionOffset),
newSelection: delta.selection,
@@ -69,7 +72,8 @@ class _MyHomePageState extends State<MyHomePage> {
);
} else if (delta is TextEditingDeltaDeletion) {
deltaView = TextEditingDeltaView(
deltaType: delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaType:
delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaText: delta.textDeleted,
deltaRange: delta.deletedRange,
newSelection: delta.selection,
@@ -77,7 +81,8 @@ class _MyHomePageState extends State<MyHomePage> {
);
} else if (delta is TextEditingDeltaReplacement) {
deltaView = TextEditingDeltaView(
deltaType: delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaType:
delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaText: delta.replacementText,
deltaRange: delta.replacedRange,
newSelection: delta.selection,
@@ -85,7 +90,8 @@ class _MyHomePageState extends State<MyHomePage> {
);
} else if (delta is TextEditingDeltaNonTextUpdate) {
deltaView = TextEditingDeltaView(
deltaType: delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaType:
delta.runtimeType.toString().replaceAll('TextEditingDelta', ''),
deltaText: '',
deltaRange: TextRange.empty,
newSelection: delta.selection,
@@ -111,7 +117,8 @@ class _MyHomePageState extends State<MyHomePage> {
// 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 = _replacementTextEditingController.getReplacementsAtSelection(selection);
final List<TextStyle> replacementStyles =
_replacementTextEditingController.getReplacementsAtSelection(selection);
final List<bool> hasChanged = [false, false, false];
if (replacementStyles.isEmpty) {
@@ -157,9 +164,9 @@ class _MyHomePageState extends State<MyHomePage> {
void _updateToggleButtonsStateOnButtonPressed(int index) {
Map<int, TextStyle> attributeMap = const <int, TextStyle>{
0 : TextStyle(fontWeight: FontWeight.bold),
1 : TextStyle(fontStyle: FontStyle.italic),
2 : TextStyle(decoration: TextDecoration.underline),
0: TextStyle(fontWeight: FontWeight.bold),
1: TextStyle(fontStyle: FontStyle.italic),
2: TextStyle(decoration: TextDecoration.underline),
};
final TextRange replacementRange = TextRange(
@@ -172,14 +179,15 @@ class _MyHomePageState extends State<MyHomePage> {
_replacementTextEditingController.applyReplacement(
TextEditingInlineSpanReplacement(
replacementRange,
(string, range) => TextSpan(text: string, style: attributeMap[index]),
(string, range) => TextSpan(text: string, style: attributeMap[index]),
true,
),
);
setState(() {});
} else {
_replacementTextEditingController.disableExpand(attributeMap[index]!);
_replacementTextEditingController.removeReplacementsAtRange(replacementRange, attributeMap[index]);
_replacementTextEditingController.removeReplacementsAtRange(
replacementRange, attributeMap[index]);
setState(() {});
}
}
@@ -188,8 +196,8 @@ class _MyHomePageState extends State<MyHomePage> {
return Text(
text,
style: const TextStyle(
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
);
}
@@ -200,35 +208,38 @@ class _MyHomePageState extends State<MyHomePage> {
child: Row(
children: [
Expanded(
child: Tooltip(
message: 'The type of text input that is occurring.'
' Check out the documentation for TextEditingDelta for more information.',
child: _buildTextEditingDeltaViewHeading('Delta Type'),
),
child: Tooltip(
message: 'The type of text input that is occurring.'
' Check out the documentation for TextEditingDelta for more information.',
child: _buildTextEditingDeltaViewHeading('Delta Type'),
),
),
Expanded(
child: Tooltip(
message: 'The text that is being inserted or deleted',
child: _buildTextEditingDeltaViewHeading('Delta Text'),
),
child: Tooltip(
message: 'The text that is being inserted or deleted',
child: _buildTextEditingDeltaViewHeading('Delta Text'),
),
),
Expanded(
child: Tooltip(
message: 'The offset in the text where the text input is occurring.',
child: _buildTextEditingDeltaViewHeading('Delta Offset'),
),
child: Tooltip(
message:
'The offset in the text where the text input is occurring.',
child: _buildTextEditingDeltaViewHeading('Delta Offset'),
),
),
Expanded(
child: Tooltip(
message: 'The new text selection range after the text input has occurred.',
child: _buildTextEditingDeltaViewHeading('New Selection'),
),
child: Tooltip(
message:
'The new text selection range after the text input has occurred.',
child: _buildTextEditingDeltaViewHeading('New Selection'),
),
),
Expanded(
child: Tooltip(
message: 'The new composing range after the text input has occurred.',
child: _buildTextEditingDeltaViewHeading('New Composing'),
),
child: Tooltip(
message:
'The new composing range after the text input has occurred.',
child: _buildTextEditingDeltaViewHeading('New Composing'),
),
),
],
),
@@ -247,8 +258,7 @@ 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(
builder: (context) => const AlertDialog(
title: Center(child: Text('About')),
content: Text(aboutContent),
),
@@ -264,7 +274,7 @@ class _MyHomePageState extends State<MyHomePage> {
IconButton(
onPressed: () {
Navigator.of(context).restorablePush(_aboutDialogBuilder);
},
},
icon: const Icon(Icons.info_outline),
),
],
@@ -274,8 +284,10 @@ class _MyHomePageState extends State<MyHomePage> {
child: Center(
child: ToggleButtonsStateManager(
isToggleButtonsSelected: _isSelected,
updateToggleButtonsStateOnButtonPressed: _updateToggleButtonsStateOnButtonPressed,
updateToggleButtonStateOnSelectionChanged: _updateToggleButtonsStateOnSelectionChanged,
updateToggleButtonsStateOnButtonPressed:
_updateToggleButtonsStateOnButtonPressed,
updateToggleButtonStateOnSelectionChanged:
_updateToggleButtonsStateOnSelectionChanged,
child: Column(
children: [
Padding(
@@ -285,24 +297,27 @@ class _MyHomePageState extends State<MyHomePage> {
children: [
ToggleButtonsStateManager(
isToggleButtonsSelected: _isSelected,
updateToggleButtonsStateOnButtonPressed: _updateToggleButtonsStateOnButtonPressed,
updateToggleButtonStateOnSelectionChanged: _updateToggleButtonsStateOnSelectionChanged,
child: Builder(
builder: (innerContext) {
final ToggleButtonsStateManager manager = ToggleButtonsStateManager.of(innerContext);
updateToggleButtonsStateOnButtonPressed:
_updateToggleButtonsStateOnButtonPressed,
updateToggleButtonStateOnSelectionChanged:
_updateToggleButtonsStateOnSelectionChanged,
child: Builder(builder: (innerContext) {
final ToggleButtonsStateManager manager =
ToggleButtonsStateManager.of(innerContext);
return ToggleButtons(
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
isSelected: manager.toggleButtonsState,
onPressed: (index) => manager.updateToggleButtonsOnButtonPressed(index),
children: const [
Icon(Icons.format_bold),
Icon(Icons.format_italic),
Icon(Icons.format_underline),
],
);
}
),
return ToggleButtons(
borderRadius:
const BorderRadius.all(Radius.circular(4.0)),
isSelected: manager.toggleButtonsState,
onPressed: (index) => manager
.updateToggleButtonsOnButtonPressed(index),
children: const [
Icon(Icons.format_bold),
Icon(Icons.format_italic),
Icon(Icons.format_underline),
],
);
}),
),
],
),
@@ -312,14 +327,17 @@ class _MyHomePageState extends State<MyHomePage> {
padding: const EdgeInsets.symmetric(horizontal: 35.0),
child: ToggleButtonsStateManager(
isToggleButtonsSelected: _isSelected,
updateToggleButtonsStateOnButtonPressed: _updateToggleButtonsStateOnButtonPressed,
updateToggleButtonStateOnSelectionChanged: _updateToggleButtonsStateOnSelectionChanged,
updateToggleButtonsStateOnButtonPressed:
_updateToggleButtonsStateOnButtonPressed,
updateToggleButtonStateOnSelectionChanged:
_updateToggleButtonsStateOnSelectionChanged,
child: TextEditingDeltaHistoryManager(
history: _textEditingDeltaHistory,
updateHistoryOnInput: _updateTextEditingDeltaHistory,
child: BasicTextField(
controller: _replacementTextEditingController,
style: const TextStyle(fontSize: 18.0, color: Colors.black),
style: const TextStyle(
fontSize: 18.0, color: Colors.black),
focusNode: _focusNode,
),
),
@@ -336,18 +354,23 @@ class _MyHomePageState extends State<MyHomePage> {
updateHistoryOnInput: _updateTextEditingDeltaHistory,
child: Builder(
builder: (innerContext) {
final TextEditingDeltaHistoryManager manager = TextEditingDeltaHistoryManager.of(innerContext);
final TextEditingDeltaHistoryManager manager =
TextEditingDeltaHistoryManager.of(
innerContext);
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 35.0),
padding: const EdgeInsets.symmetric(
horizontal: 35.0),
itemBuilder: (context, index) {
return _buildTextEditingDeltaHistoryViews(manager.textEditingDeltaHistory)[index];
return _buildTextEditingDeltaHistoryViews(
manager.textEditingDeltaHistory)[index];
},
itemCount: manager.textEditingDeltaHistory.length,
itemCount:
manager.textEditingDeltaHistory.length,
separatorBuilder: (context, index) {
return const SizedBox(height: 2.0);
},
);
}
},
),
),
),
@@ -371,7 +394,7 @@ class TextEditingDeltaView extends StatelessWidget {
required this.deltaText,
required this.deltaRange,
required this.newSelection,
required this.newComposing
required this.newComposing,
}) : super(key: key);
final String deltaType;

View File

@@ -58,39 +58,44 @@ class TextEditingInlineSpanReplacement {
final TextRange deletedRange = delta.deletedRange;
final int deletedLength = delta.textDeleted.length;
if (range.start >= deletedRange.start
&& (range.start < deletedRange.end && range.end > deletedRange.end)) {
if (range.start >= deletedRange.start &&
(range.start < deletedRange.end && range.end > deletedRange.end)) {
return copy(
range: TextRange(
start: deletedRange.end - deletedLength,
end: range.end - deletedLength,
),
);
} else if ((range.start < deletedRange.start && range.end > deletedRange.start)
&& range.end <= deletedRange.end) {
} else if ((range.start < deletedRange.start &&
range.end > deletedRange.start) &&
range.end <= deletedRange.end) {
return copy(
range: TextRange(
start: range.start,
end: deletedRange.start,
),
);
} else if (range.start < deletedRange.start && range.end > deletedRange.end) {
} else if (range.start < deletedRange.start &&
range.end > deletedRange.end) {
return copy(
range: TextRange(
start: range.start,
end: range.end - deletedLength,
),
);
} else if (range.start >= deletedRange.start && range.end <= deletedRange.end) {
} else if (range.start >= deletedRange.start &&
range.end <= deletedRange.end) {
return null;
} else if (range.start > deletedRange.start && range.start >= deletedRange.end) {
} else if (range.start > deletedRange.start &&
range.start >= deletedRange.end) {
return copy(
range: TextRange(
start: range.start - deletedLength,
end: range.end - deletedLength,
),
);
} else if (range.end <= deletedRange.start && range.end < deletedRange.end) {
} else if (range.end <= deletedRange.start &&
range.end < deletedRange.end) {
return copy(
range: TextRange(
start: range.start,
@@ -102,7 +107,8 @@ class TextEditingInlineSpanReplacement {
return null;
}
TextEditingInlineSpanReplacement? onInsertion(TextEditingDeltaInsertion delta) {
TextEditingInlineSpanReplacement? onInsertion(
TextEditingDeltaInsertion delta) {
final int insertionOffset = delta.insertionOffset;
final int insertedLength = delta.textInserted.length;
@@ -122,7 +128,8 @@ class TextEditingInlineSpanReplacement {
),
);
}
} if (range.start < insertionOffset && range.end < insertionOffset) {
}
if (range.start < insertionOffset && range.end < insertionOffset) {
return copy(
range: TextRange(
start: range.start,
@@ -148,20 +155,21 @@ class TextEditingInlineSpanReplacement {
return null;
}
List<TextEditingInlineSpanReplacement>? onReplacement(TextEditingDeltaReplacement delta) {
List<TextEditingInlineSpanReplacement>? onReplacement(
TextEditingDeltaReplacement delta) {
final TextRange replacedRange = delta.replacedRange;
final bool replacementShortenedText = delta.replacementText.length <
delta.textReplaced.length;
final bool replacementLengthenedText = 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
final bool replacementShortenedText =
delta.replacementText.length < delta.textReplaced.length;
final bool replacementLengthenedText =
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;
if (range.start >= replacedRange.start
&& (range.start < replacedRange.end && range.end > replacedRange.end)) {
if (range.start >= replacedRange.start &&
(range.start < replacedRange.end && range.end > replacedRange.end)) {
if (replacementShortenedText) {
return [
copy(
@@ -190,8 +198,9 @@ class TextEditingInlineSpanReplacement {
),
];
}
} else if ((range.start < replacedRange.start && range.end > replacedRange.start)
&& range.end <= replacedRange.end) {
} else if ((range.start < replacedRange.start &&
range.end > replacedRange.start) &&
range.end <= replacedRange.end) {
return [
copy(
range: TextRange(
@@ -200,7 +209,8 @@ class TextEditingInlineSpanReplacement {
),
),
];
} else if (range.start < replacedRange.start && range.end > replacedRange.end) {
} else if (range.start < replacedRange.start &&
range.end > replacedRange.end) {
if (replacementShortenedText) {
return [
copy(
@@ -247,10 +257,12 @@ class TextEditingInlineSpanReplacement {
),
];
}
} else if (range.start >= replacedRange.start && range.end <= replacedRange.end) {
} else if (range.start >= replacedRange.start &&
range.end <= replacedRange.end) {
// remove attribute.
return null;
} else if (range.start > replacedRange.start && range.start >= replacedRange.end) {
} else if (range.start > replacedRange.start &&
range.start >= replacedRange.end) {
if (replacementShortenedText) {
return [
copy(
@@ -272,7 +284,8 @@ class TextEditingInlineSpanReplacement {
} else if (replacementEqualLength) {
return [this];
}
} else if (range.end <= replacedRange.start && range.end < replacedRange.end) {
} else if (range.end <= replacedRange.start &&
range.end < replacedRange.end) {
return [
copy(
range: TextRange(
@@ -286,9 +299,11 @@ class TextEditingInlineSpanReplacement {
return null;
}
TextEditingInlineSpanReplacement? onNonTextUpdate(TextEditingDeltaNonTextUpdate delta) {
TextEditingInlineSpanReplacement? onNonTextUpdate(
TextEditingDeltaNonTextUpdate delta) {
if (range.isCollapsed) {
if (range.start != delta.selection.start && range.end != delta.selection.end) {
if (range.start != delta.selection.start &&
range.end != delta.selection.end) {
return null;
}
}
@@ -296,8 +311,8 @@ class TextEditingInlineSpanReplacement {
}
List<TextEditingInlineSpanReplacement>? removeRange(TextRange removalRange) {
if (range.start >= removalRange.start
&& (range.start < removalRange.end && range.end > removalRange.end)) {
if (range.start >= removalRange.start &&
(range.start < removalRange.end && range.end > removalRange.end)) {
return [
copy(
range: TextRange(
@@ -306,8 +321,9 @@ class TextEditingInlineSpanReplacement {
),
),
];
} else if ((range.start < removalRange.start && range.end > removalRange.start)
&& range.end <= removalRange.end) {
} else if ((range.start < removalRange.start &&
range.end > removalRange.start) &&
range.end <= removalRange.end) {
return [
copy(
range: TextRange(
@@ -316,7 +332,8 @@ class TextEditingInlineSpanReplacement {
),
),
];
} else if (range.start < removalRange.start && range.end > removalRange.end) {
} else if (range.start < removalRange.start &&
range.end > removalRange.end) {
return [
copy(
range: TextRange(
@@ -332,11 +349,14 @@ class TextEditingInlineSpanReplacement {
),
),
];
} else if (range.start >= removalRange.start && range.end <= removalRange.end) {
} else if (range.start >= removalRange.start &&
range.end <= removalRange.end) {
return null;
} else if (range.start > removalRange.start && range.start >= removalRange.end) {
} else if (range.start > removalRange.start &&
range.start >= removalRange.end) {
return [this];
} else if (range.end <= removalRange.start && range.end < removalRange.end) {
} else if (range.end <= removalRange.start &&
range.end < removalRange.end) {
return [this];
} else if (removalRange.isCollapsed && range.end == removalRange.start) {
return [this];
@@ -348,7 +368,8 @@ class TextEditingInlineSpanReplacement {
/// Creates a new replacement with all properties copied except for range, which
/// is updated to the specified value.
TextEditingInlineSpanReplacement copy({TextRange? range, bool? expand}) {
return TextEditingInlineSpanReplacement(range ?? this.range, generator, expand ?? this.expand);
return TextEditingInlineSpanReplacement(
range ?? this.range, generator, expand ?? this.expand);
}
@override
@@ -371,7 +392,7 @@ class ReplacementTextEditingController extends TextEditingController {
String? text,
List<TextEditingInlineSpanReplacement>? replacements,
this.composingRegionReplaceable = true,
}) : replacements = replacements ?? [],
}) : replacements = replacements ?? [],
super(text: text);
/// Creates a controller for an editable text field from an initial [TextEditingValue].
@@ -379,7 +400,7 @@ class ReplacementTextEditingController extends TextEditingController {
/// This constructor treats a null [value] argument as if it were [TextEditingValue.empty].
ReplacementTextEditingController.fromValue(TextEditingValue? value,
{List<TextEditingInlineSpanReplacement>? replacements,
this.composingRegionReplaceable = true})
this.composingRegionReplaceable = true})
: super.fromValue(value);
/// The [TextEditingInlineSpanReplacement]s that are evaluated on the editing value.
@@ -494,7 +515,8 @@ class ReplacementTextEditingController extends TextEditingController {
}
}
for (final TextEditingInlineSpanReplacement replacementToRemove in toRemove) {
for (final TextEditingInlineSpanReplacement replacementToRemove
in toRemove) {
replacements!.remove(replacementToRemove);
}
@@ -507,17 +529,19 @@ 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 = <TextRange, InlineSpan>{};
final Map<TextRange, InlineSpan> rangeSpanMapping =
<TextRange, InlineSpan>{};
// Iterate through TextEditingInlineSpanReplacements, handling overlapping
// replacements and mapping them towards a generated InlineSpan.
if (replacements != null) {
for (final TextEditingInlineSpanReplacement replacement in replacements!) {
for (final TextEditingInlineSpanReplacement replacement
in replacements!) {
_addToMappingWithOverlaps(
replacement.generator,
TextRange(start: replacement.range.start, end: replacement.range.end),
@@ -532,9 +556,9 @@ class ReplacementTextEditingController extends TextEditingController {
// be thrown and this EditableText will be built with a broken subtree.
//
// Add composing region as a replacement to a TextSpan with underline.
if (composingRegionReplaceable
&& value.isComposingRangeValid
&& withComposing) {
if (composingRegionReplaceable &&
value.isComposingRangeValid &&
withComposing) {
_addToMappingWithOverlaps((value, range) {
final TextStyle composingStyle = style != null
? style.merge(const TextStyle(decoration: TextDecoration.underline))
@@ -556,7 +580,8 @@ 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;
@@ -583,8 +608,8 @@ class ReplacementTextEditingController extends TextEditingController {
bool overlap = false;
List<TextRange> overlapRanges = <TextRange>[];
for (final TextRange range in rangeSpanMapping.keys) {
if (math.max(matchedRange.start, range.start)
<= math.min(matchedRange.end, range.end)) {
if (math.max(matchedRange.start, range.start) <=
math.min(matchedRange.end, range.end)) {
overlap = true;
overlapRanges.add(range);
}
@@ -593,10 +618,18 @@ class ReplacementTextEditingController extends TextEditingController {
final List<List<dynamic>> overlappingTriples = <List<dynamic>>[];
if (overlap) {
overlappingTriples.add(<dynamic>[matchedRange.start, matchedRange.end, generator(matchedRange.textInside(text), matchedRange).style]);
overlappingTriples.add(<dynamic>[
matchedRange.start,
matchedRange.end,
generator(matchedRange.textInside(text), matchedRange).style
]);
for (final TextRange overlappingRange in overlapRanges) {
overlappingTriples.add(<dynamic>[overlappingRange.start, overlappingRange.end, rangeSpanMapping[overlappingRange]!.style]);
overlappingTriples.add(<dynamic>[
overlappingRange.start,
overlappingRange.end,
rangeSpanMapping[overlappingRange]!.style
]);
rangeSpanMapping.remove(overlappingRange);
}
@@ -608,10 +641,11 @@ class ReplacementTextEditingController extends TextEditingController {
if (toRemoveRangesThatHaveBeenMerged.contains(tripleA)) continue;
for (int j = i + 1; j < overlappingTriples.length; j++) {
final List<dynamic> tripleB = overlappingTriples[j];
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]);
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]);
tripleA = <dynamic>[
math.min(tripleA[0] as int, tripleB[0] as int),
math.max(tripleA[1] as int, tripleB[1] as int),
@@ -621,9 +655,9 @@ class ReplacementTextEditingController extends TextEditingController {
}
}
if (didOverlap
&& !toAddRangesThatHaveBeenMerged.contains(tripleA)
&& !toRemoveRangesThatHaveBeenMerged.contains(tripleA)) {
if (didOverlap &&
!toAddRangesThatHaveBeenMerged.contains(tripleA) &&
!toRemoveRangesThatHaveBeenMerged.contains(tripleA)) {
toAddRangesThatHaveBeenMerged.add(tripleA);
}
}
@@ -658,15 +692,14 @@ class ReplacementTextEditingController extends TextEditingController {
}
Set<TextStyle> styles = <TextStyle>{};
List<int> otherEndPoints = endPoints.getRange(1, endPoints.length).toList();
List<int> otherEndPoints =
endPoints.getRange(1, endPoints.length).toList();
for (int i = 0; i < endPoints.length - 1; i++) {
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;
@@ -674,10 +707,8 @@ 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);
}
}
@@ -704,8 +735,9 @@ 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));
@@ -713,11 +745,13 @@ class ReplacementTextEditingController extends TextEditingController {
}
}
for (final TextEditingInlineSpanReplacement replacementToRemove in toRemove) {
for (final TextEditingInlineSpanReplacement replacementToRemove
in toRemove) {
replacements!.remove(replacementToRemove);
}
for (final TextEditingInlineSpanReplacement replacementWithExpandDisabled in toAdd) {
for (final TextEditingInlineSpanReplacement replacementWithExpandDisabled
in toAdd) {
replacements!.add(replacementWithExpandDisabled);
}
}
@@ -733,30 +767,27 @@ class ReplacementTextEditingController extends TextEditingController {
for (final TextEditingInlineSpanReplacement replacement in replacements!) {
if (selection.isCollapsed) {
if (math.max(replacement.range.start, selection.start)
<= math.min(replacement.range.end, selection.end)) {
if (math.max(replacement.range.start, selection.start) <=
math.min(replacement.range.end, selection.end)) {
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!);
}
}
}
} else {
if (math.max(replacement.range.start, selection.start)
<= math.min(replacement.range.end, selection.end)) {
if (math.max(replacement.range.start, selection.start) <=
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!);
}
}
}
@@ -769,17 +800,19 @@ class ReplacementTextEditingController extends TextEditingController {
final List<TextEditingInlineSpanReplacement> toRemove = [];
final List<TextEditingInlineSpanReplacement> toAdd = [];
for(int i = 0; i < replacements!.length; i++) {
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;
if ((math.max(replacement.range.start, removalRange.start)
<= math.min(replacement.range.end, removalRange.end))
&& replacementStyle != null) {
if ((math.max(replacement.range.start, removalRange.start) <=
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) {
@@ -809,4 +842,4 @@ class ReplacementTextEditingController extends TextEditingController {
replacements!.remove(replacementToRemove);
}
}
}
}

View File

@@ -3,7 +3,8 @@ import 'package:flutter/widgets.dart';
/// Signature for the callback that updates text editing delta history when a new delta
/// is received.
typedef TextEditingDeltaHistoryUpdateCallback = void Function(List<TextEditingDelta> textEditingDeltas);
typedef TextEditingDeltaHistoryUpdateCallback = void Function(
List<TextEditingDelta> textEditingDeltas);
class TextEditingDeltaHistoryManager extends InheritedWidget {
const TextEditingDeltaHistoryManager({
@@ -11,25 +12,29 @@ class TextEditingDeltaHistoryManager extends InheritedWidget {
required Widget child,
required List<TextEditingDelta> history,
required TextEditingDeltaHistoryUpdateCallback updateHistoryOnInput,
})
: _textEditingDeltaHistory = history,
}) : _textEditingDeltaHistory = history,
_updateTextEditingDeltaHistoryOnInput = updateHistoryOnInput,
super(key: key, child: child);
static TextEditingDeltaHistoryManager of(BuildContext context) {
final TextEditingDeltaHistoryManager? result = context.dependOnInheritedWidgetOfExactType<TextEditingDeltaHistoryManager>();
final TextEditingDeltaHistoryManager? result = context
.dependOnInheritedWidgetOfExactType<TextEditingDeltaHistoryManager>();
assert(result != null, 'No ToggleButtonsStateManager found in context');
return result!;
}
final List<TextEditingDelta> _textEditingDeltaHistory;
final TextEditingDeltaHistoryUpdateCallback _updateTextEditingDeltaHistoryOnInput;
final TextEditingDeltaHistoryUpdateCallback
_updateTextEditingDeltaHistoryOnInput;
List<TextEditingDelta> get textEditingDeltaHistory => _textEditingDeltaHistory;
TextEditingDeltaHistoryUpdateCallback get updateTextEditingDeltaHistoryOnInput => _updateTextEditingDeltaHistoryOnInput;
List<TextEditingDelta> get textEditingDeltaHistory =>
_textEditingDeltaHistory;
TextEditingDeltaHistoryUpdateCallback
get updateTextEditingDeltaHistoryOnInput =>
_updateTextEditingDeltaHistoryOnInput;
@override
bool updateShouldNotify(TextEditingDeltaHistoryManager oldWidget) {
return textEditingDeltaHistory != oldWidget.textEditingDeltaHistory;
}
}
}

View File

@@ -2,40 +2,52 @@ import 'package:flutter/widgets.dart';
/// Signature for the callback that updates toggle button state when the user changes the selection
/// (including the cursor location).
typedef UpdateToggleButtonsStateOnSelectionChangedCallback = void Function(TextSelection selection);
typedef UpdateToggleButtonsStateOnSelectionChangedCallback = void Function(
TextSelection selection);
/// Signature for the callback that updates toggle button state when the user
/// presses the toggle button.
typedef UpdateToggleButtonsStateOnButtonPressedCallback = void Function(int index);
typedef UpdateToggleButtonsStateOnButtonPressedCallback = void Function(
int index);
class ToggleButtonsStateManager extends InheritedWidget {
const ToggleButtonsStateManager({
Key? key,
required Widget child,
required List<bool> isToggleButtonsSelected,
required UpdateToggleButtonsStateOnButtonPressedCallback updateToggleButtonsStateOnButtonPressed,
required UpdateToggleButtonsStateOnSelectionChangedCallback updateToggleButtonStateOnSelectionChanged,
})
: _isToggleButtonsSelected = isToggleButtonsSelected,
_updateToggleButtonsStateOnButtonPressed = updateToggleButtonsStateOnButtonPressed,
_updateToggleButtonStateOnSelectionChanged = updateToggleButtonStateOnSelectionChanged,
required UpdateToggleButtonsStateOnButtonPressedCallback
updateToggleButtonsStateOnButtonPressed,
required UpdateToggleButtonsStateOnSelectionChangedCallback
updateToggleButtonStateOnSelectionChanged,
}) : _isToggleButtonsSelected = isToggleButtonsSelected,
_updateToggleButtonsStateOnButtonPressed =
updateToggleButtonsStateOnButtonPressed,
_updateToggleButtonStateOnSelectionChanged =
updateToggleButtonStateOnSelectionChanged,
super(key: key, child: child);
static ToggleButtonsStateManager of(BuildContext context) {
final ToggleButtonsStateManager? result = context.dependOnInheritedWidgetOfExactType<ToggleButtonsStateManager>();
final ToggleButtonsStateManager? result =
context.dependOnInheritedWidgetOfExactType<ToggleButtonsStateManager>();
assert(result != null, 'No ToggleButtonsStateManager found in context');
return result!;
}
final List<bool> _isToggleButtonsSelected;
final UpdateToggleButtonsStateOnButtonPressedCallback _updateToggleButtonsStateOnButtonPressed;
final UpdateToggleButtonsStateOnSelectionChangedCallback _updateToggleButtonStateOnSelectionChanged;
final UpdateToggleButtonsStateOnButtonPressedCallback
_updateToggleButtonsStateOnButtonPressed;
final UpdateToggleButtonsStateOnSelectionChangedCallback
_updateToggleButtonStateOnSelectionChanged;
List<bool> get toggleButtonsState => _isToggleButtonsSelected;
UpdateToggleButtonsStateOnButtonPressedCallback get updateToggleButtonsOnButtonPressed => _updateToggleButtonsStateOnButtonPressed;
UpdateToggleButtonsStateOnSelectionChangedCallback get updateToggleButtonsOnSelection => _updateToggleButtonStateOnSelectionChanged;
UpdateToggleButtonsStateOnButtonPressedCallback
get updateToggleButtonsOnButtonPressed =>
_updateToggleButtonsStateOnButtonPressed;
UpdateToggleButtonsStateOnSelectionChangedCallback
get updateToggleButtonsOnSelection =>
_updateToggleButtonStateOnSelectionChanged;
@override
bool updateShouldNotify(ToggleButtonsStateManager oldWidget) =>
toggleButtonsState != oldWidget.toggleButtonsState;
}
}

View File

@@ -12,46 +12,51 @@ import 'package:simplistic_editor/basic_text_input_client.dart';
import 'package:simplistic_editor/main.dart';
void main() {
testWidgets('Default main page shows all components',
(tester) async {
await tester.pumpWidget(const MyApp());
testWidgets('Default main page shows all components', (tester) async {
await tester.pumpWidget(const MyApp());
// 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);
// 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);
// Elements on the main screen
// Delta labels.
expect(
find.widgetWithText(Tooltip, "Delta Type"), findsOneWidget);
expect(find.widgetWithText(Tooltip, "Delta Text"), findsOneWidget);
expect(
find.widgetWithText(Tooltip, "Delta Offset"),
findsOneWidget);
expect(
find.widgetWithText(Tooltip, "New Selection"), findsOneWidget);
expect(find.widgetWithText(Tooltip, "New Composing"), findsOneWidget);
// Elements on the main screen
// Delta labels.
expect(find.widgetWithText(Tooltip, "Delta Type"), findsOneWidget);
expect(find.widgetWithText(Tooltip, "Delta Text"), findsOneWidget);
expect(find.widgetWithText(Tooltip, "Delta Offset"), findsOneWidget);
expect(find.widgetWithText(Tooltip, "New Selection"), findsOneWidget);
expect(find.widgetWithText(Tooltip, "New Composing"), findsOneWidget);
// Selection delta is generated and delta history is visible.
await tester.tap(find.byType(BasicTextInputClient));
await tester.pumpAndSettle();
expect(
find.widgetWithText(TextEditingDeltaView, "NonTextUpdate"), findsOneWidget);
// Selection delta is generated and delta history is visible.
await tester.tap(find.byType(BasicTextInputClient));
await tester.pumpAndSettle();
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);
expect(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);
// 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);
expect(
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);
await tester.tap(find.widgetWithIcon(IconButton, Icons.info_outline));
await tester.pumpAndSettle();
expect(find.widgetWithText(Center, 'About'), findsOneWidget);
});
// About Dialog
expect(find.widgetWithIcon(IconButton, Icons.info_outline), findsOneWidget);
await tester.tap(find.widgetWithIcon(IconButton, Icons.info_outline));
await tester.pumpAndSettle();
expect(find.widgetWithText(Center, 'About'), findsOneWidget);
});
}