From e7d3391cdbb7a2197891800cd9819443ff6555b0 Mon Sep 17 00:00:00 2001 From: Ellet Date: Wed, 23 Oct 2024 19:17:43 +0300 Subject: [PATCH] fix(macos): Implement actions for ExpandSelectionToDocumentBoundaryIntent and ExpandSelectionToLineBreakIntent to use keyboard shortcuts, unrelated cleanup to the bug fix. (#2279) --- .../default_single_activator_intents.dart | 179 +++++++++++++++ .../editor_keyboard_shortcut_actions.dart} | 111 ++++++++- ...tor_keyboard_shortcut_actions_manager.dart | 204 +++++++++++++++++ .../editor_keyboard_shortcuts.dart} | 47 ++-- .../editor/raw_editor/raw_editor_state.dart | 212 +++--------------- .../default_single_activator_actions.dart | 206 ----------------- 6 files changed, 537 insertions(+), 422 deletions(-) create mode 100644 lib/src/editor/raw_editor/keyboard_shortcuts/default_single_activator_intents.dart rename lib/src/editor/raw_editor/{raw_editor_actions.dart => keyboard_shortcuts/editor_keyboard_shortcut_actions.dart} (87%) create mode 100644 lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions_manager.dart rename lib/src/editor/{widgets/keyboard_service_widget.dart => raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart} (86%) delete mode 100644 lib/src/editor/widgets/default_single_activator_actions.dart diff --git a/lib/src/editor/raw_editor/keyboard_shortcuts/default_single_activator_intents.dart b/lib/src/editor/raw_editor/keyboard_shortcuts/default_single_activator_intents.dart new file mode 100644 index 000000000..3d0aeb49c --- /dev/null +++ b/lib/src/editor/raw_editor/keyboard_shortcuts/default_single_activator_intents.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +import '../../../common/utils/platform.dart'; +import '../../../document/attribute.dart'; +import 'editor_keyboard_shortcut_actions.dart'; + +final _isDesktopMacOS = isMacOS; + +@internal +Map defaultSinlgeActivatorIntents() { + return { + const SingleActivator( + LogicalKeyboardKey.escape, + ): const HideSelectionToolbarIntent(), + SingleActivator( + LogicalKeyboardKey.keyZ, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const UndoTextIntent(SelectionChangedCause.keyboard), + SingleActivator( + LogicalKeyboardKey.keyY, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const RedoTextIntent(SelectionChangedCause.keyboard), + + // Selection formatting. + SingleActivator( + LogicalKeyboardKey.keyB, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.bold), + SingleActivator( + LogicalKeyboardKey.keyU, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.underline), + SingleActivator( + LogicalKeyboardKey.keyI, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.italic), + SingleActivator( + LogicalKeyboardKey.keyS, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.strikeThrough), + SingleActivator( + LogicalKeyboardKey.backquote, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.inlineCode), + SingleActivator( + LogicalKeyboardKey.tilde, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.codeBlock), + SingleActivator( + LogicalKeyboardKey.keyB, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.blockQuote), + SingleActivator( + LogicalKeyboardKey.keyK, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyLinkIntent(), + + // Lists + SingleActivator( + LogicalKeyboardKey.keyL, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.ul), + SingleActivator( + LogicalKeyboardKey.keyO, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.ol), + SingleActivator( + LogicalKeyboardKey.keyC, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + shift: true, + ): const QuillEditorApplyCheckListIntent(), + + // Indents + SingleActivator( + LogicalKeyboardKey.keyM, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const IndentSelectionIntent(true), + SingleActivator( + LogicalKeyboardKey.keyM, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + shift: true, + ): const IndentSelectionIntent(false), + + // Headers + SingleActivator( + LogicalKeyboardKey.digit1, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h1), + SingleActivator( + LogicalKeyboardKey.digit2, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h2), + SingleActivator( + LogicalKeyboardKey.digit3, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h3), + SingleActivator( + LogicalKeyboardKey.digit4, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h4), + SingleActivator( + LogicalKeyboardKey.digit5, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h5), + SingleActivator( + LogicalKeyboardKey.digit6, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h6), + SingleActivator( + LogicalKeyboardKey.digit0, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.header), + + SingleActivator( + LogicalKeyboardKey.keyG, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const QuillEditorInsertEmbedIntent(Attribute.image), + + SingleActivator( + LogicalKeyboardKey.keyF, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const OpenSearchIntent(), + + // Arrow key scrolling + SingleActivator( + LogicalKeyboardKey.arrowUp, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ScrollIntent(direction: AxisDirection.up), + SingleActivator( + LogicalKeyboardKey.arrowDown, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ScrollIntent(direction: AxisDirection.down), + SingleActivator( + LogicalKeyboardKey.pageUp, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ScrollIntent( + direction: AxisDirection.up, type: ScrollIncrementType.page), + SingleActivator( + LogicalKeyboardKey.pageDown, + control: !_isDesktopMacOS, + meta: _isDesktopMacOS, + ): const ScrollIntent( + direction: AxisDirection.down, type: ScrollIncrementType.page), + }; +} diff --git a/lib/src/editor/raw_editor/raw_editor_actions.dart b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions.dart similarity index 87% rename from lib/src/editor/raw_editor/raw_editor_actions.dart rename to lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions.dart index e9cec3af6..b87f42484 100644 --- a/lib/src/editor/raw_editor/raw_editor_actions.dart +++ b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import '../../../translations.dart'; -import '../../document/attribute.dart'; -import '../../document/style.dart'; -import '../../toolbar/buttons/link_style2_button.dart'; -import '../../toolbar/buttons/search/search_dialog.dart'; -import '../editor.dart'; -import '../widgets/link.dart'; -import 'raw_editor_state.dart'; -import 'raw_editor_text_boundaries.dart'; +import '../../../../translations.dart'; +import '../../../document/attribute.dart'; +import '../../../document/style.dart'; +import '../../../toolbar/buttons/link_style2_button.dart'; +import '../../../toolbar/buttons/search/search_dialog.dart'; +import '../../editor.dart'; +import '../../widgets/link.dart'; +import '../raw_editor_state.dart'; +import '../raw_editor_text_boundaries.dart'; // ------------------------------- Text Actions ------------------------------- class QuillEditorDeleteTextAction @@ -268,6 +268,98 @@ class QuillEditorExtendSelectionOrCaretPositionAction extends ContextAction< state.textEditingValue.selection.isValid; } +/// Expands the selection to the start/end of the document. +/// +/// This matches macOS behavior and differs from [ExpandSelectionToLineBreakIntent]. +/// +/// See: [ExpandSelectionToDocumentBoundaryIntent]. +class ExpandSelectionToDocumentBoundaryAction + extends ContextAction { + ExpandSelectionToDocumentBoundaryAction(this.state); + + final QuillRawEditorState state; + + @override + Object? invoke(ExpandSelectionToDocumentBoundaryIntent intent, + [BuildContext? context]) { + final currentSelection = state.controller.selection; + final documentLength = state.controller.document.length; + + final newSelection = intent.forward + ? currentSelection.copyWith( + extentOffset: documentLength, + ) + : currentSelection.copyWith( + extentOffset: 0, + ); + return Actions.invoke( + context ?? (throw StateError('BuildContext should not be null.')), + UpdateSelectionIntent( + state.textEditingValue, + newSelection, + SelectionChangedCause.keyboard, + ), + ); + } +} + +/// Extends the selection to the next/previous line break (`\n`). +/// +/// This behavior is standard on macOS. +/// +/// See: [ExpandSelectionToLineBreakIntent] +class ExpandSelectionToLineBreakAction + extends ContextAction { + ExpandSelectionToLineBreakAction(this.state); + + final QuillRawEditorState state; + @override + Object? invoke(ExpandSelectionToLineBreakIntent intent, + [BuildContext? context]) { + // Plain text of the document (needed to find line breaks) + final text = state.controller.plainTextEditingValue.text; + + final currentSelection = state.controller.selection; + + // Calculate the next or previous line break based on direction + final searchStartOffset = currentSelection.extentOffset; + + final targetLineBreak = () { + if (intent.forward) { + final nextLineBreak = text.indexOf('\n', searchStartOffset); + final noNextLineBreak = nextLineBreak == -1; + return noNextLineBreak ? text.length : nextLineBreak + 1; + } + + // Backward + + // Ensure (searchStartOffset - 1) is not negative to avoid [RangeError] + final safePreviousSearchOffset = + (searchStartOffset > 0) ? (searchStartOffset - 1) : 0; + + final previousLineBreak = + text.lastIndexOf('\n', safePreviousSearchOffset); + + final noPreviousLineBreak = previousLineBreak == -1; + return noPreviousLineBreak ? 0 : previousLineBreak; + }(); + + // Create a new selection, extending it to the line break was found + final newSelection = currentSelection.copyWith( + extentOffset: targetLineBreak, + ); + + return Actions.invoke( + context ?? (throw StateError('BuildContext should not be null.')), + UpdateSelectionIntent( + state.textEditingValue, + newSelection, + SelectionChangedCause.keyboard, + ), + ); + } +} + class QuillEditorUpdateTextSelectionToAdjacentLineAction< T extends DirectionalCaretMovementIntent> extends ContextAction { QuillEditorUpdateTextSelectionToAdjacentLineAction(this.state); @@ -627,6 +719,7 @@ class QuillEditorInsertEmbedIntent extends Intent { final Attribute type; } +/// Navigate to the start or end of the document class NavigateToDocumentBoundaryAction extends ContextAction { NavigateToDocumentBoundaryAction(this.state); diff --git a/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions_manager.dart b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions_manager.dart new file mode 100644 index 000000000..bb03c2900 --- /dev/null +++ b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions_manager.dart @@ -0,0 +1,204 @@ +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + +import '../raw_editor_state.dart'; +import '../raw_editor_text_boundaries.dart'; +import 'editor_keyboard_shortcut_actions.dart'; + +@internal +class EditorKeyboardShortcutsActionsManager { + EditorKeyboardShortcutsActionsManager({ + required this.rawEditorState, + required this.context, + }); + + final QuillRawEditorState rawEditorState; + final BuildContext context; + + void _updateSelection(UpdateSelectionIntent intent) { + rawEditorState.userUpdateTextEditingValue( + intent.currentTextEditingValue.copyWith(selection: intent.newSelection), + intent.cause, + ); + } + + QuillEditorTextBoundary _characterBoundary( + DirectionalTextEditingIntent intent) { + final atomicTextBoundary = + QuillEditorCharacterBoundary(rawEditorState.textEditingValue); + return QuillEditorCollapsedSelectionBoundary( + atomicTextBoundary, intent.forward); + } + + QuillEditorTextBoundary _nextWordBoundary( + DirectionalTextEditingIntent intent) { + final QuillEditorTextBoundary atomicTextBoundary; + final QuillEditorTextBoundary boundary; + + // final TextEditingValue textEditingValue = + // _textEditingValueForTextLayoutMetrics; + atomicTextBoundary = + QuillEditorCharacterBoundary(rawEditorState.textEditingValue); + // This isn't enough. Newline characters. + boundary = QuillEditorExpandedTextBoundary( + QuillEditorWhitespaceBoundary(rawEditorState.textEditingValue), + QuillEditorWordBoundary( + rawEditorState.renderEditor, rawEditorState.textEditingValue)); + + final mixedBoundary = intent.forward + ? QuillEditorMixedBoundary(atomicTextBoundary, boundary) + : QuillEditorMixedBoundary(boundary, atomicTextBoundary); + // Use a _MixedBoundary to make sure we don't leave invalid codepoints in + // the field after deletion. + return QuillEditorCollapsedSelectionBoundary(mixedBoundary, intent.forward); + } + + QuillEditorTextBoundary _linebreak(DirectionalTextEditingIntent intent) { + final QuillEditorTextBoundary atomicTextBoundary; + final QuillEditorTextBoundary boundary; + + // final TextEditingValue textEditingValue = + // _textEditingValueforTextLayoutMetrics; + atomicTextBoundary = + QuillEditorCharacterBoundary(rawEditorState.textEditingValue); + boundary = QuillEditorLineBreak( + rawEditorState.renderEditor, rawEditorState.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 + ? QuillEditorMixedBoundary( + QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), + boundary) + : QuillEditorMixedBoundary( + boundary, + QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, false), + ); + } + + void _replaceText(ReplaceTextIntent intent) { + rawEditorState.userUpdateTextEditingValue( + intent.currentTextEditingValue + .replaced(intent.replacementRange, intent.replacementText), + intent.cause, + ); + } + + late final Action _replaceTextAction = + CallbackAction(onInvoke: _replaceText); + + QuillEditorTextBoundary _documentBoundary( + DirectionalTextEditingIntent intent) => + QuillEditorDocumentBoundary(rawEditorState.textEditingValue); + + Action _makeOverridable(Action defaultAction) { + return Action.overridable( + context: context, defaultAction: defaultAction); + } + + late final Action _updateSelectionAction = + CallbackAction(onInvoke: _updateSelection); + + late final QuillEditorUpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent> adjacentLineAction = + QuillEditorUpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent>(rawEditorState); + + late final _adjacentPageAction = + QuillEditorUpdateTextSelectionToAdjacentPageAction< + ExtendSelectionVerticallyToAdjacentPageIntent>(rawEditorState); + + late final QuillEditorToggleTextStyleAction _formatSelectionAction = + QuillEditorToggleTextStyleAction(rawEditorState); + + late final QuillEditorIndentSelectionAction _indentSelectionAction = + QuillEditorIndentSelectionAction(rawEditorState); + + late final QuillEditorOpenSearchAction _openSearchAction = + QuillEditorOpenSearchAction(rawEditorState); + late final QuillEditorApplyHeaderAction _applyHeaderAction = + QuillEditorApplyHeaderAction(rawEditorState); + late final QuillEditorApplyCheckListAction _applyCheckListAction = + QuillEditorApplyCheckListAction(rawEditorState); + + late final Map> _actions = >{ + DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), + ReplaceTextIntent: _replaceTextAction, + UpdateSelectionIntent: _updateSelectionAction, + DirectionalFocusIntent: DirectionalFocusAction.forTextField(), + + // Delete + DeleteCharacterIntent: _makeOverridable( + QuillEditorDeleteTextAction( + rawEditorState, _characterBoundary)), + DeleteToNextWordBoundaryIntent: _makeOverridable( + QuillEditorDeleteTextAction( + rawEditorState, _nextWordBoundary)), + DeleteToLineBreakIntent: _makeOverridable( + QuillEditorDeleteTextAction( + rawEditorState, _linebreak)), + + // Extend/Move Selection + ExtendSelectionByCharacterIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction( + rawEditorState, + false, + _characterBoundary, + )), + ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToNextWordBoundaryIntent>( + rawEditorState, true, _nextWordBoundary)), + ExtendSelectionToLineBreakIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction( + rawEditorState, true, _linebreak)), + ExtendSelectionVerticallyToAdjacentLineIntent: + _makeOverridable(adjacentLineAction), + ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToDocumentBoundaryIntent>( + rawEditorState, true, _documentBoundary)), + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( + QuillEditorExtendSelectionOrCaretPositionAction( + rawEditorState, _nextWordBoundary)), + ExpandSelectionToDocumentBoundaryIntent: _makeOverridable( + ExpandSelectionToDocumentBoundaryAction(rawEditorState)), + ExpandSelectionToLineBreakIntent: + _makeOverridable(ExpandSelectionToLineBreakAction(rawEditorState)), + + // Copy Paste + SelectAllTextIntent: + _makeOverridable(QuillEditorSelectAllAction(rawEditorState)), + CopySelectionTextIntent: + _makeOverridable(QuillEditorCopySelectionAction(rawEditorState)), + PasteTextIntent: _makeOverridable(CallbackAction( + onInvoke: (intent) => rawEditorState.pasteText(intent.cause))), + + HideSelectionToolbarIntent: + _makeOverridable(QuillEditorHideSelectionToolbarAction(rawEditorState)), + UndoTextIntent: + _makeOverridable(QuillEditorUndoKeyboardAction(rawEditorState)), + RedoTextIntent: + _makeOverridable(QuillEditorRedoKeyboardAction(rawEditorState)), + + OpenSearchIntent: _openSearchAction, + + // Selection Formatting + ToggleTextStyleIntent: _formatSelectionAction, + IndentSelectionIntent: _indentSelectionAction, + QuillEditorApplyHeaderIntent: _applyHeaderAction, + QuillEditorApplyCheckListIntent: _applyCheckListAction, + QuillEditorApplyLinkIntent: QuillEditorApplyLinkAction(rawEditorState), + ScrollToDocumentBoundaryIntent: + NavigateToDocumentBoundaryAction(rawEditorState), + + // Paging and scrolling + ExtendSelectionVerticallyToAdjacentPageIntent: _adjacentPageAction, + ScrollIntent: QuillEditorScrollAction(rawEditorState), + }; + + Map> get actions => _actions; +} diff --git a/lib/src/editor/widgets/keyboard_service_widget.dart b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart similarity index 86% rename from lib/src/editor/widgets/keyboard_service_widget.dart rename to lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart index 1231fe830..336726628 100644 --- a/lib/src/editor/widgets/keyboard_service_widget.dart +++ b/lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcuts.dart @@ -1,22 +1,23 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import '../../common/utils/cast.dart'; -import '../../common/utils/platform.dart'; -import '../../controller/quill_controller.dart'; -import '../../document/attribute.dart'; -import '../../document/document.dart'; -import '../../document/nodes/block.dart'; -import '../../document/nodes/leaf.dart' as leaf; -import '../../document/nodes/line.dart'; -import '../raw_editor/config/events/character_shortcuts_events.dart'; -import '../raw_editor/config/events/space_shortcut_events.dart'; -import 'default_single_activator_actions.dart'; -import 'keyboard_listener.dart'; - -class QuillKeyboardServiceWidget extends StatelessWidget { - const QuillKeyboardServiceWidget({ +import 'package:meta/meta.dart'; + +import '../../../common/utils/cast.dart'; +import '../../../controller/quill_controller.dart'; +import '../../../document/attribute.dart'; +import '../../../document/document.dart'; +import '../../../document/nodes/block.dart'; +import '../../../document/nodes/leaf.dart' as leaf; +import '../../../document/nodes/line.dart'; +import '../../widgets/keyboard_listener.dart'; +import '../config/events/character_shortcuts_events.dart'; +import '../config/events/space_shortcut_events.dart'; +import 'default_single_activator_intents.dart'; + +@internal +class EditorKeyboardShortcuts extends StatelessWidget { + const EditorKeyboardShortcuts({ required this.actions, required this.constraints, required this.focusNode, @@ -45,17 +46,19 @@ class QuillKeyboardServiceWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final isDesktopMacOS = isMacOS; return Shortcuts( /// Merge with widget.configurations.customShortcuts /// first to allow user's defined shortcuts to take /// priority when activation triggers are the same - shortcuts: mergeMaps({...?customShortcuts}, - {...defaultSinlgeActivatorActions(isDesktopMacOS)}), + shortcuts: mergeMaps( + {...?customShortcuts}, + {...defaultSinlgeActivatorIntents()}, + ), child: Actions( - actions: mergeMaps>(actions, { - ...?customActions, - }), + actions: mergeMaps>( + actions, + {...?customActions}, + ), child: Focus( focusNode: focusNode, onKeyEvent: _onKeyEvent, diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index 6c16dae22..a1902164e 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -29,18 +29,17 @@ import '../../editor_toolbar_controller_shared/clipboard/clipboard_service_provi import '../editor.dart'; import '../widgets/cursor.dart'; import '../widgets/default_styles.dart'; -import '../widgets/keyboard_service_widget.dart'; import '../widgets/link.dart'; import '../widgets/proxy.dart'; import '../widgets/text/text_block.dart'; import '../widgets/text/text_line.dart'; import '../widgets/text/text_selection.dart'; +import 'keyboard_shortcuts/editor_keyboard_shortcut_actions_manager.dart'; +import 'keyboard_shortcuts/editor_keyboard_shortcuts.dart'; import 'raw_editor.dart'; -import 'raw_editor_actions.dart'; import 'raw_editor_render_object.dart'; import 'raw_editor_state_selection_delegate_mixin.dart'; import 'raw_editor_state_text_input_client_mixin.dart'; -import 'raw_editor_text_boundaries.dart'; import 'scribble_focusable.dart'; class QuillRawEditorState extends EditorState @@ -50,6 +49,8 @@ class QuillRawEditorState extends EditorState TickerProviderStateMixin, RawEditorStateTextInputClientMixin, RawEditorStateSelectionDelegateMixin { + late final EditorKeyboardShortcutsActionsManager _shortcutActionsManager; + final GlobalKey _editorKey = GlobalKey(); KeyboardVisibilityController? _keyboardVisibilityController; @@ -530,8 +531,8 @@ class QuillRawEditorState extends EditorState }, child: QuillStyles( data: _styles!, - child: QuillKeyboardServiceWidget( - actions: _actions, + child: EditorKeyboardShortcuts( + actions: _shortcutActionsManager.actions, characterEvents: widget.configurations.characterShortcutEvents, spaceEvents: widget.configurations.spaceShortcutEvents, constraints: constraints, @@ -833,6 +834,11 @@ class QuillRawEditorState extends EditorState @override void initState() { super.initState(); + _shortcutActionsManager = EditorKeyboardShortcutsActionsManager( + rawEditorState: this, + context: context, + ); + if (_clipboardStatus != null) { _clipboardStatus!.addListener(_onChangedClipboardStatus); } @@ -1047,7 +1053,8 @@ class QuillRawEditorState extends EditorState } } - _adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges(); + _shortcutActionsManager.adjacentLineAction + .stopCurrentVerticalRunIfSelectionChanges(); } void _onChangeTextEditingValue([bool ignoreCaret = false]) { @@ -1278,14 +1285,6 @@ class QuillRawEditorState extends EditorState } } - void _replaceText(ReplaceTextIntent intent) { - userUpdateTextEditingValue( - intent.currentTextEditingValue - .replaced(intent.replacementRange, intent.replacementText), - intent.cause, - ); - } - @override bool get wantKeepAlive => widget.configurations.focusNode.hasFocus; @@ -1295,171 +1294,6 @@ class QuillRawEditorState extends EditorState late AnimationController _floatingCursorResetController; - // --------------------------- Text Editing Actions -------------------------- - - QuillEditorTextBoundary _characterBoundary( - DirectionalTextEditingIntent intent) { - final atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - return QuillEditorCollapsedSelectionBoundary( - atomicTextBoundary, intent.forward); - } - - QuillEditorTextBoundary _nextWordBoundary( - DirectionalTextEditingIntent intent) { - final QuillEditorTextBoundary atomicTextBoundary; - final QuillEditorTextBoundary boundary; - - // final TextEditingValue textEditingValue = - // _textEditingValueForTextLayoutMetrics; - atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - // This isn't enough. Newline characters. - boundary = QuillEditorExpandedTextBoundary( - QuillEditorWhitespaceBoundary(textEditingValue), - QuillEditorWordBoundary(renderEditor, textEditingValue)); - - final mixedBoundary = intent.forward - ? QuillEditorMixedBoundary(atomicTextBoundary, boundary) - : QuillEditorMixedBoundary(boundary, atomicTextBoundary); - // Use a _MixedBoundary to make sure we don't leave invalid codepoints in - // the field after deletion. - return QuillEditorCollapsedSelectionBoundary(mixedBoundary, intent.forward); - } - - QuillEditorTextBoundary _linebreak(DirectionalTextEditingIntent intent) { - final QuillEditorTextBoundary atomicTextBoundary; - final QuillEditorTextBoundary boundary; - - // final TextEditingValue textEditingValue = - // _textEditingValueforTextLayoutMetrics; - atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - boundary = QuillEditorLineBreak(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 - ? QuillEditorMixedBoundary( - QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), - boundary) - : QuillEditorMixedBoundary( - boundary, - QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, false), - ); - } - - QuillEditorTextBoundary _documentBoundary( - DirectionalTextEditingIntent intent) => - QuillEditorDocumentBoundary(textEditingValue); - - Action _makeOverridable(Action defaultAction) { - return Action.overridable( - context: context, defaultAction: defaultAction); - } - - late final Action _replaceTextAction = - CallbackAction(onInvoke: _replaceText); - - void _updateSelection(UpdateSelectionIntent intent) { - userUpdateTextEditingValue( - intent.currentTextEditingValue.copyWith(selection: intent.newSelection), - intent.cause, - ); - } - - late final Action _updateSelectionAction = - CallbackAction(onInvoke: _updateSelection); - - late final QuillEditorUpdateTextSelectionToAdjacentLineAction< - ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = - QuillEditorUpdateTextSelectionToAdjacentLineAction< - ExtendSelectionVerticallyToAdjacentLineIntent>(this); - - late final _adjacentPageAction = - QuillEditorUpdateTextSelectionToAdjacentPageAction< - ExtendSelectionVerticallyToAdjacentPageIntent>(this); - - late final QuillEditorToggleTextStyleAction _formatSelectionAction = - QuillEditorToggleTextStyleAction(this); - - late final QuillEditorIndentSelectionAction _indentSelectionAction = - QuillEditorIndentSelectionAction(this); - - late final QuillEditorOpenSearchAction _openSearchAction = - QuillEditorOpenSearchAction(this); - late final QuillEditorApplyHeaderAction _applyHeaderAction = - QuillEditorApplyHeaderAction(this); - late final QuillEditorApplyCheckListAction _applyCheckListAction = - QuillEditorApplyCheckListAction(this); - - late final Map> _actions = >{ - DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), - ReplaceTextIntent: _replaceTextAction, - UpdateSelectionIntent: _updateSelectionAction, - DirectionalFocusIntent: DirectionalFocusAction.forTextField(), - - // Delete - DeleteCharacterIntent: _makeOverridable( - QuillEditorDeleteTextAction( - this, _characterBoundary)), - DeleteToNextWordBoundaryIntent: _makeOverridable( - QuillEditorDeleteTextAction( - this, _nextWordBoundary)), - DeleteToLineBreakIntent: _makeOverridable( - QuillEditorDeleteTextAction(this, _linebreak)), - - // Extend/Move Selection - ExtendSelectionByCharacterIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction( - this, - false, - _characterBoundary, - )), - ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction< - ExtendSelectionToNextWordBoundaryIntent>( - this, true, _nextWordBoundary)), - ExtendSelectionToLineBreakIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction( - this, true, _linebreak)), - ExtendSelectionVerticallyToAdjacentLineIntent: - _makeOverridable(_adjacentLineAction), - ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction< - ExtendSelectionToDocumentBoundaryIntent>( - this, true, _documentBoundary)), - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( - QuillEditorExtendSelectionOrCaretPositionAction( - this, _nextWordBoundary)), - - // Copy Paste - SelectAllTextIntent: _makeOverridable(QuillEditorSelectAllAction(this)), - CopySelectionTextIntent: - _makeOverridable(QuillEditorCopySelectionAction(this)), - PasteTextIntent: _makeOverridable(CallbackAction( - onInvoke: (intent) => pasteText(intent.cause))), - - HideSelectionToolbarIntent: - _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), - UndoTextIntent: _makeOverridable(QuillEditorUndoKeyboardAction(this)), - RedoTextIntent: _makeOverridable(QuillEditorRedoKeyboardAction(this)), - - OpenSearchIntent: _openSearchAction, - - // Selection Formatting - ToggleTextStyleIntent: _formatSelectionAction, - IndentSelectionIntent: _indentSelectionAction, - QuillEditorApplyHeaderIntent: _applyHeaderAction, - QuillEditorApplyCheckListIntent: _applyCheckListAction, - QuillEditorApplyLinkIntent: QuillEditorApplyLinkAction(this), - ScrollToDocumentBoundaryIntent: NavigateToDocumentBoundaryAction(this), - - // Paging and scrolling - ExtendSelectionVerticallyToAdjacentPageIntent: _adjacentPageAction, - ScrollIntent: QuillEditorScrollAction(this), - }; - @override void insertTextPlaceholder(Size size) { // this is needed for Scribble (Stylus input) in Apple platforms @@ -1480,16 +1314,24 @@ class QuillRawEditorState extends EditorState // TODO: implement didChangeInputControl } + /// macOS-specific method that should not be called on other platforms. + /// This method interacts with the `NSStandardKeyBindingResponding` protocol + /// from Cocoa, which is available only on macOS systems. @override void performSelector(String selectorName) { + assert( + isMacOSApp, + 'Should call performSelector() only on macOS desktop platform.', + ); final intent = intentForMacOSSelector(selectorName); - - if (intent != null) { - final primaryContext = primaryFocus?.context; - if (primaryContext != null) { - Actions.invoke(primaryContext, intent); - } + if (intent == null) { + return; + } + final primaryContext = primaryFocus?.context; + if (primaryContext == null) { + return; } + Actions.invoke(primaryContext, intent); } @override diff --git a/lib/src/editor/widgets/default_single_activator_actions.dart b/lib/src/editor/widgets/default_single_activator_actions.dart deleted file mode 100644 index 4eb927100..000000000 --- a/lib/src/editor/widgets/default_single_activator_actions.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:flutter/material.dart' - show - AxisDirection, - Intent, - RedoTextIntent, - ScrollIncrementType, - ScrollIntent, - ScrollToDocumentBoundaryIntent, - SelectionChangedCause, - SingleActivator, - UndoTextIntent; -import 'package:flutter/services.dart' - show LogicalKeyboardKey, SelectionChangedCause; - -import '../../document/attribute.dart'; -import '../raw_editor/raw_editor_actions.dart' - show - HideSelectionToolbarIntent, - IndentSelectionIntent, - OpenSearchIntent, - QuillEditorApplyCheckListIntent, - QuillEditorApplyHeaderIntent, - QuillEditorApplyLinkIntent, - QuillEditorInsertEmbedIntent, - ToggleTextStyleIntent; - -Map defaultSinlgeActivatorActions( - bool isDesktopMacOS) => - { - const SingleActivator( - LogicalKeyboardKey.escape, - ): const HideSelectionToolbarIntent(), - SingleActivator( - LogicalKeyboardKey.keyZ, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const UndoTextIntent(SelectionChangedCause.keyboard), - SingleActivator( - LogicalKeyboardKey.keyY, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const RedoTextIntent(SelectionChangedCause.keyboard), - - // Selection formatting. - SingleActivator( - LogicalKeyboardKey.keyB, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.bold), - SingleActivator( - LogicalKeyboardKey.keyU, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.underline), - SingleActivator( - LogicalKeyboardKey.keyI, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.italic), - SingleActivator( - LogicalKeyboardKey.keyS, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.strikeThrough), - SingleActivator( - LogicalKeyboardKey.backquote, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.inlineCode), - SingleActivator( - LogicalKeyboardKey.tilde, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.codeBlock), - SingleActivator( - LogicalKeyboardKey.keyB, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.blockQuote), - SingleActivator( - LogicalKeyboardKey.keyK, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyLinkIntent(), - - // Lists - SingleActivator( - LogicalKeyboardKey.keyL, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.ul), - SingleActivator( - LogicalKeyboardKey.keyO, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.ol), - SingleActivator( - LogicalKeyboardKey.keyC, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const QuillEditorApplyCheckListIntent(), - - // Indents - SingleActivator( - LogicalKeyboardKey.keyM, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const IndentSelectionIntent(true), - SingleActivator( - LogicalKeyboardKey.keyM, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const IndentSelectionIntent(false), - - // Headers - SingleActivator( - LogicalKeyboardKey.digit1, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h1), - SingleActivator( - LogicalKeyboardKey.digit2, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h2), - SingleActivator( - LogicalKeyboardKey.digit3, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h3), - SingleActivator( - LogicalKeyboardKey.digit4, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h4), - SingleActivator( - LogicalKeyboardKey.digit5, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h5), - SingleActivator( - LogicalKeyboardKey.digit6, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h6), - SingleActivator( - LogicalKeyboardKey.digit0, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.header), - - SingleActivator( - LogicalKeyboardKey.keyG, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorInsertEmbedIntent(Attribute.image), - - SingleActivator( - LogicalKeyboardKey.keyF, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const OpenSearchIntent(), - - // Navigate to the start or end of the document - SingleActivator( - LogicalKeyboardKey.home, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ScrollToDocumentBoundaryIntent(forward: false), - SingleActivator( - LogicalKeyboardKey.end, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ScrollToDocumentBoundaryIntent(forward: true), - - // Arrow key scrolling - SingleActivator( - LogicalKeyboardKey.arrowUp, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ScrollIntent(direction: AxisDirection.up), - SingleActivator( - LogicalKeyboardKey.arrowDown, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ScrollIntent(direction: AxisDirection.down), - SingleActivator( - LogicalKeyboardKey.pageUp, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ScrollIntent( - direction: AxisDirection.up, type: ScrollIncrementType.page), - SingleActivator( - LogicalKeyboardKey.pageDown, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ScrollIntent( - direction: AxisDirection.down, type: ScrollIncrementType.page), - };