mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #989 from LucasXu0/imporve_keyboard_service
#964 Support the shortcut keys customization
This commit is contained in:
commit
3d10d46e6a
@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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<MyHomePage> {
|
||||
child: AppFlowyEditor(
|
||||
editorState: _editorState,
|
||||
editorStyle: const EditorStyle.defaultStyle(),
|
||||
shortcutEvents: [
|
||||
underscoreToItalicEvent,
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
@ -1,14 +1,13 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
AppFlowyKeyEventHandler 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;
|
||||
}
|
||||
ShortcutEvent underscoreToItalicEvent = ShortcutEvent(
|
||||
key: 'Underscore to italic',
|
||||
command: 'underscore',
|
||||
handler: _underscoreToItalicHandler,
|
||||
);
|
||||
|
||||
ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
|
||||
// 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;
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
@ -35,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);
|
||||
@ -46,7 +47,7 @@ class AppFlowyEditor extends StatefulWidget {
|
||||
final NodeWidgetBuilders customBuilders;
|
||||
|
||||
/// Keyboard event handlers.
|
||||
final List<AppFlowyKeyEventHandler> keyEventHandlers;
|
||||
final List<ShortcutEvent> shortcutEvents;
|
||||
|
||||
final List<SelectionMenuItem> selectionMenuItems;
|
||||
|
||||
@ -93,9 +94,9 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||
editorState: editorState,
|
||||
child: AppFlowyKeyboard(
|
||||
key: editorState.service.keyboardServiceKey,
|
||||
handlers: [
|
||||
...defaultKeyEventHandlers,
|
||||
...widget.keyEventHandlers,
|
||||
shortcutEvents: [
|
||||
...builtInShortcutEvents,
|
||||
...widget.shortcutEvents,
|
||||
],
|
||||
editorState: editorState,
|
||||
child: FlowyToolbar(
|
||||
|
@ -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';
|
||||
|
||||
AppFlowyKeyEventHandler 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<TextNode>()
|
||||
.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<TextNode>()
|
||||
.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<TextNode>()
|
||||
.first
|
||||
.selectable
|
||||
?.start();
|
||||
if (position != null) {
|
||||
end = position;
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
final position = editorState.document.root.children
|
||||
.whereType<TextNode>()
|
||||
.last
|
||||
.selectable
|
||||
?.end();
|
||||
if (position != null) {
|
||||
end = position;
|
||||
}
|
||||
final position = editorState.document.root.children
|
||||
.whereType<TextNode>()
|
||||
.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<TextNode>()
|
||||
.first
|
||||
.selectable
|
||||
?.start();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
position = editorState.document.root.children
|
||||
.whereType<TextNode>()
|
||||
.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<TextNode>()
|
||||
.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 {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ 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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||
|
||||
_handleCopy(EditorState editorState) async {
|
||||
@ -303,18 +302,17 @@ _deleteSelectedContent(EditorState editorState) {
|
||||
tb.commit();
|
||||
}
|
||||
|
||||
AppFlowyKeyEventHandler 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;
|
||||
};
|
||||
|
@ -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<AppFlowyKeyEventHandler> defaultKeyEventHandlers = [
|
||||
deleteTextHandler,
|
||||
slashShortcutHandler,
|
||||
// arrowKeysHandler,
|
||||
arrowKeysHandler,
|
||||
copyPasteKeysHandler,
|
||||
redoUndoKeysHandler,
|
||||
enterWithoutShiftInTextNodesHandler,
|
||||
updateTextStyleByCommandXHandler,
|
||||
whiteSpaceHandler,
|
||||
selectAllHandler,
|
||||
pageUpDownHandler,
|
||||
];
|
@ -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;
|
||||
|
@ -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<TextNode>().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<TextNode>().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<TextNode>().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<TextNode>().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<TextNode>().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<TextNode>().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;
|
||||
};
|
@ -103,7 +103,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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,15 +1,12 @@
|
||||
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) {
|
||||
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;
|
||||
};
|
||||
|
@ -1,8 +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;
|
||||
}
|
||||
@ -12,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;
|
||||
}
|
||||
|
||||
AppFlowyKeyEventHandler selectAllHandler = (editorState, event) {
|
||||
if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyA) {
|
||||
return _selectAll(editorState);
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
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) {
|
||||
if (!event.isMetaPressed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final textNodes = nodes.whereType<TextNode>().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;
|
||||
};
|
@ -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<String> 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;
|
||||
}
|
||||
|
@ -22,6 +22,9 @@ abstract class AppFlowyKeyboardService {
|
||||
/// Processes shortcut key input.
|
||||
KeyEventResult onKey(RawKeyEvent event);
|
||||
|
||||
/// Gets the shortcut events
|
||||
List<ShortcutEvent> 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<AppFlowyKeyEventHandler> handlers;
|
||||
final List<ShortcutEvent> shortcutEvents;
|
||||
|
||||
@override
|
||||
State<AppFlowyKeyboard> createState() => _AppFlowyKeyboardState();
|
||||
@ -63,6 +61,10 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
|
||||
bool isFocus = true;
|
||||
|
||||
@override
|
||||
// TODO: implement shortcutEvents
|
||||
List<ShortcutEvent> get shortcutEvents => widget.shortcutEvents;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
@ -111,16 +113,14 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -0,0 +1,203 @@
|
||||
// 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/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';
|
||||
|
||||
//
|
||||
List<ShortcutEvent> builtInShortcutEvents = [
|
||||
ShortcutEvent(
|
||||
key: 'Move cursor up',
|
||||
command: 'arrow up',
|
||||
handler: cursorUp,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Move cursor down',
|
||||
command: 'arrow down',
|
||||
handler: cursorDown,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Move cursor left',
|
||||
command: 'arrow left',
|
||||
handler: cursorLeft,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Move cursor right',
|
||||
command: 'arrow right',
|
||||
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: cursorTop,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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',
|
||||
command: 'delete,backspace',
|
||||
handler: deleteTextHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'selection menu',
|
||||
command: 'slash',
|
||||
handler: slashShortcutHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'enter',
|
||||
command: 'enter',
|
||||
handler: enterWithoutShiftInTextNodesHandler,
|
||||
),
|
||||
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,
|
||||
),
|
||||
];
|
@ -0,0 +1,451 @@
|
||||
/// Keyboard key to keycode mapping table
|
||||
///
|
||||
/// Copy from flutter project, keyboard_key.dart.
|
||||
///
|
||||
|
||||
Map<String, int> keyToCodeMapping = <String, int>{
|
||||
'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));
|
@ -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<Keybinding> {
|
||||
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<String, dynamic> toMap() {
|
||||
return {
|
||||
'isAltPressed': isAltPressed,
|
||||
'isControlPressed': isControlPressed,
|
||||
'isMetaPressed': isMetaPressed,
|
||||
'isShiftPressed': isShiftPressed,
|
||||
'keyLabel': keyLabel,
|
||||
};
|
||||
}
|
||||
|
||||
factory Keybinding.fromMap(Map<String, dynamic> 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
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: 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<Keybinding> get keybindings => _keybindings;
|
||||
List<Keybinding> _keybindings = [];
|
||||
|
||||
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;
|
||||
matched = true;
|
||||
} else if (command != null && command.isNotEmpty) {
|
||||
this.command = command;
|
||||
matched = true;
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
_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;
|
||||
}
|
@ -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,
|
||||
);
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
@ -271,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<void> _testPressArrowKeyInNotCollapsedSelection(
|
||||
@ -328,37 +398,69 @@ Future<void> _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),
|
||||
|
@ -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<void> _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);
|
||||
}
|
||||
|
@ -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<void> _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,
|
||||
|
@ -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<void> _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<void> _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<void> _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<void> _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<TextNode>();
|
||||
expect(nodes.length, 3);
|
||||
@ -158,11 +192,20 @@ Future<void> _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<TextNode>();
|
||||
expect(nodes.length, 3);
|
||||
@ -196,8 +239,11 @@ Future<void> _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<void> _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<void> _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);
|
||||
|
@ -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()));
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
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(command: '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(
|
||||
windowsCommand: 'alt+arrow left',
|
||||
macOSCommand: 'alt+arrow left',
|
||||
);
|
||||
}
|
||||
}
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user