From 804b2b27c13f4db636275610beab409ff3e165b9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 6 Sep 2022 10:21:21 +0800 Subject: [PATCH] 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(