|
|
|
@ -363,12 +363,15 @@ class RawEditorState extends EditorState |
|
|
|
|
|
|
|
|
|
return QuillStyles( |
|
|
|
|
data: _styles!, |
|
|
|
|
child: MouseRegion( |
|
|
|
|
cursor: SystemMouseCursors.text, |
|
|
|
|
child: QuillKeyboardListener( |
|
|
|
|
child: Container( |
|
|
|
|
constraints: constraints, |
|
|
|
|
child: child, |
|
|
|
|
child: Actions( |
|
|
|
|
actions: _actions, |
|
|
|
|
child: Focus( |
|
|
|
|
focusNode: widget.focusNode, |
|
|
|
|
child: QuillKeyboardListener( |
|
|
|
|
child: Container( |
|
|
|
|
constraints: constraints, |
|
|
|
|
child: child, |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
@ -1006,6 +1009,123 @@ class RawEditorState extends EditorState |
|
|
|
|
_floatingCursorResetController; |
|
|
|
|
|
|
|
|
|
late AnimationController _floatingCursorResetController; |
|
|
|
|
|
|
|
|
|
// --------------------------- Text Editing Actions -------------------------- |
|
|
|
|
|
|
|
|
|
_TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) { |
|
|
|
|
final _TextBoundary atomicTextBoundary = |
|
|
|
|
_CharacterBoundary(textEditingValue); |
|
|
|
|
return _CollapsedSelectionBoundary(atomicTextBoundary, intent.forward); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) { |
|
|
|
|
final _TextBoundary atomicTextBoundary; |
|
|
|
|
final _TextBoundary boundary; |
|
|
|
|
|
|
|
|
|
// final TextEditingValue textEditingValue = |
|
|
|
|
// _textEditingValueforTextLayoutMetrics; |
|
|
|
|
atomicTextBoundary = _CharacterBoundary(textEditingValue); |
|
|
|
|
// This isn't enough. Newline characters. |
|
|
|
|
boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue), |
|
|
|
|
_WordBoundary(renderEditor, textEditingValue)); |
|
|
|
|
|
|
|
|
|
final mixedBoundary = intent.forward |
|
|
|
|
? _MixedBoundary(atomicTextBoundary, boundary) |
|
|
|
|
: _MixedBoundary(boundary, atomicTextBoundary); |
|
|
|
|
// Use a _MixedBoundary to make sure we don't leave invalid codepoints in |
|
|
|
|
// the field after deletion. |
|
|
|
|
return _CollapsedSelectionBoundary(mixedBoundary, intent.forward); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_TextBoundary _linebreak(DirectionalTextEditingIntent intent) { |
|
|
|
|
final _TextBoundary atomicTextBoundary; |
|
|
|
|
final _TextBoundary boundary; |
|
|
|
|
|
|
|
|
|
// final TextEditingValue textEditingValue = |
|
|
|
|
// _textEditingValueforTextLayoutMetrics; |
|
|
|
|
atomicTextBoundary = _CharacterBoundary(textEditingValue); |
|
|
|
|
boundary = _LineBreak(renderEditor, textEditingValue); |
|
|
|
|
|
|
|
|
|
// The _MixedBoundary is to make sure we don't leave invalid code units in |
|
|
|
|
// the field after deletion. |
|
|
|
|
// `boundary` doesn't need to be wrapped in a _CollapsedSelectionBoundary, |
|
|
|
|
// since the document boundary is unique and the linebreak boundary is |
|
|
|
|
// already caret-location based. |
|
|
|
|
return intent.forward |
|
|
|
|
? _MixedBoundary( |
|
|
|
|
_CollapsedSelectionBoundary(atomicTextBoundary, true), boundary) |
|
|
|
|
: _MixedBoundary( |
|
|
|
|
boundary, _CollapsedSelectionBoundary(atomicTextBoundary, false)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => |
|
|
|
|
_DocumentBoundary(textEditingValue); |
|
|
|
|
|
|
|
|
|
Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) { |
|
|
|
|
return Action<T>.overridable( |
|
|
|
|
context: context, defaultAction: defaultAction); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
late final Action<ReplaceTextIntent> _replaceTextAction = |
|
|
|
|
CallbackAction<ReplaceTextIntent>(onInvoke: _replaceText); |
|
|
|
|
|
|
|
|
|
void _updateSelection(UpdateSelectionIntent intent) { |
|
|
|
|
userUpdateTextEditingValue( |
|
|
|
|
intent.currentTextEditingValue.copyWith(selection: intent.newSelection), |
|
|
|
|
intent.cause, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
late final Action<UpdateSelectionIntent> _updateSelectionAction = |
|
|
|
|
CallbackAction<UpdateSelectionIntent>(onInvoke: _updateSelection); |
|
|
|
|
|
|
|
|
|
late final _UpdateTextSelectionToAdjacentLineAction< |
|
|
|
|
ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = |
|
|
|
|
_UpdateTextSelectionToAdjacentLineAction< |
|
|
|
|
ExtendSelectionVerticallyToAdjacentLineIntent>(this); |
|
|
|
|
|
|
|
|
|
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{ |
|
|
|
|
DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), |
|
|
|
|
ReplaceTextIntent: _replaceTextAction, |
|
|
|
|
UpdateSelectionIntent: _updateSelectionAction, |
|
|
|
|
DirectionalFocusIntent: DirectionalFocusAction.forTextField(), |
|
|
|
|
|
|
|
|
|
// Delete |
|
|
|
|
DeleteCharacterIntent: _makeOverridable( |
|
|
|
|
_DeleteTextAction<DeleteCharacterIntent>(this, _characterBoundary)), |
|
|
|
|
DeleteToNextWordBoundaryIntent: _makeOverridable( |
|
|
|
|
_DeleteTextAction<DeleteToNextWordBoundaryIntent>( |
|
|
|
|
this, _nextWordBoundary)), |
|
|
|
|
DeleteToLineBreakIntent: _makeOverridable( |
|
|
|
|
_DeleteTextAction<DeleteToLineBreakIntent>(this, _linebreak)), |
|
|
|
|
|
|
|
|
|
// Extend/Move Selection |
|
|
|
|
ExtendSelectionByCharacterIntent: _makeOverridable( |
|
|
|
|
_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>( |
|
|
|
|
this, |
|
|
|
|
false, |
|
|
|
|
_characterBoundary, |
|
|
|
|
)), |
|
|
|
|
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( |
|
|
|
|
_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>( |
|
|
|
|
this, true, _nextWordBoundary)), |
|
|
|
|
ExtendSelectionToLineBreakIntent: _makeOverridable( |
|
|
|
|
_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>( |
|
|
|
|
this, true, _linebreak)), |
|
|
|
|
ExtendSelectionVerticallyToAdjacentLineIntent: |
|
|
|
|
_makeOverridable(_adjacentLineAction), |
|
|
|
|
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( |
|
|
|
|
_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>( |
|
|
|
|
this, true, _documentBoundary)), |
|
|
|
|
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( |
|
|
|
|
_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)), |
|
|
|
|
|
|
|
|
|
// Copy Paste |
|
|
|
|
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), |
|
|
|
|
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), |
|
|
|
|
PasteTextIntent: _makeOverridable(CallbackAction<PasteTextIntent>( |
|
|
|
|
onInvoke: (intent) => pasteText(intent.cause))), |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class _Editor extends MultiChildRenderObjectWidget { |
|
|
|
@ -1083,3 +1203,609 @@ class _Editor extends MultiChildRenderObjectWidget { |
|
|
|
|
..maxContentWidth = maxContentWidth; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// An interface for retrieving the logical text boundary |
|
|
|
|
/// (left-closed-right-open) |
|
|
|
|
/// at a given location in a document. |
|
|
|
|
/// |
|
|
|
|
/// Depending on the implementation of the [_TextBoundary], the input |
|
|
|
|
/// [TextPosition] can either point to a code unit, or a position between 2 code |
|
|
|
|
/// units (which can be visually represented by the caret if the selection were |
|
|
|
|
/// to collapse to that position). |
|
|
|
|
/// |
|
|
|
|
/// For example, [_LineBreak] interprets the input [TextPosition] as a caret |
|
|
|
|
/// location, since in Flutter the caret is generally painted between the |
|
|
|
|
/// character the [TextPosition] points to and its previous character, and |
|
|
|
|
/// [_LineBreak] cares about the affinity of the input [TextPosition]. Most |
|
|
|
|
/// other text boundaries however, interpret the input [TextPosition] as the |
|
|
|
|
/// location of a code unit in the document, since it's easier to reason about |
|
|
|
|
/// the text boundary given a code unit in the text. |
|
|
|
|
/// |
|
|
|
|
/// To convert a "code-unit-based" [_TextBoundary] to "caret-location-based", |
|
|
|
|
/// use the [_CollapsedSelectionBoundary] combinator. |
|
|
|
|
abstract class _TextBoundary { |
|
|
|
|
const _TextBoundary(); |
|
|
|
|
|
|
|
|
|
TextEditingValue get textEditingValue; |
|
|
|
|
|
|
|
|
|
/// Returns the leading text boundary at the given location, inclusive. |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position); |
|
|
|
|
|
|
|
|
|
/// Returns the trailing text boundary at the given location, exclusive. |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position); |
|
|
|
|
|
|
|
|
|
TextRange getTextBoundaryAt(TextPosition position) { |
|
|
|
|
return TextRange( |
|
|
|
|
start: getLeadingTextBoundaryAt(position).offset, |
|
|
|
|
end: getTrailingTextBoundaryAt(position).offset, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ----------------------------- Text Boundaries ----------------------------- |
|
|
|
|
|
|
|
|
|
// The word modifier generally removes the word boundaries around white spaces |
|
|
|
|
// (and newlines), IOW white spaces and some other punctuations are considered |
|
|
|
|
// a part of the next word in the search direction. |
|
|
|
|
class _WhitespaceBoundary extends _TextBoundary { |
|
|
|
|
const _WhitespaceBoundary(this.textEditingValue); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
final TextEditingValue textEditingValue; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) { |
|
|
|
|
for (var index = position.offset; index >= 0; index -= 1) { |
|
|
|
|
if (!TextLayoutMetrics.isWhitespace( |
|
|
|
|
textEditingValue.text.codeUnitAt(index))) { |
|
|
|
|
return TextPosition(offset: index); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return const TextPosition(offset: 0); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) { |
|
|
|
|
for (var index = position.offset; |
|
|
|
|
index < textEditingValue.text.length; |
|
|
|
|
index += 1) { |
|
|
|
|
if (!TextLayoutMetrics.isWhitespace( |
|
|
|
|
textEditingValue.text.codeUnitAt(index))) { |
|
|
|
|
return TextPosition(offset: index + 1); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return TextPosition(offset: textEditingValue.text.length); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Most apps delete the entire grapheme when the backspace key is pressed. |
|
|
|
|
// Also always put the new caret location to character boundaries to avoid |
|
|
|
|
// sending malformed UTF-16 code units to the paragraph builder. |
|
|
|
|
class _CharacterBoundary extends _TextBoundary { |
|
|
|
|
const _CharacterBoundary(this.textEditingValue); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
final TextEditingValue textEditingValue; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) { |
|
|
|
|
final int endOffset = |
|
|
|
|
math.min(position.offset + 1, textEditingValue.text.length); |
|
|
|
|
return TextPosition( |
|
|
|
|
offset: |
|
|
|
|
CharacterRange.at(textEditingValue.text, position.offset, endOffset) |
|
|
|
|
.stringBeforeLength, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) { |
|
|
|
|
final int endOffset = |
|
|
|
|
math.min(position.offset + 1, textEditingValue.text.length); |
|
|
|
|
final range = |
|
|
|
|
CharacterRange.at(textEditingValue.text, position.offset, endOffset); |
|
|
|
|
return TextPosition( |
|
|
|
|
offset: textEditingValue.text.length - range.stringAfterLength, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextRange getTextBoundaryAt(TextPosition position) { |
|
|
|
|
final int endOffset = |
|
|
|
|
math.min(position.offset + 1, textEditingValue.text.length); |
|
|
|
|
final range = |
|
|
|
|
CharacterRange.at(textEditingValue.text, position.offset, endOffset); |
|
|
|
|
return TextRange( |
|
|
|
|
start: range.stringBeforeLength, |
|
|
|
|
end: textEditingValue.text.length - range.stringAfterLength, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries. |
|
|
|
|
class _WordBoundary extends _TextBoundary { |
|
|
|
|
const _WordBoundary(this.textLayout, this.textEditingValue); |
|
|
|
|
|
|
|
|
|
final TextLayoutMetrics textLayout; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
final TextEditingValue textEditingValue; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return TextPosition( |
|
|
|
|
offset: textLayout.getWordBoundary(position).start, |
|
|
|
|
// Word boundary seems to always report downstream on many platforms. |
|
|
|
|
affinity: |
|
|
|
|
TextAffinity.downstream, // ignore: avoid_redundant_argument_values |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return TextPosition( |
|
|
|
|
offset: textLayout.getWordBoundary(position).end, |
|
|
|
|
// Word boundary seems to always report downstream on many platforms. |
|
|
|
|
affinity: |
|
|
|
|
TextAffinity.downstream, // ignore: avoid_redundant_argument_values |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// The linebreaks of the current text layout. The input [TextPosition]s are |
|
|
|
|
// interpreted as caret locations because [TextPainter.getLineAtOffset] is |
|
|
|
|
// text-affinity-aware. |
|
|
|
|
class _LineBreak extends _TextBoundary { |
|
|
|
|
const _LineBreak(this.textLayout, this.textEditingValue); |
|
|
|
|
|
|
|
|
|
final TextLayoutMetrics textLayout; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
final TextEditingValue textEditingValue; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return TextPosition( |
|
|
|
|
offset: textLayout.getLineAtOffset(position).start, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return TextPosition( |
|
|
|
|
offset: textLayout.getLineAtOffset(position).end, |
|
|
|
|
affinity: TextAffinity.upstream, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// The document boundary is unique and is a constant function of the input |
|
|
|
|
// position. |
|
|
|
|
class _DocumentBoundary extends _TextBoundary { |
|
|
|
|
const _DocumentBoundary(this.textEditingValue); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
final TextEditingValue textEditingValue; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) => |
|
|
|
|
const TextPosition(offset: 0); |
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return TextPosition( |
|
|
|
|
offset: textEditingValue.text.length, |
|
|
|
|
affinity: TextAffinity.upstream, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ------------------------ Text Boundary Combinators ------------------------ |
|
|
|
|
|
|
|
|
|
// Expands the innerTextBoundary with outerTextBoundary. |
|
|
|
|
class _ExpandedTextBoundary extends _TextBoundary { |
|
|
|
|
_ExpandedTextBoundary(this.innerTextBoundary, this.outerTextBoundary); |
|
|
|
|
|
|
|
|
|
final _TextBoundary innerTextBoundary; |
|
|
|
|
final _TextBoundary outerTextBoundary; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextEditingValue get textEditingValue { |
|
|
|
|
assert(innerTextBoundary.textEditingValue == |
|
|
|
|
outerTextBoundary.textEditingValue); |
|
|
|
|
return innerTextBoundary.textEditingValue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return outerTextBoundary.getLeadingTextBoundaryAt( |
|
|
|
|
innerTextBoundary.getLeadingTextBoundaryAt(position), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return outerTextBoundary.getTrailingTextBoundaryAt( |
|
|
|
|
innerTextBoundary.getTrailingTextBoundaryAt(position), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Force the innerTextBoundary to interpret the input [TextPosition]s as caret |
|
|
|
|
// locations instead of code unit positions. |
|
|
|
|
// |
|
|
|
|
// The innerTextBoundary must be a [_TextBoundary] that interprets the input |
|
|
|
|
// [TextPosition]s as code unit positions. |
|
|
|
|
class _CollapsedSelectionBoundary extends _TextBoundary { |
|
|
|
|
_CollapsedSelectionBoundary(this.innerTextBoundary, this.isForward); |
|
|
|
|
|
|
|
|
|
final _TextBoundary innerTextBoundary; |
|
|
|
|
final bool isForward; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return isForward |
|
|
|
|
? innerTextBoundary.getLeadingTextBoundaryAt(position) |
|
|
|
|
: position.offset <= 0 |
|
|
|
|
? const TextPosition(offset: 0) |
|
|
|
|
: innerTextBoundary.getLeadingTextBoundaryAt( |
|
|
|
|
TextPosition(offset: position.offset - 1)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) { |
|
|
|
|
return isForward |
|
|
|
|
? innerTextBoundary.getTrailingTextBoundaryAt(position) |
|
|
|
|
: position.offset <= 0 |
|
|
|
|
? const TextPosition(offset: 0) |
|
|
|
|
: innerTextBoundary.getTrailingTextBoundaryAt( |
|
|
|
|
TextPosition(offset: position.offset - 1)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// A _TextBoundary that creates a [TextRange] where its start is from the |
|
|
|
|
// specified leading text boundary and its end is from the specified trailing |
|
|
|
|
// text boundary. |
|
|
|
|
class _MixedBoundary extends _TextBoundary { |
|
|
|
|
_MixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary); |
|
|
|
|
|
|
|
|
|
final _TextBoundary leadingTextBoundary; |
|
|
|
|
final _TextBoundary trailingTextBoundary; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextEditingValue get textEditingValue { |
|
|
|
|
assert(leadingTextBoundary.textEditingValue == |
|
|
|
|
trailingTextBoundary.textEditingValue); |
|
|
|
|
return leadingTextBoundary.textEditingValue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getLeadingTextBoundaryAt(TextPosition position) => |
|
|
|
|
leadingTextBoundary.getLeadingTextBoundaryAt(position); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
TextPosition getTrailingTextBoundaryAt(TextPosition position) => |
|
|
|
|
trailingTextBoundary.getTrailingTextBoundaryAt(position); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ------------------------------- Text Actions ------------------------------- |
|
|
|
|
class _DeleteTextAction<T extends DirectionalTextEditingIntent> |
|
|
|
|
extends ContextAction<T> { |
|
|
|
|
_DeleteTextAction(this.state, this.getTextBoundariesForIntent); |
|
|
|
|
|
|
|
|
|
final RawEditorState state; |
|
|
|
|
final _TextBoundary Function(T intent) getTextBoundariesForIntent; |
|
|
|
|
|
|
|
|
|
TextRange _expandNonCollapsedRange(TextEditingValue value) { |
|
|
|
|
final TextRange selection = value.selection; |
|
|
|
|
assert(selection.isValid); |
|
|
|
|
assert(!selection.isCollapsed); |
|
|
|
|
final _TextBoundary atomicBoundary = _CharacterBoundary(value); |
|
|
|
|
|
|
|
|
|
return TextRange( |
|
|
|
|
start: atomicBoundary |
|
|
|
|
.getLeadingTextBoundaryAt(TextPosition(offset: selection.start)) |
|
|
|
|
.offset, |
|
|
|
|
end: atomicBoundary |
|
|
|
|
.getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1)) |
|
|
|
|
.offset, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Object? invoke(T intent, [BuildContext? context]) { |
|
|
|
|
final selection = state.textEditingValue.selection; |
|
|
|
|
assert(selection.isValid); |
|
|
|
|
|
|
|
|
|
if (!selection.isCollapsed) { |
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
ReplaceTextIntent( |
|
|
|
|
state.textEditingValue, |
|
|
|
|
'', |
|
|
|
|
_expandNonCollapsedRange(state.textEditingValue), |
|
|
|
|
SelectionChangedCause.keyboard), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final textBoundary = getTextBoundariesForIntent(intent); |
|
|
|
|
if (!textBoundary.textEditingValue.selection.isValid) { |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
if (!textBoundary.textEditingValue.selection.isCollapsed) { |
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
ReplaceTextIntent( |
|
|
|
|
state.textEditingValue, |
|
|
|
|
'', |
|
|
|
|
_expandNonCollapsedRange(textBoundary.textEditingValue), |
|
|
|
|
SelectionChangedCause.keyboard), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
ReplaceTextIntent( |
|
|
|
|
textBoundary.textEditingValue, |
|
|
|
|
'', |
|
|
|
|
textBoundary |
|
|
|
|
.getTextBoundaryAt(textBoundary.textEditingValue.selection.base), |
|
|
|
|
SelectionChangedCause.keyboard, |
|
|
|
|
), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
bool get isActionEnabled => |
|
|
|
|
!state.widget.readOnly && state.textEditingValue.selection.isValid; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> |
|
|
|
|
extends ContextAction<T> { |
|
|
|
|
_UpdateTextSelectionAction(this.state, this.ignoreNonCollapsedSelection, |
|
|
|
|
this.getTextBoundariesForIntent); |
|
|
|
|
|
|
|
|
|
final RawEditorState state; |
|
|
|
|
final bool ignoreNonCollapsedSelection; |
|
|
|
|
final _TextBoundary Function(T intent) getTextBoundariesForIntent; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Object? invoke(T intent, [BuildContext? context]) { |
|
|
|
|
final selection = state.textEditingValue.selection; |
|
|
|
|
assert(selection.isValid); |
|
|
|
|
|
|
|
|
|
final collapseSelection = |
|
|
|
|
intent.collapseSelection || !state.widget.selectionEnabled; |
|
|
|
|
// Collapse to the logical start/end. |
|
|
|
|
TextSelection _collapse(TextSelection selection) { |
|
|
|
|
assert(selection.isValid); |
|
|
|
|
assert(!selection.isCollapsed); |
|
|
|
|
return selection.copyWith( |
|
|
|
|
baseOffset: intent.forward ? selection.end : selection.start, |
|
|
|
|
extentOffset: intent.forward ? selection.end : selection.start, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!selection.isCollapsed && |
|
|
|
|
!ignoreNonCollapsedSelection && |
|
|
|
|
collapseSelection) { |
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
UpdateSelectionIntent(state.textEditingValue, _collapse(selection), |
|
|
|
|
SelectionChangedCause.keyboard), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final textBoundary = getTextBoundariesForIntent(intent); |
|
|
|
|
final textBoundarySelection = textBoundary.textEditingValue.selection; |
|
|
|
|
if (!textBoundarySelection.isValid) { |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
if (!textBoundarySelection.isCollapsed && |
|
|
|
|
!ignoreNonCollapsedSelection && |
|
|
|
|
collapseSelection) { |
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
UpdateSelectionIntent(state.textEditingValue, |
|
|
|
|
_collapse(textBoundarySelection), SelectionChangedCause.keyboard), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final extent = textBoundarySelection.extent; |
|
|
|
|
final newExtent = intent.forward |
|
|
|
|
? textBoundary.getTrailingTextBoundaryAt(extent) |
|
|
|
|
: textBoundary.getLeadingTextBoundaryAt(extent); |
|
|
|
|
|
|
|
|
|
final newSelection = collapseSelection |
|
|
|
|
? TextSelection.fromPosition(newExtent) |
|
|
|
|
: textBoundarySelection.extendTo(newExtent); |
|
|
|
|
|
|
|
|
|
// If collapseAtReversal is true and would have an effect, collapse it. |
|
|
|
|
if (!selection.isCollapsed && |
|
|
|
|
intent.collapseAtReversal && |
|
|
|
|
(selection.baseOffset < selection.extentOffset != |
|
|
|
|
newSelection.baseOffset < newSelection.extentOffset)) { |
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
UpdateSelectionIntent( |
|
|
|
|
state.textEditingValue, |
|
|
|
|
TextSelection.fromPosition(selection.base), |
|
|
|
|
SelectionChangedCause.keyboard, |
|
|
|
|
), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, |
|
|
|
|
SelectionChangedCause.keyboard), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
bool get isActionEnabled => state.textEditingValue.selection.isValid; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class _ExtendSelectionOrCaretPositionAction extends ContextAction< |
|
|
|
|
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> { |
|
|
|
|
_ExtendSelectionOrCaretPositionAction( |
|
|
|
|
this.state, this.getTextBoundariesForIntent); |
|
|
|
|
|
|
|
|
|
final RawEditorState state; |
|
|
|
|
final _TextBoundary Function( |
|
|
|
|
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) |
|
|
|
|
getTextBoundariesForIntent; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent, |
|
|
|
|
[BuildContext? context]) { |
|
|
|
|
final selection = state.textEditingValue.selection; |
|
|
|
|
assert(selection.isValid); |
|
|
|
|
|
|
|
|
|
final textBoundary = getTextBoundariesForIntent(intent); |
|
|
|
|
final textBoundarySelection = textBoundary.textEditingValue.selection; |
|
|
|
|
if (!textBoundarySelection.isValid) { |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final extent = textBoundarySelection.extent; |
|
|
|
|
final newExtent = intent.forward |
|
|
|
|
? textBoundary.getTrailingTextBoundaryAt(extent) |
|
|
|
|
: textBoundary.getLeadingTextBoundaryAt(extent); |
|
|
|
|
|
|
|
|
|
final newSelection = (newExtent.offset - textBoundarySelection.baseOffset) * |
|
|
|
|
(textBoundarySelection.extentOffset - |
|
|
|
|
textBoundarySelection.baseOffset) < |
|
|
|
|
0 |
|
|
|
|
? textBoundarySelection.copyWith( |
|
|
|
|
extentOffset: textBoundarySelection.baseOffset, |
|
|
|
|
affinity: textBoundarySelection.extentOffset > |
|
|
|
|
textBoundarySelection.baseOffset |
|
|
|
|
? TextAffinity.downstream |
|
|
|
|
: TextAffinity.upstream, |
|
|
|
|
) |
|
|
|
|
: textBoundarySelection.extendTo(newExtent); |
|
|
|
|
|
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, |
|
|
|
|
SelectionChangedCause.keyboard), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
bool get isActionEnabled => |
|
|
|
|
state.widget.selectionEnabled && state.textEditingValue.selection.isValid; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class _UpdateTextSelectionToAdjacentLineAction< |
|
|
|
|
T extends DirectionalCaretMovementIntent> extends ContextAction<T> { |
|
|
|
|
_UpdateTextSelectionToAdjacentLineAction(this.state); |
|
|
|
|
|
|
|
|
|
final RawEditorState state; |
|
|
|
|
|
|
|
|
|
QuillVerticalCaretMovementRun? _verticalMovementRun; |
|
|
|
|
TextSelection? _runSelection; |
|
|
|
|
|
|
|
|
|
void stopCurrentVerticalRunIfSelectionChanges() { |
|
|
|
|
final runSelection = _runSelection; |
|
|
|
|
if (runSelection == null) { |
|
|
|
|
assert(_verticalMovementRun == null); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
_runSelection = state.textEditingValue.selection; |
|
|
|
|
final currentSelection = state.widget.controller.selection; |
|
|
|
|
final continueCurrentRun = currentSelection.isValid && |
|
|
|
|
currentSelection.isCollapsed && |
|
|
|
|
currentSelection.baseOffset == runSelection.baseOffset && |
|
|
|
|
currentSelection.extentOffset == runSelection.extentOffset; |
|
|
|
|
if (!continueCurrentRun) { |
|
|
|
|
_verticalMovementRun = null; |
|
|
|
|
_runSelection = null; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
void invoke(T intent, [BuildContext? context]) { |
|
|
|
|
assert(state.textEditingValue.selection.isValid); |
|
|
|
|
|
|
|
|
|
final collapseSelection = |
|
|
|
|
intent.collapseSelection || !state.widget.selectionEnabled; |
|
|
|
|
final value = state.textEditingValue; |
|
|
|
|
if (!value.selection.isValid) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final currentRun = _verticalMovementRun ?? |
|
|
|
|
state.renderEditor |
|
|
|
|
.startVerticalCaretMovement(state.renderEditor.selection.extent); |
|
|
|
|
|
|
|
|
|
final shouldMove = |
|
|
|
|
intent.forward ? currentRun.moveNext() : currentRun.movePrevious(); |
|
|
|
|
final newExtent = shouldMove |
|
|
|
|
? currentRun.current |
|
|
|
|
: (intent.forward |
|
|
|
|
? TextPosition(offset: state.textEditingValue.text.length) |
|
|
|
|
: const TextPosition(offset: 0)); |
|
|
|
|
final newSelection = collapseSelection |
|
|
|
|
? TextSelection.fromPosition(newExtent) |
|
|
|
|
: value.selection.extendTo(newExtent); |
|
|
|
|
|
|
|
|
|
Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
UpdateSelectionIntent( |
|
|
|
|
value, newSelection, SelectionChangedCause.keyboard), |
|
|
|
|
); |
|
|
|
|
if (state.textEditingValue.selection == newSelection) { |
|
|
|
|
_verticalMovementRun = currentRun; |
|
|
|
|
_runSelection = newSelection; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
bool get isActionEnabled => state.textEditingValue.selection.isValid; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class _SelectAllAction extends ContextAction<SelectAllTextIntent> { |
|
|
|
|
_SelectAllAction(this.state); |
|
|
|
|
|
|
|
|
|
final RawEditorState state; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) { |
|
|
|
|
return Actions.invoke( |
|
|
|
|
context!, |
|
|
|
|
UpdateSelectionIntent( |
|
|
|
|
state.textEditingValue, |
|
|
|
|
TextSelection( |
|
|
|
|
baseOffset: 0, extentOffset: state.textEditingValue.text.length), |
|
|
|
|
intent.cause, |
|
|
|
|
), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
bool get isActionEnabled => state.widget.selectionEnabled; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> { |
|
|
|
|
_CopySelectionAction(this.state); |
|
|
|
|
|
|
|
|
|
final RawEditorState state; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
void invoke(CopySelectionTextIntent intent, [BuildContext? context]) { |
|
|
|
|
if (intent.collapseSelection) { |
|
|
|
|
state.cutSelection(intent.cause); |
|
|
|
|
} else { |
|
|
|
|
state.copySelection(intent.cause); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
bool get isActionEnabled => |
|
|
|
|
state.textEditingValue.selection.isValid && |
|
|
|
|
!state.textEditingValue.selection.isCollapsed; |
|
|
|
|
} |
|
|
|
|