Use actions to fix showing handler

pull/670/head
X Code 3 years ago
parent 22ab369c02
commit f6223e062a
  1. 37
      lib/src/widgets/editor.dart
  2. 738
      lib/src/widgets/raw_editor.dart
  3. 99
      lib/src/widgets/shortcut.dart

@ -1544,6 +1544,14 @@ class RenderEditor extends RenderEditableContainerBox
// End TextLayoutMetrics implementation
QuillVerticalCaretMovementRun startVerticalCaretMovement(
TextPosition startPosition) {
return QuillVerticalCaretMovementRun._(
this,
startPosition,
);
}
@override
void systemFontsDidChange() {
super.systemFontsDidChange();
@ -1551,6 +1559,35 @@ class RenderEditor extends RenderEditableContainerBox
}
}
class QuillVerticalCaretMovementRun
extends BidirectionalIterator<TextPosition> {
QuillVerticalCaretMovementRun._(
this._editor,
this._currentTextPosition,
);
TextPosition _currentTextPosition;
final RenderEditor _editor;
@override
TextPosition get current {
return _currentTextPosition;
}
@override
bool moveNext() {
_currentTextPosition = _editor.getTextPositionBelow(_currentTextPosition);
return true;
}
@override
bool movePrevious() {
_currentTextPosition = _editor.getTextPositionAbove(_currentTextPosition);
return true;
}
}
class EditableContainerParentData
extends ContainerBoxParentData<RenderEditableBox> {}

@ -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;
}

@ -0,0 +1,99 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/documents/attribute.dart';
import 'raw_editor.dart';
class QuillShortcuts extends Shortcuts {
QuillShortcuts({required Widget child, Key? key})
: super(
key: key,
shortcuts: _shortcuts,
child: child,
);
static Map<ShortcutActivator, Intent> get _shortcuts {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return _defaultShortcuts;
case TargetPlatform.fuchsia:
return _defaultShortcuts;
case TargetPlatform.iOS:
return _macShortcuts;
case TargetPlatform.linux:
return _defaultShortcuts;
case TargetPlatform.macOS:
return _macShortcuts;
case TargetPlatform.windows:
return _defaultShortcuts;
}
}
static const Map<ShortcutActivator, Intent> _defaultShortcuts =
<ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.keyB, control: true):
ToggleBoldStyleIntent(),
SingleActivator(LogicalKeyboardKey.keyI, control: true):
ToggleItalicStyleIntent(),
SingleActivator(LogicalKeyboardKey.keyU, control: true):
ToggleUnderlineStyleIntent(),
};
static final Map<ShortcutActivator, Intent> _macShortcuts =
<ShortcutActivator, Intent>{
const SingleActivator(LogicalKeyboardKey.keyB, meta: true):
const ToggleBoldStyleIntent(),
const SingleActivator(LogicalKeyboardKey.keyI, meta: true):
const ToggleItalicStyleIntent(),
const SingleActivator(LogicalKeyboardKey.keyU, meta: true):
const ToggleUnderlineStyleIntent(),
};
}
class ToggleBoldStyleIntent extends Intent {
const ToggleBoldStyleIntent();
}
class ToggleItalicStyleIntent extends Intent {
const ToggleItalicStyleIntent();
}
class ToggleUnderlineStyleIntent extends Intent {
const ToggleUnderlineStyleIntent();
}
class QuillActions extends Actions {
QuillActions({
required Widget child,
Key? key,
}) : super(
key: key,
actions: _shortcutsActions,
child: child,
);
static final Map<Type, Action<Intent>> _shortcutsActions =
<Type, Action<Intent>>{
ToggleBoldStyleIntent: _ToggleInlineStyleAction(Attribute.bold),
ToggleItalicStyleIntent: _ToggleInlineStyleAction(Attribute.italic),
ToggleUnderlineStyleIntent: _ToggleInlineStyleAction(Attribute.underline),
};
}
class _ToggleInlineStyleAction extends ContextAction<Intent> {
_ToggleInlineStyleAction(this.attribute);
final Attribute attribute;
@override
Object? invoke(Intent intent, [BuildContext? context]) {
final editorState = context!.findAncestorStateOfType<RawEditorState>()!;
final style = editorState.controller.getSelectionStyle();
final actualAttr = style.containsKey(attribute.key)
? Attribute.clone(attribute, null)
: attribute;
editorState.controller.formatSelection(actualAttr);
return null;
}
}
Loading…
Cancel
Save