mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: #1624 add shortcut for Shift + Option + Left/Right Arrow
This commit is contained in:
parent
2006d35a50
commit
35a72f701b
@ -23,8 +23,8 @@ class SettingsLocation {
|
||||
|
||||
String? get path {
|
||||
if (Platform.isMacOS) {
|
||||
// remove the prefix `/Volumes/Macintosh HD/Users/`
|
||||
return _path?.replaceFirst('/Volumes/Macintosh HD/Users', '');
|
||||
// remove the prefix `/Volumes/*`
|
||||
return _path?.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
|
||||
}
|
||||
return _path;
|
||||
}
|
||||
|
@ -35,8 +35,11 @@ mixin DefaultSelectable {
|
||||
Offset localToGlobal(Offset offset) =>
|
||||
forward.localToGlobal(offset) - baseOffset;
|
||||
|
||||
Selection? getWorldBoundaryInOffset(Offset offset) =>
|
||||
forward.getWorldBoundaryInOffset(offset);
|
||||
Selection? getWordBoundaryInOffset(Offset offset) =>
|
||||
forward.getWordBoundaryInOffset(offset);
|
||||
|
||||
Selection? getWordBoundaryInPosition(Position position) =>
|
||||
forward.getWordBoundaryInPosition(position);
|
||||
|
||||
Position start() => forward.start();
|
||||
|
||||
|
@ -112,7 +112,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||
}
|
||||
|
||||
@override
|
||||
Selection? getWorldBoundaryInOffset(Offset offset) {
|
||||
Selection? getWordBoundaryInOffset(Offset offset) {
|
||||
final localOffset = _renderParagraph.globalToLocal(offset);
|
||||
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
|
||||
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
||||
@ -121,6 +121,15 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
|
||||
return Selection(start: start, end: end);
|
||||
}
|
||||
|
||||
@override
|
||||
Selection? getWordBoundaryInPosition(Position position) {
|
||||
final textPosition = TextPosition(offset: position.offset);
|
||||
final textRange = _renderParagraph.getWordBoundary(textPosition);
|
||||
final start = Position(path: widget.textNode.path, offset: textRange.start);
|
||||
final end = Position(path: widget.textNode.path, offset: textRange.end);
|
||||
return Selection(start: start, end: end);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
assert(selection.isSingle &&
|
||||
|
@ -55,9 +55,13 @@ mixin SelectableMixin<T extends StatefulWidget> on State<T> {
|
||||
///
|
||||
/// Only the widget rendered by [TextNode] need to implement the detail,
|
||||
/// and the rest can return null.
|
||||
Selection? getWorldBoundaryInOffset(Offset start) {
|
||||
return null;
|
||||
}
|
||||
Selection? getWordBoundaryInOffset(Offset start) => null;
|
||||
|
||||
/// For [TextNode] only.
|
||||
///
|
||||
/// Only the widget rendered by [TextNode] need to implement the detail,
|
||||
/// and the rest can return null.
|
||||
Selection? getWordBoundaryInPosition(Position position) => null;
|
||||
|
||||
bool get shouldCursorBlink => true;
|
||||
|
||||
|
@ -289,8 +289,50 @@ ShortcutEventHandler cursorRight = (editorState, event) {
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
ShortcutEventHandler cursorLeftWordSelect = (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.goLeft(editorState, selectionRange: _SelectionRange.word);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
editorState.service.selectionService.updateSelection(
|
||||
selection.copyWith(end: end),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
ShortcutEventHandler cursorRightWordSelect = (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, selectionRange: _SelectionRange.word);
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
editorState.service.selectionService.updateSelection(
|
||||
selection.copyWith(end: end),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
enum _SelectionRange {
|
||||
character,
|
||||
word,
|
||||
}
|
||||
|
||||
extension on Position {
|
||||
Position? goLeft(EditorState editorState) {
|
||||
Position? goLeft(
|
||||
EditorState editorState, {
|
||||
_SelectionRange selectionRange = _SelectionRange.character,
|
||||
}) {
|
||||
final node = editorState.document.nodeAtPath(path);
|
||||
if (node == null) {
|
||||
return null;
|
||||
@ -302,14 +344,38 @@ extension on Position {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (node is TextNode) {
|
||||
return Position(path: path, offset: node.delta.prevRunePosition(offset));
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
switch (selectionRange) {
|
||||
case _SelectionRange.character:
|
||||
if (node is TextNode) {
|
||||
return Position(
|
||||
path: path,
|
||||
offset: node.delta.prevRunePosition(offset),
|
||||
);
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
}
|
||||
case _SelectionRange.word:
|
||||
if (node is TextNode) {
|
||||
final result = node.selectable?.getWordBoundaryInPosition(
|
||||
Position(
|
||||
path: path,
|
||||
offset: node.delta.prevRunePosition(offset),
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
return result.start;
|
||||
}
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Position? goRight(EditorState editorState) {
|
||||
Position? goRight(
|
||||
EditorState editorState, {
|
||||
_SelectionRange selectionRange = _SelectionRange.character,
|
||||
}) {
|
||||
final node = editorState.document.nodeAtPath(path);
|
||||
if (node == null) {
|
||||
return null;
|
||||
@ -322,11 +388,30 @@ extension on Position {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (node is TextNode) {
|
||||
return Position(path: path, offset: node.delta.nextRunePosition(offset));
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
switch (selectionRange) {
|
||||
case _SelectionRange.character:
|
||||
if (node is TextNode) {
|
||||
return Position(
|
||||
path: path, offset: node.delta.nextRunePosition(offset));
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
}
|
||||
case _SelectionRange.word:
|
||||
if (node is TextNode) {
|
||||
final result = node.selectable?.getWordBoundaryInPosition(
|
||||
Position(
|
||||
path: path,
|
||||
offset: node.delta.nextRunePosition(offset),
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
return result.end;
|
||||
}
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,7 +298,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
void _onDoubleTapDown(TapDownDetails details) {
|
||||
final offset = details.globalPosition;
|
||||
final node = getNodeInOffset(offset);
|
||||
final selection = node?.selectable?.getWorldBoundaryInOffset(offset);
|
||||
final selection = node?.selectable?.getWordBoundaryInOffset(offset);
|
||||
if (selection == null) {
|
||||
clearSelection();
|
||||
return;
|
||||
|
@ -48,6 +48,16 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
||||
command: 'shift+arrow down',
|
||||
handler: cursorDownSelect,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Cursor down select',
|
||||
command: 'shift+alt+arrow left',
|
||||
handler: cursorLeftWordSelect,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Cursor down select',
|
||||
command: 'shift+alt+arrow right',
|
||||
handler: cursorRightWordSelect,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Cursor left select',
|
||||
command: 'shift+arrow left',
|
||||
|
@ -341,6 +341,114 @@ void main() async {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Presses shift + alt + arrow left to select a word',
|
||||
(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: 10);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
isAltPressed: true,
|
||||
);
|
||||
// <to>
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [1], offset: 8),
|
||||
),
|
||||
);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
isAltPressed: true,
|
||||
);
|
||||
// < to>
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [1], offset: 7),
|
||||
),
|
||||
);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
isAltPressed: true,
|
||||
);
|
||||
// <Welcome to>
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [1], offset: 0),
|
||||
),
|
||||
);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
isAltPressed: true,
|
||||
);
|
||||
// <😁>
|
||||
// <Welcome to>
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: 22),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Presses shift + alt + arrow left to select a word',
|
||||
(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: 10);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
isAltPressed: true,
|
||||
);
|
||||
// < Appflowy>
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: 19),
|
||||
),
|
||||
);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
isAltPressed: true,
|
||||
);
|
||||
// < Appflowy 😁>
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: 22),
|
||||
),
|
||||
);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
isAltPressed: true,
|
||||
);
|
||||
// < Appflowy 😁>
|
||||
// <>
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [1], offset: 0),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _testPressArrowKeyInNotCollapsedSelection(
|
||||
|
Loading…
Reference in New Issue
Block a user