From 43a0a02328c48a4b00985c09d24c6ef5a738f0c8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 5 Sep 2022 22:16:28 +0800 Subject: [PATCH 1/7] feat: keyboard service improvement --- .../appflowy_editor/example/lib/main.dart | 4 + ...nderscore_to_italic_key_event_handler.dart | 8 +- .../appflowy_editor/lib/appflowy_editor.dart | 3 + .../lib/src/service/editor_service.dart | 9 +- .../arrow_keys_handler.dart | 2 +- .../backspace_handler.dart | 24 +- .../copy_paste_handler.dart | 3 +- .../default_key_event_handlers.dart | 25 - ...er_without_shift_in_text_node_handler.dart | 4 +- .../legacy/arrow_keys_handler.dart | 3 +- .../page_up_down_handler.dart | 4 +- .../redo_undo_handler.dart | 4 +- .../select_all_handler.dart | 3 +- .../slash_handler.dart | 4 +- ...pdate_text_style_by_command_x_handler.dart | 6 +- .../whitespace_handler.dart | 4 +- .../lib/src/service/keyboard_service.dart | 44 +- .../built_in_shortcut_events.dart | 121 +++++ .../service/shortcut_event/key_mapping.dart | 451 ++++++++++++++++++ .../service/shortcut_event/keybinding.dart | 153 ++++++ .../shortcut_event/shortcut_event.dart | 108 +++++ .../shortcut_event_handler.dart | 7 + .../arrow_keys_handler_test.dart | 66 ++- .../redo_undo_handler_test.dart | 35 +- .../select_all_handler_test.dart | 8 +- ..._text_style_by_command_x_handler_test.dart | 112 +++-- 26 files changed, 1085 insertions(+), 130 deletions(-) delete mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/key_mapping.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/keybinding.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event_handler.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 4bd1cb1972..80e1813e89 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:example/plugin/underscore_to_italic_key_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -95,6 +96,9 @@ class _MyHomePageState extends State { child: AppFlowyEditor( editorState: _editorState, editorStyle: const EditorStyle.defaultStyle(), + keyEventHandlers: [ + underscoreToItalicEvent, + ], ), ); } else { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart index 539a756e1f..13ee27af05 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart @@ -2,7 +2,13 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -AppFlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) { +ShortcutEvent underscoreToItalicEvent = ShortcutEvent( + key: 'Underscore to italic', + command: 'shift+underscore', + handler: _underscoreToItalicHandler, +); + +ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { // Since we only need to handler the input of `underscore`. // All inputs except `underscore` will be ignored directly. if (event.logicalKey != LogicalKeyboardKey.underscore) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index a77f3fe7eb..60f63a66e2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -23,3 +23,6 @@ export 'src/service/scroll_service.dart'; export 'src/service/toolbar_service.dart'; export 'src/service/keyboard_service.dart'; export 'src/service/input_service.dart'; +export 'src/service/shortcut_event/keybinding.dart'; +export 'src/service/shortcut_event/shortcut_event.dart'; +export 'src/service/shortcut_event/shortcut_event_handler.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 3a8d75560b..9465d44de4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -1,7 +1,8 @@ import 'package:appflowy_editor/src/render/image/image_node_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/render/style/editor_style.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/src/editor_state.dart'; @@ -46,7 +47,7 @@ class AppFlowyEditor extends StatefulWidget { final NodeWidgetBuilders customBuilders; /// Keyboard event handlers. - final List keyEventHandlers; + final List keyEventHandlers; final List selectionMenuItems; @@ -93,8 +94,8 @@ class _AppFlowyEditorState extends State { editorState: editorState, child: AppFlowyKeyboard( key: editorState.service.keyboardServiceKey, - handlers: [ - ...defaultKeyEventHandlers, + shortcutEvents: [ + ...builtInShortcutEvents, ...widget.keyEventHandlers, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index a19541da0f..d5a840c4df 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -3,7 +3,7 @@ import 'package:appflowy_editor/src/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) { +ShortcutEventHandler arrowKeysHandler = (editorState, event) { if (!_arrowKeys.contains(event.logicalKey)) { return KeyEventResult.ignored; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 675ee5b446..ee3f80f197 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -4,6 +4,18 @@ import 'package:flutter/services.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +// Handle delete text. +ShortcutEventHandler deleteTextHandler = (editorState, event) { + if (event.logicalKey == LogicalKeyboardKey.backspace) { + return _handleBackspace(editorState, event); + } + if (event.logicalKey == LogicalKeyboardKey.delete) { + return _handleDelete(editorState, event); + } + + return KeyEventResult.ignored; +}; + KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { var selection = editorState.service.selectionService.currentSelection.value; if (selection == null) { @@ -159,15 +171,3 @@ void _deleteTextNodes(TransactionBuilder transactionBuilder, secondOffset: selection.end.offset, ); } - -// Handle delete text. -AppFlowyKeyEventHandler deleteTextHandler = (editorState, event) { - if (event.logicalKey == LogicalKeyboardKey.backspace) { - return _handleBackspace(editorState, event); - } - if (event.logicalKey == LogicalKeyboardKey.delete) { - return _handleDelete(editorState, event); - } - - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index f6dba97c49..6a4f58fa86 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,6 +1,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/infra/html_converter.dart'; import 'package:appflowy_editor/src/document/node_iterator.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; @@ -303,7 +304,7 @@ _deleteSelectedContent(EditorState editorState) { tb.commit(); } -AppFlowyKeyEventHandler copyPasteKeysHandler = (editorState, event) { +ShortcutEventHandler copyPasteKeysHandler = (editorState, event) { if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyC) { _handleCopy(editorState); return KeyEventResult.handled; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart deleted file mode 100644 index f8c7fa71cb..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart'; -import 'package:appflowy_editor/src/service/keyboard_service.dart'; - -List defaultKeyEventHandlers = [ - deleteTextHandler, - slashShortcutHandler, - // arrowKeysHandler, - arrowKeysHandler, - copyPasteKeysHandler, - redoUndoKeysHandler, - enterWithoutShiftInTextNodesHandler, - updateTextStyleByCommandXHandler, - whiteSpaceHandler, - selectAllHandler, - pageUpDownHandler, -]; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 04540b8754..8f12dbb9e6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -8,7 +8,7 @@ import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/extensions/path_extensions.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; -import 'package:appflowy_editor/src/service/keyboard_service.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; /// Handle some cases where enter is pressed and shift is not pressed. /// @@ -18,7 +18,7 @@ import 'package:appflowy_editor/src/service/keyboard_service.dart'; /// 2. Single selection and the selected node is [TextNode] /// 2.1 split the node into two nodes with style /// 2.2 or insert a empty text node before. -AppFlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = +ShortcutEventHandler enterWithoutShiftInTextNodesHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.enter || event.isShiftPressed) { return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart index ecaf3325e4..56150020e2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -103,7 +104,7 @@ KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { return KeyEventResult.ignored; } -AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) { +ShortcutEventHandler arrowKeysHandler = (editorState, event) { if (event.isShiftPressed) { return _handleShiftKey(editorState, event); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart index d33e290d8e..b5f7afe885 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart @@ -1,8 +1,8 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; -AppFlowyKeyEventHandler pageUpDownHandler = (editorState, event) { +ShortcutEventHandler pageUpDownHandler = (editorState, event) { if (event.logicalKey == LogicalKeyboardKey.pageUp) { final scrollHeight = editorState.service.scrollService?.onePageHeight; final scrollService = editorState.service.scrollService; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart index 268697640a..7bec895f91 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart @@ -1,8 +1,8 @@ -import 'package:appflowy_editor/src/service/keyboard_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; -AppFlowyKeyEventHandler redoUndoKeysHandler = (editorState, event) { +ShortcutEventHandler redoUndoKeysHandler = (editorState, event) { if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyZ) { if (event.isShiftPressed) { editorState.undoManager.redo(); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart index 8ffd2d176e..6f8e1ae750 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,7 +19,7 @@ KeyEventResult _selectAll(EditorState editorState) { return KeyEventResult.handled; } -AppFlowyKeyEventHandler selectAllHandler = (editorState, event) { +ShortcutEventHandler selectAllHandler = (editorState, event) { if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyA) { return _selectAll(editorState); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 79b7ee6579..50e72c25ab 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -1,13 +1,13 @@ import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; -import 'package:appflowy_editor/src/service/keyboard_service.dart'; import 'package:appflowy_editor/src/extensions/node_extensions.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; SelectionMenuService? _selectionMenuService; -AppFlowyKeyEventHandler slashShortcutHandler = (editorState, event) { +ShortcutEventHandler slashShortcutHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.slash) { return KeyEventResult.ignored; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 00b304f527..19d50f80cf 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -1,12 +1,12 @@ +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; -import 'package:appflowy_editor/src/service/keyboard_service.dart'; + import 'package:flutter/services.dart'; -AppFlowyKeyEventHandler updateTextStyleByCommandXHandler = - (editorState, event) { +ShortcutEventHandler updateTextStyleByCommandXHandler = (editorState, event) { if (!event.isMetaPressed) { return KeyEventResult.ignored; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index 68e706128c..c6046ba6dd 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -7,7 +8,6 @@ import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; -import 'package:appflowy_editor/src/service/keyboard_service.dart'; @visibleForTesting List get checkboxListSymbols => _checkboxListSymbols; @@ -20,7 +20,7 @@ const _bulletedListSymbols = ['*', '-']; const _checkboxListSymbols = ['[x]', '-[x]']; const _unCheckboxListSymbols = ['[]', '-[]']; -AppFlowyKeyEventHandler whiteSpaceHandler = (editorState, event) { +ShortcutEventHandler whiteSpaceHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.space) { return KeyEventResult.ignored; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart index dee3f42725..4b511c2ca4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -22,6 +22,9 @@ abstract class AppFlowyKeyboardService { /// Processes shortcut key input. KeyEventResult onKey(RawKeyEvent event); + /// Gets the shortcut events + List get shortcutEvents; + /// Enables shortcuts service. void enable(); @@ -35,23 +38,18 @@ abstract class AppFlowyKeyboardService { void disable(); } -typedef AppFlowyKeyEventHandler = KeyEventResult Function( - EditorState editorState, - RawKeyEvent event, -); - /// Process keyboard events class AppFlowyKeyboard extends StatefulWidget { const AppFlowyKeyboard({ Key? key, - required this.handlers, + required this.shortcutEvents, required this.editorState, required this.child, }) : super(key: key); final EditorState editorState; final Widget child; - final List handlers; + final List shortcutEvents; @override State createState() => _AppFlowyKeyboardState(); @@ -63,6 +61,10 @@ class _AppFlowyKeyboardState extends State bool isFocus = true; + @override + // TODO: implement shortcutEvents + List get shortcutEvents => widget.shortcutEvents; + @override Widget build(BuildContext context) { return Focus( @@ -111,16 +113,14 @@ class _AppFlowyKeyboardState extends State return KeyEventResult.ignored; } - for (final handler in widget.handlers) { - KeyEventResult result = handler(widget.editorState, event); - - switch (result) { - case KeyEventResult.handled: + // TODO: use cache to optimize the searching time. + for (final shortcutEvent in widget.shortcutEvents) { + if (shortcutEvent.keybindings.containsKeyEvent(event)) { + final result = shortcutEvent.handler(widget.editorState, event); + if (result == KeyEventResult.handled) { return KeyEventResult.handled; - case KeyEventResult.skipRemainingHandlers: - return KeyEventResult.skipRemainingHandlers; - case KeyEventResult.ignored: - continue; + } + continue; } } @@ -139,3 +139,15 @@ class _AppFlowyKeyboardState extends State return onKey(event); } } + +extension on RawKeyEvent { + Keybinding get toKeybinding { + return Keybinding( + isAltPressed: isAltPressed, + isControlPressed: isControlPressed, + isMetaPressed: isMetaPressed, + isShiftPressed: isShiftPressed, + keyLabel: logicalKey.keyLabel, + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart new file mode 100644 index 0000000000..bfd1b97d6f --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -0,0 +1,121 @@ +// List<> + +import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; + +// +List builtInShortcutEvents = [ + ShortcutEvent( + key: 'Move cursor up', + command: 'arrow up', + handler: arrowKeysHandler, + ), + ShortcutEvent( + key: 'Move cursor down', + command: 'arrow down', + handler: arrowKeysHandler, + ), + ShortcutEvent( + key: 'Move cursor left', + command: 'arrow left', + handler: arrowKeysHandler, + ), + ShortcutEvent( + key: 'Move cursor right', + command: 'arrow right', + handler: arrowKeysHandler, + ), + // TODO: split the keys. + ShortcutEvent( + key: 'Shift + Arrow Keys', + command: + 'shift+arrow up,shift+arrow down,shift+arrow left,shift+arrow right', + handler: arrowKeysHandler, + ), + ShortcutEvent( + key: 'Control + Arrow Keys', + command: 'meta+arrow up,meta+arrow down,meta+arrow left,meta+arrow right', + windowsCommand: + 'ctrl+arrow up,ctrl+arrow down,ctrl+arrow left,ctrl+arrow right', + macOSCommand: 'cmd+arrow up,cmd+arrow down,cmd+arrow left,cmd+arrow right', + handler: arrowKeysHandler, + ), + ShortcutEvent( + key: 'Meta + Shift + Arrow Keys', + command: + 'meta+shift+arrow up,meta+shift+arrow down,meta+shift+arrow left,meta+shift+arrow right', + windowsCommand: + 'ctrl+shift+arrow up,ctrl+shift+arrow down,ctrl+shift+arrow left,ctrl+shift+arrow right', + macOSCommand: + 'cmd+shift+arrow up,cmd+shift+arrow down,cmd+shift+arrow left,cmd+shift+arrow right', + handler: arrowKeysHandler, + ), + ShortcutEvent( + key: 'Meta + Shift + Arrow Keys', + command: + 'meta+shift+arrow up,meta+shift+arrow down,meta+shift+arrow left,meta+shift+arrow right', + windowsCommand: + 'ctrl+shift+arrow up,ctrl+shift+arrow down,ctrl+shift+arrow left,ctrl+shift+arrow right', + macOSCommand: + 'cmd+shift+arrow up,cmd+shift+arrow down,cmd+shift+arrow left,cmd+shift+arrow right', + handler: arrowKeysHandler, + ), + ShortcutEvent( + key: 'Delete Text', + command: 'delete,backspace', + handler: deleteTextHandler, + ), + ShortcutEvent( + key: 'selection menu', + command: 'slash', + handler: slashShortcutHandler, + ), + ShortcutEvent( + key: 'copy / paste', + command: 'meta+c,meta+v', + windowsCommand: 'ctrl+c,ctrl+v', + handler: copyPasteKeysHandler, + ), + ShortcutEvent( + key: 'redo / undo', + command: 'meta+z,meta+meta+shift+z', + windowsCommand: 'ctrl+z,meta+ctrl+shift+z', + handler: redoUndoKeysHandler, + ), + ShortcutEvent( + key: 'enter', + command: 'enter', + handler: enterWithoutShiftInTextNodesHandler, + ), + ShortcutEvent( + key: 'update text style', + command: 'meta+b,meta+i,meta+u,meta+shift+s,meta+shift+h,meta+k', + windowsCommand: 'ctrl+b,ctrl+i,ctrl+u,ctrl+shift+s,ctrl+shift+h,ctrl+k', + handler: updateTextStyleByCommandXHandler, + ), + ShortcutEvent( + key: 'markdown', + command: 'space', + handler: whiteSpaceHandler, + ), + ShortcutEvent( + key: 'select all', + command: 'meta+a', + windowsCommand: 'ctrl+a', + handler: selectAllHandler, + ), + ShortcutEvent( + key: 'page up / page down', + command: 'page up,page down', + handler: pageUpDownHandler, + ), +]; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/key_mapping.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/key_mapping.dart new file mode 100644 index 0000000000..bddbd09711 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/key_mapping.dart @@ -0,0 +1,451 @@ +/// Keyboard key to keycode mapping table +/// +/// Copy from flutter project, keyboard_key.dart. +/// + +Map keyToCodeMapping = { + 'Space': 0x00000000020, + 'Exclamation': 0x00000000021, + 'Quote': 0x00000000022, + 'Number Sign': 0x00000000023, + 'Dollar': 0x00000000024, + 'Percent': 0x00000000025, + 'Ampersand': 0x00000000026, + 'Quote Single': 0x00000000027, + 'Parenthesis Left': 0x00000000028, + 'Parenthesis Right': 0x00000000029, + 'Asterisk': 0x0000000002a, + 'Add': 0x0000000002b, + 'Comma': 0x0000000002c, + 'Minus': 0x0000000002d, + 'Period': 0x0000000002e, + 'Slash': 0x0000000002f, + 'Digit 0': 0x00000000030, + 'Digit 1': 0x00000000031, + 'Digit 2': 0x00000000032, + 'Digit 3': 0x00000000033, + 'Digit 4': 0x00000000034, + 'Digit 5': 0x00000000035, + 'Digit 6': 0x00000000036, + 'Digit 7': 0x00000000037, + 'Digit 8': 0x00000000038, + 'Digit 9': 0x00000000039, + 'Colon': 0x0000000003a, + 'Semicolon': 0x0000000003b, + 'Less': 0x0000000003c, + 'Equal': 0x0000000003d, + 'Greater': 0x0000000003e, + 'Question': 0x0000000003f, + 'At': 0x00000000040, + 'Bracket Left': 0x0000000005b, + 'Backslash': 0x0000000005c, + 'Bracket Right': 0x0000000005d, + 'Caret': 0x0000000005e, + 'Underscore': 0x0000000005f, + 'Backquote': 0x00000000060, + 'A': 0x00000000061, + 'B': 0x00000000062, + 'C': 0x00000000063, + 'D': 0x00000000064, + 'E': 0x00000000065, + 'F': 0x00000000066, + 'G': 0x00000000067, + 'H': 0x00000000068, + 'I': 0x00000000069, + 'J': 0x0000000006a, + 'K': 0x0000000006b, + 'L': 0x0000000006c, + 'M': 0x0000000006d, + 'N': 0x0000000006e, + 'O': 0x0000000006f, + 'P': 0x00000000070, + 'Q': 0x00000000071, + 'R': 0x00000000072, + 'S': 0x00000000073, + 'T': 0x00000000074, + 'U': 0x00000000075, + 'V': 0x00000000076, + 'W': 0x00000000077, + 'X': 0x00000000078, + 'Y': 0x00000000079, + 'Z': 0x0000000007a, + 'Brace Left': 0x0000000007b, + 'Bar': 0x0000000007c, + 'Brace Right': 0x0000000007d, + 'Tilde': 0x0000000007e, + 'Unidentified': 0x00100000001, + 'Backspace': 0x00100000008, + 'Tab': 0x00100000009, + 'Enter': 0x0010000000d, + 'Escape': 0x0010000001b, + 'Delete': 0x0010000007f, + 'Accel': 0x00100000101, + 'Alt Graph': 0x00100000103, + 'Caps Lock': 0x00100000104, + 'Fn': 0x00100000106, + 'Fn Lock': 0x00100000107, + 'Hyper': 0x00100000108, + 'Num Lock': 0x0010000010a, + 'Scroll Lock': 0x0010000010c, + 'Super': 0x0010000010e, + 'Symbol': 0x0010000010f, + 'Symbol Lock': 0x00100000110, + 'Shift Level 5': 0x00100000111, + 'Arrow Down': 0x00100000301, + 'Arrow Left': 0x00100000302, + 'Arrow Right': 0x00100000303, + 'Arrow Up': 0x00100000304, + 'End': 0x00100000305, + 'Home': 0x00100000306, + 'Page Down': 0x00100000307, + 'Page Up': 0x00100000308, + 'Clear': 0x00100000401, + 'Copy': 0x00100000402, + 'Cr Sel': 0x00100000403, + 'Cut': 0x00100000404, + 'Erase Eof': 0x00100000405, + 'Ex Sel': 0x00100000406, + 'Insert': 0x00100000407, + 'Paste': 0x00100000408, + 'Redo': 0x00100000409, + 'Undo': 0x0010000040a, + 'Accept': 0x00100000501, + 'Again': 0x00100000502, + 'Attn': 0x00100000503, + 'Cancel': 0x00100000504, + 'Context Menu': 0x00100000505, + 'Execute': 0x00100000506, + 'Find': 0x00100000507, + 'Help': 0x00100000508, + 'Pause': 0x00100000509, + 'Play': 0x0010000050a, + 'Props': 0x0010000050b, + 'Select': 0x0010000050c, + 'Zoom In': 0x0010000050d, + 'Zoom Out': 0x0010000050e, + 'Brightness Down': 0x00100000601, + 'Brightness Up': 0x00100000602, + 'Camera': 0x00100000603, + 'Eject': 0x00100000604, + 'Log Off': 0x00100000605, + 'Power': 0x00100000606, + 'Power Off': 0x00100000607, + 'Print Screen': 0x00100000608, + 'Hibernate': 0x00100000609, + 'Standby': 0x0010000060a, + 'Wake Up': 0x0010000060b, + 'All Candidates': 0x00100000701, + 'Alphanumeric': 0x00100000702, + 'Code Input': 0x00100000703, + 'Compose': 0x00100000704, + 'Convert': 0x00100000705, + 'Final Mode': 0x00100000706, + 'Group First': 0x00100000707, + 'Group Last': 0x00100000708, + 'Group Next': 0x00100000709, + 'Group Previous': 0x0010000070a, + 'Mode Change': 0x0010000070b, + 'Next Candidate': 0x0010000070c, + 'Non Convert': 0x0010000070d, + 'Previous Candidate': 0x0010000070e, + 'Process': 0x0010000070f, + 'Single Candidate': 0x00100000710, + 'Hangul Mode': 0x00100000711, + 'Hanja Mode': 0x00100000712, + 'Junja Mode': 0x00100000713, + 'Eisu': 0x00100000714, + 'Hankaku': 0x00100000715, + 'Hiragana': 0x00100000716, + 'Hiragana Katakana': 0x00100000717, + 'Kana Mode': 0x00100000718, + 'Kanji Mode': 0x00100000719, + 'Katakana': 0x0010000071a, + 'Romaji': 0x0010000071b, + 'Zenkaku': 0x0010000071c, + 'Zenkaku Hankaku': 0x0010000071d, + 'F1': 0x00100000801, + 'F2': 0x00100000802, + 'F3': 0x00100000803, + 'F4': 0x00100000804, + 'F5': 0x00100000805, + 'F6': 0x00100000806, + 'F7': 0x00100000807, + 'F8': 0x00100000808, + 'F9': 0x00100000809, + 'F10': 0x0010000080a, + 'F11': 0x0010000080b, + 'F12': 0x0010000080c, + 'F13': 0x0010000080d, + 'F14': 0x0010000080e, + 'F15': 0x0010000080f, + 'F16': 0x00100000810, + 'F17': 0x00100000811, + 'F18': 0x00100000812, + 'F19': 0x00100000813, + 'F20': 0x00100000814, + 'F21': 0x00100000815, + 'F22': 0x00100000816, + 'F23': 0x00100000817, + 'F24': 0x00100000818, + 'Soft 1': 0x00100000901, + 'Soft 2': 0x00100000902, + 'Soft 3': 0x00100000903, + 'Soft 4': 0x00100000904, + 'Soft 5': 0x00100000905, + 'Soft 6': 0x00100000906, + 'Soft 7': 0x00100000907, + 'Soft 8': 0x00100000908, + 'Close': 0x00100000a01, + 'Mail Forward': 0x00100000a02, + 'Mail Reply': 0x00100000a03, + 'Mail Send': 0x00100000a04, + 'Media Play Pause': 0x00100000a05, + 'Media Stop': 0x00100000a07, + 'Media Track Next': 0x00100000a08, + 'Media Track Previous': 0x00100000a09, + 'New': 0x00100000a0a, + 'Open': 0x00100000a0b, + 'Print': 0x00100000a0c, + 'Save': 0x00100000a0d, + 'Spell Check': 0x00100000a0e, + 'Audio Volume Down': 0x00100000a0f, + 'Audio Volume Up': 0x00100000a10, + 'Audio Volume Mute': 0x00100000a11, + 'Launch Application 2': 0x00100000b01, + 'Launch Calendar': 0x00100000b02, + 'Launch Mail': 0x00100000b03, + 'Launch Media Player': 0x00100000b04, + 'Launch Music Player': 0x00100000b05, + 'Launch Application 1': 0x00100000b06, + 'Launch Screen Saver': 0x00100000b07, + 'Launch Spreadsheet': 0x00100000b08, + 'Launch Web Browser': 0x00100000b09, + 'Launch Web Cam': 0x00100000b0a, + 'Launch Word Processor': 0x00100000b0b, + 'Launch Contacts': 0x00100000b0c, + 'Launch Phone': 0x00100000b0d, + 'Launch Assistant': 0x00100000b0e, + 'Launch Control Panel': 0x00100000b0f, + 'Browser Back': 0x00100000c01, + 'Browser Favorites': 0x00100000c02, + 'Browser Forward': 0x00100000c03, + 'Browser Home': 0x00100000c04, + 'Browser Refresh': 0x00100000c05, + 'Browser Search': 0x00100000c06, + 'Browser Stop': 0x00100000c07, + 'Audio Balance Left': 0x00100000d01, + 'Audio Balance Right': 0x00100000d02, + 'Audio Bass Boost Down': 0x00100000d03, + 'Audio Bass Boost Up': 0x00100000d04, + 'Audio Fader Front': 0x00100000d05, + 'Audio Fader Rear': 0x00100000d06, + 'Audio Surround Mode Next': 0x00100000d07, + 'AVR Input': 0x00100000d08, + 'AVR Power': 0x00100000d09, + 'Channel Down': 0x00100000d0a, + 'Channel Up': 0x00100000d0b, + 'Color F0 Red': 0x00100000d0c, + 'Color F1 Green': 0x00100000d0d, + 'Color F2 Yellow': 0x00100000d0e, + 'Color F3 Blue': 0x00100000d0f, + 'Color F4 Grey': 0x00100000d10, + 'Color F5 Brown': 0x00100000d11, + 'Closed Caption Toggle': 0x00100000d12, + 'Dimmer': 0x00100000d13, + 'Display Swap': 0x00100000d14, + 'Exit': 0x00100000d15, + 'Favorite Clear 0': 0x00100000d16, + 'Favorite Clear 1': 0x00100000d17, + 'Favorite Clear 2': 0x00100000d18, + 'Favorite Clear 3': 0x00100000d19, + 'Favorite Recall 0': 0x00100000d1a, + 'Favorite Recall 1': 0x00100000d1b, + 'Favorite Recall 2': 0x00100000d1c, + 'Favorite Recall 3': 0x00100000d1d, + 'Favorite Store 0': 0x00100000d1e, + 'Favorite Store 1': 0x00100000d1f, + 'Favorite Store 2': 0x00100000d20, + 'Favorite Store 3': 0x00100000d21, + 'Guide': 0x00100000d22, + 'Guide Next Day': 0x00100000d23, + 'Guide Previous Day': 0x00100000d24, + 'Info': 0x00100000d25, + 'Instant Replay': 0x00100000d26, + 'Link': 0x00100000d27, + 'List Program': 0x00100000d28, + 'Live Content': 0x00100000d29, + 'Lock': 0x00100000d2a, + 'Media Apps': 0x00100000d2b, + 'Media Fast Forward': 0x00100000d2c, + 'Media Last': 0x00100000d2d, + 'Media Pause': 0x00100000d2e, + 'Media Play': 0x00100000d2f, + 'Media Record': 0x00100000d30, + 'Media Rewind': 0x00100000d31, + 'Media Skip': 0x00100000d32, + 'Next Favorite Channel': 0x00100000d33, + 'Next User Profile': 0x00100000d34, + 'On Demand': 0x00100000d35, + 'P In P Down': 0x00100000d36, + 'P In P Move': 0x00100000d37, + 'P In P Toggle': 0x00100000d38, + 'P In P Up': 0x00100000d39, + 'Play Speed Down': 0x00100000d3a, + 'Play Speed Reset': 0x00100000d3b, + 'Play Speed Up': 0x00100000d3c, + 'Random Toggle': 0x00100000d3d, + 'Rc Low Battery': 0x00100000d3e, + 'Record Speed Next': 0x00100000d3f, + 'Rf Bypass': 0x00100000d40, + 'Scan Channels Toggle': 0x00100000d41, + 'Screen Mode Next': 0x00100000d42, + 'Settings': 0x00100000d43, + 'Split Screen Toggle': 0x00100000d44, + 'STB Input': 0x00100000d45, + 'STB Power': 0x00100000d46, + 'Subtitle': 0x00100000d47, + 'Teletext': 0x00100000d48, + 'TV': 0x00100000d49, + 'TV Input': 0x00100000d4a, + 'TV Power': 0x00100000d4b, + 'Video Mode Next': 0x00100000d4c, + 'Wink': 0x00100000d4d, + 'Zoom Toggle': 0x00100000d4e, + 'DVR': 0x00100000d4f, + 'Media Audio Track': 0x00100000d50, + 'Media Skip Backward': 0x00100000d51, + 'Media Skip Forward': 0x00100000d52, + 'Media Step Backward': 0x00100000d53, + 'Media Step Forward': 0x00100000d54, + 'Media Top Menu': 0x00100000d55, + 'Navigate In': 0x00100000d56, + 'Navigate Next': 0x00100000d57, + 'Navigate Out': 0x00100000d58, + 'Navigate Previous': 0x00100000d59, + 'Pairing': 0x00100000d5a, + 'Media Close': 0x00100000d5b, + 'Audio Bass Boost Toggle': 0x00100000e02, + 'Audio Treble Down': 0x00100000e04, + 'Audio Treble Up': 0x00100000e05, + 'Microphone Toggle': 0x00100000e06, + 'Microphone Volume Down': 0x00100000e07, + 'Microphone Volume Up': 0x00100000e08, + 'Microphone Volume Mute': 0x00100000e09, + 'Speech Correction List': 0x00100000f01, + 'Speech Input Toggle': 0x00100000f02, + 'App Switch': 0x00100001001, + 'Call': 0x00100001002, + 'Camera Focus': 0x00100001003, + 'End Call': 0x00100001004, + 'Go Back': 0x00100001005, + 'Go Home': 0x00100001006, + 'Headset Hook': 0x00100001007, + 'Last Number Redial': 0x00100001008, + 'Notification': 0x00100001009, + 'Manner Mode': 0x0010000100a, + 'Voice Dial': 0x0010000100b, + 'TV 3 D Mode': 0x00100001101, + 'TV Antenna Cable': 0x00100001102, + 'TV Audio Description': 0x00100001103, + 'TV Audio Description Mix Dow': 0x00100001104, + 'TV Audio Description Mix Up': 0x00100001105, + 'TV Contents Menu': 0x00100001106, + 'TV Data Service': 0x00100001107, + 'TV Input Component 1': 0x00100001108, + 'TV Input Component 2': 0x00100001109, + 'TV Input Composite 1': 0x0010000110a, + 'TV Input Composite 2': 0x0010000110b, + 'TV Input HDMI 1': 0x0010000110c, + 'TV Input HDMI 2': 0x0010000110d, + 'TV Input HDMI 3': 0x0010000110e, + 'TV Input HDMI 4': 0x0010000110f, + 'TV Input VGA 1': 0x00100001110, + 'TV Media Context': 0x00100001111, + 'TV Network': 0x00100001112, + 'TV Number Entry': 0x00100001113, + 'TV Radio Service': 0x00100001114, + 'TV Satellite': 0x00100001115, + 'TV Satellite BS': 0x00100001116, + 'TV Satellite CS': 0x00100001117, + 'TV Satellite Toggle': 0x00100001118, + 'TV Terrestrial Analog': 0x00100001119, + 'TV Terrestrial Digital': 0x0010000111a, + 'TV Timer': 0x0010000111b, + 'Key 11': 0x00100001201, + 'Key 12': 0x00100001202, + 'Suspend': 0x00200000000, + 'Resume': 0x00200000001, + 'Sleep': 0x00200000002, + 'Abort': 0x00200000003, + 'Lang 1': 0x00200000010, + 'Lang 2': 0x00200000011, + 'Lang 3': 0x00200000012, + 'Lang 4': 0x00200000013, + 'Lang 5': 0x00200000014, + 'Intl Backslash': 0x00200000020, + 'Intl Ro': 0x00200000021, + 'Intl Yen': 0x00200000022, + 'Control Left': 0x00200000100, + 'Control Right': 0x00200000101, + 'Shift Left': 0x00200000102, + 'Shift Right': 0x00200000103, + 'Alt Left': 0x00200000104, + 'Alt Right': 0x00200000105, + 'Meta Left': 0x00200000106, + 'Meta Right': 0x00200000107, + 'Control': 0x002000001f0, + 'Shift': 0x002000001f2, + 'Alt': 0x002000001f4, + 'Meta': 0x002000001f6, + 'Numpad Enter': 0x0020000020d, + 'Numpad Paren Left': 0x00200000228, + 'Numpad Paren Right': 0x00200000229, + 'Numpad Multiply': 0x0020000022a, + 'Numpad Add': 0x0020000022b, + 'Numpad Comma': 0x0020000022c, + 'Numpad Subtract': 0x0020000022d, + 'Numpad Decimal': 0x0020000022e, + 'Numpad Divide': 0x0020000022f, + 'Numpad 0': 0x00200000230, + 'Numpad 1': 0x00200000231, + 'Numpad 2': 0x00200000232, + 'Numpad 3': 0x00200000233, + 'Numpad 4': 0x00200000234, + 'Numpad 5': 0x00200000235, + 'Numpad 6': 0x00200000236, + 'Numpad 7': 0x00200000237, + 'Numpad 8': 0x00200000238, + 'Numpad 9': 0x00200000239, + 'Numpad Equal': 0x0020000023d, + 'Game Button 1': 0x00200000301, + 'Game Button 2': 0x00200000302, + 'Game Button 3': 0x00200000303, + 'Game Button 4': 0x00200000304, + 'Game Button 5': 0x00200000305, + 'Game Button 6': 0x00200000306, + 'Game Button 7': 0x00200000307, + 'Game Button 8': 0x00200000308, + 'Game Button 9': 0x00200000309, + 'Game Button 10': 0x0020000030a, + 'Game Button 11': 0x0020000030b, + 'Game Button 12': 0x0020000030c, + 'Game Button 13': 0x0020000030d, + 'Game Button 14': 0x0020000030e, + 'Game Button 15': 0x0020000030f, + 'Game Button 16': 0x00200000310, + 'Game Button A': 0x00200000311, + 'Game Button B': 0x00200000312, + 'Game Button C': 0x00200000313, + 'Game Button Left 1': 0x00200000314, + 'Game Button Left 2': 0x00200000315, + 'Game Button Mode': 0x00200000316, + 'Game Button Right 1': 0x00200000317, + 'Game Button Right 2': 0x00200000318, + 'Game Button Select': 0x00200000319, + 'Game Button Start': 0x0020000031a, + 'Game Button Thumb Left': 0x0020000031b, + 'Game Button Thumb Right': 0x0020000031c, + 'Game Button X': 0x0020000031d, + 'Game Button Y': 0x0020000031e, + 'Game Button Z': 0x0020000031f, +}.map((key, value) => MapEntry(key.toLowerCase(), value)); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/keybinding.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/keybinding.dart new file mode 100644 index 0000000000..f346514f7c --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/keybinding.dart @@ -0,0 +1,153 @@ +import 'dart:convert'; + +import 'package:appflowy_editor/src/service/shortcut_event/key_mapping.dart'; +import 'package:flutter/material.dart'; + +extension KeybindingsExtension on List { + bool containsKeyEvent(RawKeyEvent keyEvent) { + for (final keybinding in this) { + if (keybinding.isMetaPressed == keyEvent.isMetaPressed && + keybinding.isControlPressed == keyEvent.isControlPressed && + keybinding.isAltPressed == keyEvent.isAltPressed && + keybinding.isShiftPressed == keyEvent.isShiftPressed && + keybinding.keyCode == keyEvent.logicalKey.keyId) { + return true; + } + } + return false; + } +} + +class Keybinding { + Keybinding({ + required this.isAltPressed, + required this.isControlPressed, + required this.isMetaPressed, + required this.isShiftPressed, + required this.keyLabel, + }); + + factory Keybinding.parse(String command) { + command = command.toLowerCase().trim(); + + var isAltPressed = false; + var isControlPressed = false; + var isMetaPressed = false; + var isShiftPressed = false; + + var matchedModifier = false; + + do { + matchedModifier = false; + if (RegExp(r'^alt(\+|\-)').hasMatch(command)) { + isAltPressed = true; + command = command.substring(4); // 4 = 'alt '.length + matchedModifier = true; + } + if (RegExp(r'^ctrl(\+|\-)').hasMatch(command)) { + isControlPressed = true; + command = command.substring(5); // 5 = 'ctrl '.length + matchedModifier = true; + } + if (RegExp(r'^shift(\+|\-)').hasMatch(command)) { + isShiftPressed = true; + command = command.substring(6); // 6 = 'shift '.length + matchedModifier = true; + } + if (RegExp(r'^meta(\+|\-)').hasMatch(command)) { + isMetaPressed = true; + command = command.substring(5); // 5 = 'meta '.length + matchedModifier = true; + } + if (RegExp(r'^cmd(\+|\-)').hasMatch(command) || + RegExp(r'^win(\+|\-)').hasMatch(command)) { + isMetaPressed = true; + command = command.substring(4); // 4 = 'win '.length + matchedModifier = true; + } + } while (matchedModifier); + + return Keybinding( + isAltPressed: isAltPressed, + isControlPressed: isControlPressed, + isMetaPressed: isMetaPressed, + isShiftPressed: isShiftPressed, + keyLabel: command, + ); + } + + final bool isAltPressed; + final bool isControlPressed; + final bool isMetaPressed; + final bool isShiftPressed; + final String keyLabel; + + int get keyCode => keyToCodeMapping[keyLabel.toLowerCase()]!; + + Keybinding copyWith({ + bool? isAltPressed, + bool? isControlPressed, + bool? isMetaPressed, + bool? isShiftPressed, + String? keyLabel, + }) { + return Keybinding( + isAltPressed: isAltPressed ?? this.isAltPressed, + isControlPressed: isControlPressed ?? this.isControlPressed, + isMetaPressed: isMetaPressed ?? this.isMetaPressed, + isShiftPressed: isShiftPressed ?? this.isShiftPressed, + keyLabel: keyLabel ?? this.keyLabel, + ); + } + + Map toMap() { + return { + 'isAltPressed': isAltPressed, + 'isControlPressed': isControlPressed, + 'isMetaPressed': isMetaPressed, + 'isShiftPressed': isShiftPressed, + 'keyLabel': keyLabel, + }; + } + + factory Keybinding.fromMap(Map map) { + return Keybinding( + isAltPressed: map['isAltPressed'] ?? false, + isControlPressed: map['isControlPressed'] ?? false, + isMetaPressed: map['isMetaPressed'] ?? false, + isShiftPressed: map['isShiftPressed'] ?? false, + keyLabel: map['keyLabel'] ?? '', + ); + } + + String toJson() => json.encode(toMap()); + + factory Keybinding.fromJson(String source) => + Keybinding.fromMap(json.decode(source)); + + @override + String toString() { + return 'Keybinding(isAltPressed: $isAltPressed, isControlPressed: $isControlPressed, isMetaPressed: $isMetaPressed, isShiftPressed: $isShiftPressed, keyLabel: $keyLabel)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Keybinding && + other.isAltPressed == isAltPressed && + other.isControlPressed == isControlPressed && + other.isMetaPressed == isMetaPressed && + other.isShiftPressed == isShiftPressed && + other.keyCode == keyCode; + } + + @override + int get hashCode { + return isAltPressed.hashCode ^ + isControlPressed.hashCode ^ + isMetaPressed.hashCode ^ + isShiftPressed.hashCode ^ + keyCode; + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart new file mode 100644 index 0000000000..444f797ecd --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import 'package:appflowy_editor/src/service/shortcut_event/keybinding.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; + +/// Defines the implementation of shortcut event. +class ShortcutEvent { + ShortcutEvent({ + required this.key, + required this.command, + required this.handler, + String? windowsCommand, + String? macOSCommand, + String? linuxCommand, + }) { + updateCommand( + command, + windowsCommand: windowsCommand, + macOSCommand: macOSCommand, + linuxCommand: linuxCommand, + ); + } + + /// The unique key. + /// + /// Usually, uses the description as the key. + final String key; + + /// The string representation for the keyboard keys. + /// + /// The following is the mapping relationship of modify key. + /// ctrl: Ctrl + /// meta: Command in macOS or Control in Windows. + /// alt: Alt + /// shift: Shift + /// cmd: meta + /// win: meta + /// + /// Refer to [keyMapping] for other keys. + /// + /// Uses ',' to split different keyboard key combinations. + /// + /// Like, 'ctrl+c,cmd+c' + /// + String command; + + final ShortcutEventHandler handler; + + List keybindings = []; + + void updateCommand( + String command, { + String? windowsCommand, + String? macOSCommand, + String? linuxCommand, + }) { + if (Platform.isWindows && + windowsCommand != null && + windowsCommand.isNotEmpty) { + this.command = windowsCommand; + } else if (Platform.isMacOS && + macOSCommand != null && + macOSCommand.isNotEmpty) { + this.command = macOSCommand; + } else if (Platform.isLinux && + linuxCommand != null && + linuxCommand.isNotEmpty) { + this.command = linuxCommand; + } else { + this.command = command; + } + + keybindings = this + .command + .split(',') + .map((e) => Keybinding.parse(e)) + .toList(growable: false); + } + + ShortcutEvent copyWith({ + String? key, + String? command, + ShortcutEventHandler? handler, + }) { + return ShortcutEvent( + key: key ?? this.key, + command: command ?? this.command, + handler: handler ?? this.handler, + ); + } + + @override + String toString() => + 'ShortcutEvent(key: $key, command: $command, handler: $handler)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ShortcutEvent && + other.key == key && + other.command == command && + other.handler == handler; + } + + @override + int get hashCode => key.hashCode ^ command.hashCode ^ handler.hashCode; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event_handler.dart new file mode 100644 index 0000000000..2cb00ff198 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event_handler.dart @@ -0,0 +1,7 @@ +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:flutter/material.dart'; + +typedef ShortcutEventHandler = KeyEventResult Function( + EditorState editorState, + RawKeyEvent event, +); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart index ebbd71a392..eeb27d1c19 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -328,37 +330,69 @@ Future _testPressArrowKeyWithMetaInSelection( } } await editor.updateSelection(selection); - await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isMetaPressed: true, + ); + } + expect( editor.documentSelection, Selection.single(path: [0], startOffset: 0), ); - await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isMetaPressed: true, + ); + } + expect( editor.documentSelection, Selection.single(path: [0], startOffset: text.length), ); - await editor.pressLogicKey( - LogicalKeyboardKey.arrowUp, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowUp, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowUp, + isMetaPressed: true, + ); + } + expect( editor.documentSelection, Selection.single(path: [0], startOffset: 0), ); - await editor.pressLogicKey( - LogicalKeyboardKey.arrowDown, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowDown, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowDown, + isMetaPressed: true, + ); + } + expect( editor.documentSelection, Selection.single(path: [1], startOffset: text.length), diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart index 0f91e0b1d9..6fa98e9997 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -41,20 +43,35 @@ Future _testBackspaceUndoRedo( await editor.pressLogicKey(LogicalKeyboardKey.backspace); expect(editor.documentLength, 2); - await editor.pressLogicKey( - LogicalKeyboardKey.keyZ, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isMetaPressed: true, + ); + } expect(editor.documentLength, 3); expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); expect(editor.documentSelection, selection); - await editor.pressLogicKey( - LogicalKeyboardKey.keyZ, - isMetaPressed: true, - isShiftPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isControlPressed: true, + isShiftPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isMetaPressed: true, + isShiftPressed: true, + ); + } expect(editor.documentLength, 2); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart index 6f4f9d0ce6..6efd2064e9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -26,7 +28,11 @@ Future _testSelectAllHandler(WidgetTester tester, int lines) async { editor.insertTextNode(text); } await editor.startTesting(); - await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); + if (Platform.isWindows) { + await editor.pressLogicKey(LogicalKeyboardKey.keyA, isControlPressed: true); + } else { + await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); + } expect( editor.documentSelection, diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index d7afacb27a..bf595b1deb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; @@ -82,11 +84,19 @@ Future _testUpdateTextStyleByCommandX( var selection = Selection.single(path: [1], startOffset: 2, endOffset: text.length - 2); await editor.updateSelection(selection); - await editor.pressLogicKey( - key, - isShiftPressed: isShiftPressed, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isMetaPressed: true, + ); + } var textNode = editor.nodeAtPath([1]) as TextNode; expect( textNode.allSatisfyInSelection( @@ -101,11 +111,19 @@ Future _testUpdateTextStyleByCommandX( selection = Selection.single(path: [1], startOffset: 0, endOffset: text.length); await editor.updateSelection(selection); - await editor.pressLogicKey( - key, - isShiftPressed: isShiftPressed, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isMetaPressed: true, + ); + } textNode = editor.nodeAtPath([1]) as TextNode; expect( textNode.allSatisfyInSelection( @@ -118,11 +136,19 @@ Future _testUpdateTextStyleByCommandX( true); await editor.updateSelection(selection); - await editor.pressLogicKey( - key, - isShiftPressed: isShiftPressed, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isMetaPressed: true, + ); + } textNode = editor.nodeAtPath([1]) as TextNode; expect(textNode.allNotSatisfyInSelection(matchStyle, matchValue, selection), true); @@ -132,11 +158,19 @@ Future _testUpdateTextStyleByCommandX( end: Position(path: [2], offset: text.length), ); await editor.updateSelection(selection); - await editor.pressLogicKey( - key, - isShiftPressed: isShiftPressed, - isMetaPressed: true, - ); + if (Platform.isWindows) { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isMetaPressed: true, + ); + } var nodes = editor.editorState.service.selectionService.currentSelectedNodes .whereType(); expect(nodes.length, 3); @@ -158,11 +192,20 @@ Future _testUpdateTextStyleByCommandX( } await editor.updateSelection(selection); - await editor.pressLogicKey( - key, - isShiftPressed: isShiftPressed, - isMetaPressed: true, - ); + + if (Platform.isWindows) { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key, + isShiftPressed: isShiftPressed, + isMetaPressed: true, + ); + } nodes = editor.editorState.service.selectionService.currentSelectedNodes .whereType(); expect(nodes.length, 3); @@ -196,8 +239,11 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { expect(find.byType(ToolbarWidget), findsOneWidget); // trigger the link menu - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); - + if (Platform.isWindows) { + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true); + } else { + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + } expect(find.byType(LinkMenu), findsOneWidget); await tester.enterText(find.byType(TextField), link); @@ -216,7 +262,11 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { true); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + if (Platform.isWindows) { + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true); + } else { + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + } expect(find.byType(LinkMenu), findsOneWidget); expect( find.text(link, findRichText: true, skipOffstage: false), findsOneWidget); @@ -229,7 +279,11 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { expect(find.byType(LinkMenu), findsNothing); // Remove link - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + if (Platform.isWindows) { + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true); + } else { + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + } final removeLink = find.text('Remove link'); expect(removeLink, findsOneWidget); await tester.tap(removeLink); From 804b2b27c13f4db636275610beab409ff3e165b9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 6 Sep 2022 10:21:21 +0800 Subject: [PATCH 2/7] feat: refactor arrow key handler --- .../arrow_keys_handler.dart | 408 +++++++++++------- .../built_in_shortcut_events.dart | 110 +++-- .../arrow_keys_handler_test.dart | 68 +++ 3 files changed, 385 insertions(+), 201 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index d5a840c4df..c04dafe986 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,40 +1,140 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -ShortcutEventHandler arrowKeysHandler = (editorState, event) { - if (!_arrowKeys.contains(event.logicalKey)) { +ShortcutEventHandler cursorLeftSelect = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } - - if (event.isMetaPressed && event.isShiftPressed) { - return _arrowKeysWithMetaAndShift(editorState, event); - } else if (event.isMetaPressed) { - return _arrowKeysWithMeta(editorState, event); - } else if (event.isShiftPressed) { - return _arrowKeysWithShift(editorState, event); - } else { - return _arrowKeysOnly(editorState, event); + final end = selection.end.goLeft(editorState); + if (end == null) { + return KeyEventResult.ignored; } + editorState.service.selectionService.updateSelection( + selection.copyWith(end: end), + ); + return KeyEventResult.handled; }; -final _arrowKeys = [ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown -]; - -KeyEventResult _arrowKeysWithMetaAndShift( - EditorState editorState, RawKeyEvent event) { - if (!event.isMetaPressed || - !event.isShiftPressed || - !_arrowKeys.contains(event.logicalKey)) { - assert(false); +ShortcutEventHandler cursorRightSelect = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } + final end = selection.end.goRight(editorState); + if (end == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + selection.copyWith(end: end), + ); + return KeyEventResult.handled; +}; +ShortcutEventHandler cursorUpSelect = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + final end = _goUp(editorState); + if (end == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + selection.copyWith(end: end), + ); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cursorDownSelect = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + final end = _goDown(editorState); + if (end == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + selection.copyWith(end: end), + ); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cursorTop = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + if (nodes.isEmpty) { + return KeyEventResult.ignored; + } + final position = editorState.document.root.children + .whereType() + .first + .selectable + ?.start(); + if (position == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + Selection.collapsed(position), + ); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cursorBottom = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + if (nodes.isEmpty) { + return KeyEventResult.ignored; + } + final position = editorState.document.root.children + .whereType() + .last + .selectable + ?.end(); + if (position == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + Selection.collapsed(position), + ); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cursorBegin = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + if (nodes.isEmpty) { + return KeyEventResult.ignored; + } + final position = nodes.first.selectable?.start(); + if (position == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + Selection.collapsed(position), + ); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cursorEnd = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + if (nodes.isEmpty) { + return KeyEventResult.ignored; + } + final position = nodes.first.selectable?.end(); + if (position == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + Selection.collapsed(position), + ); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cursorTopSelect = (editorState, event) { final nodes = editorState.service.selectionService.currentSelectedNodes; final selection = editorState.service.selectionService.currentSelection.value; if (nodes.isEmpty || selection == null) { @@ -43,168 +143,150 @@ KeyEventResult _arrowKeysWithMetaAndShift( var start = selection.start; var end = selection.end; - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - final position = nodes.first.selectable?.start(); - if (position != null) { - end = position; - } - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - final position = nodes.first.selectable?.end(); - if (position != null) { - end = position; - } - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - final position = editorState.document.root.children - .whereType() - .first - .selectable - ?.start(); - if (position != null) { - end = position; - } - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - final position = editorState.document.root.children - .whereType() - .last - .selectable - ?.end(); - if (position != null) { - end = position; - } + final position = editorState.document.root.children + .whereType() + .first + .selectable + ?.start(); + if (position != null) { + end = position; } editorState.service.selectionService.updateSelection( selection.copyWith(start: start, end: end), ); return KeyEventResult.handled; -} - -// Move the cursor to top, bottom, left and right of the document. -KeyEventResult _arrowKeysWithMeta(EditorState editorState, RawKeyEvent event) { - if (!event.isMetaPressed || - event.isShiftPressed || - !_arrowKeys.contains(event.logicalKey)) { - assert(false); - return KeyEventResult.ignored; - } - - final nodes = editorState.service.selectionService.currentSelectedNodes; - if (nodes.isEmpty) { - return KeyEventResult.ignored; - } - Position? position; - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - position = nodes.first.selectable?.start(); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - position = nodes.last.selectable?.end(); - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - position = editorState.document.root.children - .whereType() - .first - .selectable - ?.start(); - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - position = editorState.document.root.children - .whereType() - .last - .selectable - ?.end(); - } - if (position == null) { - return KeyEventResult.ignored; - } - editorState.service.selectionService.updateSelection( - Selection.collapsed(position), - ); - return KeyEventResult.handled; -} - -KeyEventResult _arrowKeysWithShift(EditorState editorState, RawKeyEvent event) { - if (event.isMetaPressed || - !event.isShiftPressed || - !_arrowKeys.contains(event.logicalKey)) { - assert(false); - return KeyEventResult.ignored; - } +}; +ShortcutEventHandler cursorBottomSelect = (editorState, event) { final nodes = editorState.service.selectionService.currentSelectedNodes; final selection = editorState.service.selectionService.currentSelection.value; if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } - Position? end; - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - end = selection.end.goLeft(editorState); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - end = selection.end.goRight(editorState); - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - end = _goUp(editorState); - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - end = _goDown(editorState); + var start = selection.start; + var end = selection.end; + final position = editorState.document.root.children + .whereType() + .last + .selectable + ?.end(); + if (position != null) { + end = position; } - if (end == null) { - return KeyEventResult.ignored; - } - editorState.service.selectionService - .updateSelection(selection.copyWith(end: end)); + editorState.service.selectionService.updateSelection( + selection.copyWith(start: start, end: end), + ); return KeyEventResult.handled; -} +}; -KeyEventResult _arrowKeysOnly(EditorState editorState, RawKeyEvent event) { - if (event.isMetaPressed || - event.isShiftPressed || - !_arrowKeys.contains(event.logicalKey)) { - assert(false); +ShortcutEventHandler cursorBeginSelect = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } + var start = selection.start; + var end = selection.end; + final position = nodes.last.selectable?.start(); + if (position != null) { + end = position; + } + editorState.service.selectionService.updateSelection( + selection.copyWith(start: start, end: end), + ); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cursorEndSelect = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + + var start = selection.start; + var end = selection.end; + final position = nodes.last.selectable?.end(); + if (position != null) { + end = position; + } + editorState.service.selectionService.updateSelection( + selection.copyWith(start: start, end: end), + ); + return KeyEventResult.handled; +}; + +KeyEventResult cursorUp(EditorState editorState, RawKeyEvent event) { final nodes = editorState.service.selectionService.currentSelectedNodes; final selection = editorState.service.selectionService.currentSelection.value?.normalize; if (nodes.isEmpty || selection == null) { return KeyEventResult.ignored; } - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - if (selection.isCollapsed) { - final leftPosition = selection.start.goLeft(editorState); - if (leftPosition != null) { - editorState.service.selectionService.updateSelection( - Selection.collapsed(leftPosition), - ); - } - } else { - editorState.service.selectionService.updateSelection( - Selection.collapsed(selection.start), - ); - } - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - if (selection.isCollapsed) { - final rightPosition = selection.start.goRight(editorState); - if (rightPosition != null) { - editorState.service.selectionService.updateSelection( - Selection.collapsed(rightPosition), - ); - } - } else { - editorState.service.selectionService.updateSelection( - Selection.collapsed(selection.end), - ); - } - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - final upPosition = _goUp(editorState); - editorState.updateCursorSelection( - upPosition == null ? null : Selection.collapsed(upPosition), - ); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - final downPosition = _goDown(editorState); - editorState.updateCursorSelection( - downPosition == null ? null : Selection.collapsed(downPosition), - ); - return KeyEventResult.handled; + final upPosition = _goUp(editorState); + editorState.updateCursorSelection( + upPosition == null ? null : Selection.collapsed(upPosition), + ); + return KeyEventResult.handled; +} + +KeyEventResult cursorDown(EditorState editorState, RawKeyEvent event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = + editorState.service.selectionService.currentSelection.value?.normalize; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; } - return KeyEventResult.ignored; + final downPosition = _goDown(editorState); + editorState.updateCursorSelection( + downPosition == null ? null : Selection.collapsed(downPosition), + ); + return KeyEventResult.handled; +} + +KeyEventResult cursorLeft(EditorState editorState, RawKeyEvent event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = + editorState.service.selectionService.currentSelection.value?.normalize; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + if (selection.isCollapsed) { + final leftPosition = selection.start.goLeft(editorState); + if (leftPosition != null) { + editorState.service.selectionService.updateSelection( + Selection.collapsed(leftPosition), + ); + } + } else { + editorState.service.selectionService.updateSelection( + Selection.collapsed(selection.start), + ); + } + return KeyEventResult.handled; +} + +KeyEventResult cursorRight(EditorState editorState, RawKeyEvent event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = + editorState.service.selectionService.currentSelection.value?.normalize; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + if (selection.isCollapsed) { + final rightPosition = selection.start.goRight(editorState); + if (rightPosition != null) { + editorState.service.selectionService.updateSelection( + Selection.collapsed(rightPosition), + ); + } + } else { + editorState.service.selectionService.updateSelection( + Selection.collapsed(selection.end), + ); + } + return KeyEventResult.handled; } extension on Position { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index bfd1b97d6f..9f96ef3185 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -17,58 +17,92 @@ List builtInShortcutEvents = [ ShortcutEvent( key: 'Move cursor up', command: 'arrow up', - handler: arrowKeysHandler, + handler: cursorUp, ), ShortcutEvent( key: 'Move cursor down', command: 'arrow down', - handler: arrowKeysHandler, + handler: cursorDown, ), ShortcutEvent( key: 'Move cursor left', command: 'arrow left', - handler: arrowKeysHandler, + handler: cursorLeft, ), ShortcutEvent( key: 'Move cursor right', command: 'arrow right', - handler: arrowKeysHandler, + handler: cursorRight, + ), + ShortcutEvent( + key: 'Cursor up select', + command: 'shift+arrow up', + handler: cursorUpSelect, + ), + ShortcutEvent( + key: 'Cursor down select', + command: 'shift+arrow down', + handler: cursorDownSelect, + ), + ShortcutEvent( + key: 'Cursor left select', + command: 'shift+arrow left', + handler: cursorLeftSelect, + ), + ShortcutEvent( + key: 'Cursor right select', + command: 'shift+arrow right', + handler: cursorRightSelect, + ), + ShortcutEvent( + key: 'Move cursor top', + command: 'meta+arrow up', + windowsCommand: 'ctrl+arrow up', + handler: cursorBegin, + ), + ShortcutEvent( + key: 'Move cursor bottom', + command: 'meta+arrow down', + windowsCommand: 'ctrl+arrow down', + handler: cursorBottom, + ), + ShortcutEvent( + key: 'Move cursor begin', + command: 'meta+arrow left', + windowsCommand: 'ctrl+arrow left', + handler: cursorBegin, + ), + ShortcutEvent( + key: 'Move cursor end', + command: 'meta+arrow right', + windowsCommand: 'ctrl+arrow right', + handler: cursorEnd, + ), + ShortcutEvent( + key: 'Cursor top select', + command: 'meta+shift+arrow up', + windowsCommand: 'ctrl+shift+arrow up', + handler: cursorTopSelect, + ), + ShortcutEvent( + key: 'Cursor bottom select', + command: 'meta+shift+arrow down', + windowsCommand: 'ctrl+shift+arrow down', + handler: cursorBottomSelect, + ), + ShortcutEvent( + key: 'Cursor begin select', + command: 'meta+shift+arrow left', + windowsCommand: 'ctrl+shift+arrow left', + handler: cursorBeginSelect, + ), + ShortcutEvent( + key: 'Cursor end select', + command: 'meta+shift+arrow right', + windowsCommand: 'ctrl+shift+arrow right', + handler: cursorEndSelect, ), // TODO: split the keys. - ShortcutEvent( - key: 'Shift + Arrow Keys', - command: - 'shift+arrow up,shift+arrow down,shift+arrow left,shift+arrow right', - handler: arrowKeysHandler, - ), - ShortcutEvent( - key: 'Control + Arrow Keys', - command: 'meta+arrow up,meta+arrow down,meta+arrow left,meta+arrow right', - windowsCommand: - 'ctrl+arrow up,ctrl+arrow down,ctrl+arrow left,ctrl+arrow right', - macOSCommand: 'cmd+arrow up,cmd+arrow down,cmd+arrow left,cmd+arrow right', - handler: arrowKeysHandler, - ), - ShortcutEvent( - key: 'Meta + Shift + Arrow Keys', - command: - 'meta+shift+arrow up,meta+shift+arrow down,meta+shift+arrow left,meta+shift+arrow right', - windowsCommand: - 'ctrl+shift+arrow up,ctrl+shift+arrow down,ctrl+shift+arrow left,ctrl+shift+arrow right', - macOSCommand: - 'cmd+shift+arrow up,cmd+shift+arrow down,cmd+shift+arrow left,cmd+shift+arrow right', - handler: arrowKeysHandler, - ), - ShortcutEvent( - key: 'Meta + Shift + Arrow Keys', - command: - 'meta+shift+arrow up,meta+shift+arrow down,meta+shift+arrow left,meta+shift+arrow right', - windowsCommand: - 'ctrl+shift+arrow up,ctrl+shift+arrow down,ctrl+shift+arrow left,ctrl+shift+arrow right', - macOSCommand: - 'cmd+shift+arrow up,cmd+shift+arrow down,cmd+shift+arrow left,cmd+shift+arrow right', - handler: arrowKeysHandler, - ), ShortcutEvent( key: 'Delete Text', command: 'delete,backspace', diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart index eeb27d1c19..5000c3ec93 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart @@ -273,6 +273,74 @@ void main() async { ), ); }); + + testWidgets('Presses shift + arrow down and meta/ctrl + shift + right', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + final selection = Selection.single(path: [0], startOffset: 8); + await editor.updateSelection(selection); + await editor.pressLogicKey( + LogicalKeyboardKey.arrowDown, + isShiftPressed: true, + ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isShiftPressed: true, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isShiftPressed: true, + isMetaPressed: true, + ); + } + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [1], offset: text.length), + ), + ); + }); + + testWidgets('Presses shift + arrow up and meta/ctrl + shift + left', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + final selection = Selection.single(path: [1], startOffset: 8); + await editor.updateSelection(selection); + await editor.pressLogicKey( + LogicalKeyboardKey.arrowUp, + isShiftPressed: true, + ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isShiftPressed: true, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isShiftPressed: true, + isMetaPressed: true, + ); + } + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: 0), + ), + ); + }); } Future _testPressArrowKeyInNotCollapsedSelection( From 73cfe39e41fc67b89cfc205024ccf82ce30efcfe Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 6 Sep 2022 10:55:29 +0800 Subject: [PATCH 3/7] feat: refactor copy / paste / redo / undo / select all / format event handler --- .../copy_paste_handler.dart | 29 +++---- .../format_style_handler.dart | 75 ++++++++++++++++ .../redo_undo_handler.dart | 19 ++-- .../select_all_handler.dart | 21 ++--- ...pdate_text_style_by_command_x_handler.dart | 48 ----------- .../built_in_shortcut_events.dart | 86 +++++++++++++++---- 6 files changed, 172 insertions(+), 106 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart delete mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 6a4f58fa86..15a7081653 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,9 +1,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/infra/html_converter.dart'; import 'package:appflowy_editor/src/document/node_iterator.dart'; -import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; _handleCopy(EditorState editorState) async { @@ -304,18 +302,17 @@ _deleteSelectedContent(EditorState editorState) { tb.commit(); } -ShortcutEventHandler copyPasteKeysHandler = (editorState, event) { - if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyC) { - _handleCopy(editorState); - return KeyEventResult.handled; - } - if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV) { - _handlePaste(editorState); - return KeyEventResult.handled; - } - if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyX) { - _handleCut(editorState); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; +ShortcutEventHandler copyEventHandler = (editorState, event) { + _handleCopy(editorState); + return KeyEventResult.handled; +}; + +ShortcutEventHandler pasteEventHandler = (editorState, event) { + _handlePaste(editorState); + return KeyEventResult.handled; +}; + +ShortcutEventHandler cutEventHandler = (editorState, event) { + _handleCut(editorState); + return KeyEventResult.handled; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart new file mode 100644 index 0000000000..2f17296ca6 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart @@ -0,0 +1,75 @@ +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; + +ShortcutEventHandler formatBoldEventHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final textNodes = nodes.whereType().toList(growable: false); + if (selection == null || textNodes.isEmpty) { + return KeyEventResult.ignored; + } + formatBold(editorState); + return KeyEventResult.handled; +}; + +ShortcutEventHandler formatItalicEventHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final textNodes = nodes.whereType().toList(growable: false); + if (selection == null || textNodes.isEmpty) { + return KeyEventResult.ignored; + } + formatItalic(editorState); + return KeyEventResult.handled; +}; + +ShortcutEventHandler formatUnderlineEventHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final textNodes = nodes.whereType().toList(growable: false); + if (selection == null || textNodes.isEmpty) { + return KeyEventResult.ignored; + } + formatUnderline(editorState); + return KeyEventResult.handled; +}; + +ShortcutEventHandler formatStrikethroughEventHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final textNodes = nodes.whereType().toList(growable: false); + if (selection == null || textNodes.isEmpty) { + return KeyEventResult.ignored; + } + formatStrikethrough(editorState); + return KeyEventResult.handled; +}; + +ShortcutEventHandler formatHighlightEventHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final textNodes = nodes.whereType().toList(growable: false); + if (selection == null || textNodes.isEmpty) { + return KeyEventResult.ignored; + } + formatHighlight(editorState); + return KeyEventResult.handled; +}; + +ShortcutEventHandler formatLinkEventHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final textNodes = nodes.whereType().toList(growable: false); + if (selection == null || textNodes.isEmpty) { + return KeyEventResult.ignored; + } + if (editorState.service.toolbarService + ?.triggerHandler('appflowy.toolbar.link') == + true) { + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart index 7bec895f91..e20d6dc43d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart @@ -1,15 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; -ShortcutEventHandler redoUndoKeysHandler = (editorState, event) { - if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyZ) { - if (event.isShiftPressed) { - editorState.undoManager.redo(); - } else { - editorState.undoManager.undo(); - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; +ShortcutEventHandler redoEventHandler = (editorState, event) { + editorState.undoManager.redo(); + return KeyEventResult.handled; +}; + +ShortcutEventHandler undoEventHandler = (editorState, event) { + editorState.undoManager.undo(); + return KeyEventResult.handled; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart index 6f8e1ae750..9841ec3167 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart @@ -1,9 +1,10 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/position.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -KeyEventResult _selectAll(EditorState editorState) { +ShortcutEventHandler selectAllHandler = (editorState, event) { if (editorState.document.root.children.isEmpty) { return KeyEventResult.handled; } @@ -13,15 +14,11 @@ KeyEventResult _selectAll(EditorState editorState) { if (lastNode is TextNode) { offset = lastNode.delta.length; } - editorState.updateCursorSelection(Selection( + editorState.updateCursorSelection( + Selection( start: Position(path: firstNode.path, offset: 0), - end: Position(path: lastNode.path, offset: offset))); + end: Position(path: lastNode.path, offset: offset), + ), + ); return KeyEventResult.handled; -} - -ShortcutEventHandler selectAllHandler = (editorState, event) { - if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyA) { - return _selectAll(editorState); - } - return KeyEventResult.ignored; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart deleted file mode 100644 index 19d50f80cf..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; - -import 'package:flutter/services.dart'; - -ShortcutEventHandler updateTextStyleByCommandXHandler = (editorState, event) { - if (!event.isMetaPressed) { - return KeyEventResult.ignored; - } - - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(growable: false); - - if (selection == null || textNodes.isEmpty) { - return KeyEventResult.ignored; - } - - if (event.logicalKey == LogicalKeyboardKey.keyB) { - formatBold(editorState); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.keyI) { - formatItalic(editorState); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.keyU) { - formatUnderline(editorState); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.keyS && - event.isShiftPressed) { - formatStrikethrough(editorState); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.keyH && - event.isShiftPressed) { - formatHighlight(editorState); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.keyK) { - if (editorState.service.toolbarService - ?.triggerHandler('appflowy.toolbar.link') == - true) { - return KeyEventResult.handled; - } - } - - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 9f96ef3185..d313a33dc8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -8,7 +8,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_ import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_style_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; @@ -102,6 +102,72 @@ List builtInShortcutEvents = [ windowsCommand: 'ctrl+shift+arrow right', handler: cursorEndSelect, ), + ShortcutEvent( + key: 'Redo', + command: 'meta+shift+z', + windowsCommand: 'ctrl+shift+z', + handler: redoEventHandler, + ), + ShortcutEvent( + key: 'Undo', + command: 'meta+z', + windowsCommand: 'ctrl+z', + handler: undoEventHandler, + ), + ShortcutEvent( + key: 'Format bold', + command: 'meta+b', + windowsCommand: 'ctrl+b', + handler: formatBoldEventHandler, + ), + ShortcutEvent( + key: 'Format italic', + command: 'meta+i', + windowsCommand: 'ctrl+i', + handler: formatItalicEventHandler, + ), + ShortcutEvent( + key: 'Format underline', + command: 'meta+u', + windowsCommand: 'ctrl+u', + handler: formatUnderlineEventHandler, + ), + ShortcutEvent( + key: 'Format strikethrough', + command: 'meta+shift+s', + windowsCommand: 'ctrl+shift+s', + handler: formatStrikethroughEventHandler, + ), + ShortcutEvent( + key: 'Format highlight', + command: 'meta+shift+h', + windowsCommand: 'ctrl+shift+h', + handler: formatHighlightEventHandler, + ), + ShortcutEvent( + key: 'Format link', + command: 'meta+k', + windowsCommand: 'ctrl+k', + handler: formatLinkEventHandler, + ), + ShortcutEvent( + key: 'Copy', + command: 'meta+c', + windowsCommand: 'ctrl+c', + handler: copyEventHandler, + ), + ShortcutEvent( + key: 'Paste', + command: 'meta+v', + windowsCommand: 'ctrl+v', + handler: pasteEventHandler, + ), + ShortcutEvent( + key: 'Paste', + command: 'meta+x', + windowsCommand: 'ctrl+x', + handler: cutEventHandler, + ), // TODO: split the keys. ShortcutEvent( key: 'Delete Text', @@ -113,29 +179,11 @@ List builtInShortcutEvents = [ command: 'slash', handler: slashShortcutHandler, ), - ShortcutEvent( - key: 'copy / paste', - command: 'meta+c,meta+v', - windowsCommand: 'ctrl+c,ctrl+v', - handler: copyPasteKeysHandler, - ), - ShortcutEvent( - key: 'redo / undo', - command: 'meta+z,meta+meta+shift+z', - windowsCommand: 'ctrl+z,meta+ctrl+shift+z', - handler: redoUndoKeysHandler, - ), ShortcutEvent( key: 'enter', command: 'enter', handler: enterWithoutShiftInTextNodesHandler, ), - ShortcutEvent( - key: 'update text style', - command: 'meta+b,meta+i,meta+u,meta+shift+s,meta+shift+h,meta+k', - windowsCommand: 'ctrl+b,ctrl+i,ctrl+u,ctrl+shift+s,ctrl+shift+h,ctrl+k', - handler: updateTextStyleByCommandXHandler, - ), ShortcutEvent( key: 'markdown', command: 'space', From 2571c6b1bf20e7a0e0584bdd86b1069b01091c3f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 6 Sep 2022 12:58:37 +0800 Subject: [PATCH 4/7] test: add shortcut_event / keybinding test --- .../built_in_shortcut_events.dart | 2 +- .../test/infra/test_raw_key_event.dart | 3 + .../shortcut_event/keybinding_test.dart | 82 +++++++++++++++++++ .../shortcut_event/shortcut_event_test.dart | 75 +++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/keybinding_test.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index d313a33dc8..e619246069 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -58,7 +58,7 @@ List builtInShortcutEvents = [ key: 'Move cursor top', command: 'meta+arrow up', windowsCommand: 'ctrl+arrow up', - handler: cursorBegin, + handler: cursorTop, ), ShortcutEvent( key: 'Move cursor bottom', diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 47cacc18b1..451f203e8b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -115,6 +115,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyB) { return PhysicalKeyboardKey.keyB; } + if (this == LogicalKeyboardKey.keyC) { + return PhysicalKeyboardKey.keyC; + } if (this == LogicalKeyboardKey.keyI) { return PhysicalKeyboardKey.keyI; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/keybinding_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/keybinding_test.dart new file mode 100644 index 0000000000..48b335c6c7 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/keybinding_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('keybinding_test.dart', () { + test('keybinding parse(cmd+shift+alt+ctrl+a)', () { + const command = 'cmd+shift+alt+ctrl+a'; + final keybinding = Keybinding.parse(command); + expect(keybinding.isAltPressed, true); + expect(keybinding.isShiftPressed, true); + expect(keybinding.isMetaPressed, true); + expect(keybinding.isControlPressed, true); + expect(keybinding.keyLabel, 'a'); + }); + + test('keybinding parse(cmd+shift+alt+a)', () { + const command = 'cmd+shift+alt+a'; + final keybinding = Keybinding.parse(command); + expect(keybinding.isAltPressed, true); + expect(keybinding.isShiftPressed, true); + expect(keybinding.isMetaPressed, true); + expect(keybinding.isControlPressed, false); + expect(keybinding.keyLabel, 'a'); + }); + + test('keybinding parse(cmd+shift+ctrl+a)', () { + const command = 'cmd+shift+ctrl+a'; + final keybinding = Keybinding.parse(command); + expect(keybinding.isAltPressed, false); + expect(keybinding.isShiftPressed, true); + expect(keybinding.isMetaPressed, true); + expect(keybinding.isControlPressed, true); + expect(keybinding.keyLabel, 'a'); + }); + + test('keybinding parse(cmd+alt+ctrl+a)', () { + const command = 'cmd+alt+ctrl+a'; + final keybinding = Keybinding.parse(command); + expect(keybinding.isAltPressed, true); + expect(keybinding.isShiftPressed, false); + expect(keybinding.isMetaPressed, true); + expect(keybinding.isControlPressed, true); + expect(keybinding.keyLabel, 'a'); + }); + + test('keybinding parse(shift+alt+ctrl+a)', () { + const command = 'shift+alt+ctrl+a'; + final keybinding = Keybinding.parse(command); + expect(keybinding.isAltPressed, true); + expect(keybinding.isShiftPressed, true); + expect(keybinding.isMetaPressed, false); + expect(keybinding.isControlPressed, true); + expect(keybinding.keyLabel, 'a'); + }); + + test('keybinding copyWith', () { + const command = 'shift+alt+ctrl+a'; + final keybinding = + Keybinding.parse(command).copyWith(isMetaPressed: true); + expect(keybinding.isAltPressed, true); + expect(keybinding.isShiftPressed, true); + expect(keybinding.isMetaPressed, true); + expect(keybinding.isControlPressed, true); + expect(keybinding.keyLabel, 'a'); + }); + + test('keybinding equal', () { + const command = 'cmd+shift+alt+ctrl+a'; + expect(Keybinding.parse(command), Keybinding.parse(command)); + }); + + test('keybinding toMap', () { + const command = 'cmd+shift+alt+ctrl+a'; + final keybinding = Keybinding.parse(command); + expect(keybinding, Keybinding.fromMap(keybinding.toMap())); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart new file mode 100644 index 0000000000..1a479ad086 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart @@ -0,0 +1,75 @@ +import 'dart:io'; + +import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('shortcut_event.dart', () { + test('redefine shortcut event command', () { + final shortcutEvent = ShortcutEvent( + key: 'Sample', + command: 'cmd+shift+alt+ctrl+a', + handler: (editorState, event) { + return KeyEventResult.handled; + }, + ); + shortcutEvent.updateCommand('cmd+shift+alt+ctrl+b'); + expect(shortcutEvent.keybindings.length, 1); + expect(shortcutEvent.keybindings.first.isMetaPressed, true); + expect(shortcutEvent.keybindings.first.isShiftPressed, true); + expect(shortcutEvent.keybindings.first.isAltPressed, true); + expect(shortcutEvent.keybindings.first.isControlPressed, true); + expect(shortcutEvent.keybindings.first.keyLabel, 'b'); + }); + + testWidgets('redefine move cursor begin command', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [1], startOffset: text.length), + ); + if (Platform.isWindows) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isMetaPressed: true, + ); + } + expect( + editor.documentSelection, + Selection.single(path: [1], startOffset: 0), + ); + await editor.updateSelection( + Selection.single(path: [1], startOffset: text.length), + ); + for (final event in builtInShortcutEvents) { + if (event.key == 'Move cursor begin') { + event.updateCommand('alt+arrow left'); + } + } + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isAltPressed: true, + ); + expect( + editor.documentSelection, + Selection.single(path: [1], startOffset: 0), + ); + }); + }); +} From 5d2160b0bb3e39a8955371fbc500e9e544625dcd Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 6 Sep 2022 13:00:39 +0800 Subject: [PATCH 5/7] chore: rename keyEventHandlers to shortcutEvents --- .../packages/appflowy_editor/example/lib/main.dart | 2 +- .../appflowy_editor/lib/src/service/editor_service.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 80e1813e89..921628ff21 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -96,7 +96,7 @@ class _MyHomePageState extends State { child: AppFlowyEditor( editorState: _editorState, editorStyle: const EditorStyle.defaultStyle(), - keyEventHandlers: [ + shortcutEvents: [ underscoreToItalicEvent, ], ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 9465d44de4..ba9c45732b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -36,7 +36,7 @@ class AppFlowyEditor extends StatefulWidget { Key? key, required this.editorState, this.customBuilders = const {}, - this.keyEventHandlers = const [], + this.shortcutEvents = const [], this.selectionMenuItems = const [], this.editorStyle = const EditorStyle.defaultStyle(), }) : super(key: key); @@ -47,7 +47,7 @@ class AppFlowyEditor extends StatefulWidget { final NodeWidgetBuilders customBuilders; /// Keyboard event handlers. - final List keyEventHandlers; + final List shortcutEvents; final List selectionMenuItems; @@ -96,7 +96,7 @@ class _AppFlowyEditorState extends State { key: editorState.service.keyboardServiceKey, shortcutEvents: [ ...builtInShortcutEvents, - ...widget.keyEventHandlers, + ...widget.shortcutEvents, ], editorState: editorState, child: FlowyToolbar( From 930ad261552203549e22b4750c9fba75456fb61b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 6 Sep 2022 13:14:03 +0800 Subject: [PATCH 6/7] docs: update customizing.md --- .../documentation/customizing.md | 33 +++++++++++-------- ...nderscore_to_italic_key_event_handler.dart | 9 +---- .../shortcut_event/shortcut_event.dart | 28 ++++++++++------ .../shortcut_event/shortcut_event_test.dart | 23 +++++++++---- 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md b/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md index ed0f257ad0..67b14cb9e7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md +++ b/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md @@ -16,7 +16,7 @@ Widget build(BuildContext context) { alignment: Alignment.topCenter, child: AppFlowyEditor( editorState: EditorState.empty(), - keyEventHandlers: const [], + shortcutEvents: const [], ), ), ); @@ -27,19 +27,26 @@ At this point, nothing magic will happen after typing `_xxx_`. ![Before](./images/customizing_a_shortcut_event_before.gif) -To implement our shortcut event we will create a function to handle an underscore input. +To implement our shortcut event we will create a `ShortcutEvent` instance to handle an underscore input. + +We need to define `key` and `command` in a ShortCutEvent object to customize hotkeys. We recommend using the description of your event as a key. For example, if the underscore `_` is defined to make text italic, the key can be 'Underscore to italic'. + +> The command, made up of a single keyword such as `underscore` or a combination of keywords using the `+` sign in between to concatenate, is a condition that triggers a user-defined function. To see which keywords are available to define a command, please refer to [key_mapping.dart](../lib/src/service/shortcut_event/key_mapping.dart). +> If more than one commands trigger the same handler, then we use ',' to split them. For example, using CTRL and A or CMD and A to 'select all', we describe it as `cmd+a,ctrl+a`(case-insensitive). ```dart import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) { - // Since we only need to handle the input of an 'underscore' character, - // all inputs except `underscore` will be ignored immediately. - if (event.logicalKey != LogicalKeyboardKey.underscore) { - return KeyEventResult.ignored; - } +ShortcutEvent underscoreToItalicEvent = ShortcutEvent( + key: 'Underscore to italic', + command: 'underscore', + handler: _underscoreToItalicHandler, +); + +ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { + }; ``` @@ -49,9 +56,7 @@ If so, we will continue. ```dart // ... -FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) { - // ... - +ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { // Obtain the selection and selected nodes of the current document through the 'selectionService' // to determine whether the selection is collapsed and whether the selected node is a text node. final selectionService = editorState.service.selectionService; @@ -70,7 +75,7 @@ Look for the position of the previous underscore and ```dart // ... -FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) { +ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { // ... final textNode = textNodes.first; @@ -111,8 +116,8 @@ Widget build(BuildContext context) { alignment: Alignment.topCenter, child: AppFlowyEditor( editorState: EditorState.empty(), - keyEventHandlers: [ - underscoreToItalicHandler, + shortcutEvents: [ + _underscoreToItalicHandler, ], ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart index 13ee27af05..c55bfa56bb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart @@ -1,20 +1,13 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; ShortcutEvent underscoreToItalicEvent = ShortcutEvent( key: 'Underscore to italic', - command: 'shift+underscore', + command: 'underscore', handler: _underscoreToItalicHandler, ); ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) { - // Since we only need to handler the input of `underscore`. - // All inputs except `underscore` will be ignored directly. - if (event.logicalKey != LogicalKeyboardKey.underscore) { - return KeyEventResult.ignored; - } - // Obtaining the selection and selected nodes of the current document through `selectionService`, // and determine whether it is a single selection and whether the selected node is a text node. final selectionService = editorState.service.selectionService; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart index 444f797ecd..ae64b1635c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart @@ -14,7 +14,7 @@ class ShortcutEvent { String? linuxCommand, }) { updateCommand( - command, + command: command, windowsCommand: windowsCommand, macOSCommand: macOSCommand, linuxCommand: linuxCommand, @@ -46,35 +46,43 @@ class ShortcutEvent { final ShortcutEventHandler handler; - List keybindings = []; + List get keybindings => _keybindings; + List _keybindings = []; - void updateCommand( - String command, { + void updateCommand({ + String? command, String? windowsCommand, String? macOSCommand, String? linuxCommand, }) { + var matched = false; if (Platform.isWindows && windowsCommand != null && windowsCommand.isNotEmpty) { this.command = windowsCommand; + matched = true; } else if (Platform.isMacOS && macOSCommand != null && macOSCommand.isNotEmpty) { this.command = macOSCommand; + matched = true; } else if (Platform.isLinux && linuxCommand != null && linuxCommand.isNotEmpty) { this.command = linuxCommand; - } else { + matched = true; + } else if (command != null && command.isNotEmpty) { this.command = command; + matched = true; } - keybindings = this - .command - .split(',') - .map((e) => Keybinding.parse(e)) - .toList(growable: false); + if (matched) { + _keybindings = this + .command + .split(',') + .map((e) => Keybinding.parse(e)) + .toList(growable: false); + } } ShortcutEvent copyWith({ diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart index 1a479ad086..93a0a7bb84 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart @@ -21,7 +21,7 @@ void main() async { return KeyEventResult.handled; }, ); - shortcutEvent.updateCommand('cmd+shift+alt+ctrl+b'); + shortcutEvent.updateCommand(command: 'cmd+shift+alt+ctrl+b'); expect(shortcutEvent.keybindings.length, 1); expect(shortcutEvent.keybindings.first.isMetaPressed, true); expect(shortcutEvent.keybindings.first.isShiftPressed, true); @@ -57,15 +57,26 @@ void main() async { await editor.updateSelection( Selection.single(path: [1], startOffset: text.length), ); + for (final event in builtInShortcutEvents) { if (event.key == 'Move cursor begin') { - event.updateCommand('alt+arrow left'); + event.updateCommand( + windowsCommand: 'alt+arrow left', + macOSCommand: 'alt+arrow left', + ); } } - await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, - isAltPressed: true, - ); + if (Platform.isWindows || Platform.isMacOS) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isAltPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isMetaPressed: true, + ); + } expect( editor.documentSelection, Selection.single(path: [1], startOffset: 0), From 759c10faca0124c9d917922f42fb8b55e6b7f59b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 7 Sep 2022 11:10:15 +0800 Subject: [PATCH 7/7] chore: fix analysis problems --- .../legacy/arrow_keys_handler.dart | 1 - .../lib/src/service/keyboard_service.dart | 12 ------------ .../appflowy_editor/lib/src/service/service.dart | 6 +++++- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart index 56150020e2..523e13fbad 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart index 4b511c2ca4..790e9dd94f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -139,15 +139,3 @@ class _AppFlowyKeyboardState extends State return onKey(event); } } - -extension on RawKeyEvent { - Keybinding get toKeybinding { - return Keybinding( - isAltPressed: isAltPressed, - isControlPressed: isControlPressed, - isMetaPressed: isMetaPressed, - isShiftPressed: isShiftPressed, - keyLabel: logicalKey.keyLabel, - ); - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart index b9b6ac390a..7b64e9e36d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart @@ -1,4 +1,8 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/service/input_service.dart'; +import 'package:appflowy_editor/src/service/keyboard_service.dart'; +import 'package:appflowy_editor/src/service/render_plugin_service.dart'; +import 'package:appflowy_editor/src/service/scroll_service.dart'; +import 'package:appflowy_editor/src/service/selection_service.dart'; import 'package:appflowy_editor/src/service/toolbar_service.dart'; import 'package:flutter/material.dart';