From 6c9698e377b7a99e3bc50d1203b5db18821b5008 Mon Sep 17 00:00:00 2001 From: Benjamin Quinn Date: Tue, 1 Nov 2022 22:17:51 -0400 Subject: [PATCH] Add typing shortcuts (#994) Start lists with "1." or "-". Indent / Dedent lists with and . --- lib/src/utils/cast.dart | 1 + lib/src/widgets/controller.dart | 20 +++ lib/src/widgets/raw_editor.dart | 143 ++++++++++++++++++--- lib/src/widgets/toolbar/indent_button.dart | 22 +--- 4 files changed, 145 insertions(+), 41 deletions(-) create mode 100644 lib/src/utils/cast.dart diff --git a/lib/src/utils/cast.dart b/lib/src/utils/cast.dart new file mode 100644 index 00000000..9bc50fdb --- /dev/null +++ b/lib/src/utils/cast.dart @@ -0,0 +1 @@ +T? castOrNull(dynamic x) => x is T ? x : null; diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 00c4f37f..fd85666b 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -102,6 +102,26 @@ class QuillController extends ChangeNotifier { .mergeAll(toggledStyle); } + // Increases or decreases the indent of the current selection by 1. + void indentSelection(bool isIncrease) { + final indent = getSelectionStyle().attributes[Attribute.indent.key]; + if (indent == null) { + if (isIncrease) { + formatSelection(Attribute.indentL1); + } + return; + } + if (indent.value == 1 && !isIncrease) { + formatSelection(Attribute.clone(Attribute.indentL1, null)); + return; + } + if (isIncrease) { + formatSelection(Attribute.getIndentLevel(indent.value + 1)); + return; + } + formatSelection(Attribute.getIndentLevel(indent.value - 1)); + } + /// Returns all styles for each node within selection List> getAllIndividualSelectionStyles() { final styles = document.collectAllIndividualStyles( diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index d724bb97..e480bc57 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -19,7 +19,9 @@ import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/embeddable.dart'; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; +import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/style.dart'; +import '../utils/cast.dart'; import '../utils/delta.dart'; import '../utils/embeds.dart'; import '../utils/platform.dart'; @@ -428,6 +430,7 @@ class RawEditorState extends EditorState actions: _actions, child: Focus( focusNode: widget.focusNode, + onKey: _onKey, child: QuillKeyboardListener( child: Container( constraints: constraints, @@ -440,6 +443,125 @@ class RawEditorState extends EditorState ); } + KeyEventResult _onKey(node, RawKeyEvent event) { + // Don't handle key if there is a meta key pressed. + if (event.isAltPressed || event.isControlPressed || event.isMetaPressed) { + return KeyEventResult.ignored; + } + + if (event is! RawKeyDownEvent) { + return KeyEventResult.ignored; + } + + // Don't handle key if there is an active selection. + if (controller.selection.baseOffset != controller.selection.extentOffset) { + return KeyEventResult.ignored; + } + + // Handle indenting blocks when pressing the tab key. + if (event.logicalKey == LogicalKeyboardKey.tab) { + return _handleTabKey(event); + } + + // Handle inserting lists when space is pressed following + // a list initiating phrase. + if (event.logicalKey == LogicalKeyboardKey.space) { + return _handleSpaceKey(event); + } + + return KeyEventResult.ignored; + } + + KeyEventResult _handleSpaceKey(RawKeyEvent event) { + final child = + controller.document.queryChild(controller.selection.baseOffset); + if (child.node == null) { + return KeyEventResult.ignored; + } + + final line = child.node as Line?; + if (line == null) { + return KeyEventResult.ignored; + } + + final text = castOrNull(line.first); + if (text == null) { + return KeyEventResult.ignored; + } + + const olKeyPhrase = '1.'; + const ulKeyPhrase = '-'; + + if (text.value == olKeyPhrase) { + _updateSelectionForKeyPhrase(olKeyPhrase, Attribute.ol); + } else if (text.value == ulKeyPhrase) { + _updateSelectionForKeyPhrase(ulKeyPhrase, Attribute.ul); + } else { + return KeyEventResult.ignored; + } + + return KeyEventResult.handled; + } + + KeyEventResult _handleTabKey(RawKeyEvent event) { + final child = + controller.document.queryChild(controller.selection.baseOffset); + + KeyEventResult insertTabCharacter() { + controller.replaceText(controller.selection.baseOffset, 0, '\t', null); + _moveCursor(1); + return KeyEventResult.handled; + } + + if (child.node == null) { + return insertTabCharacter(); + } + + final node = child.node!; + + final parent = node.parent; + if (parent == null || parent is! Block) { + return insertTabCharacter(); + } + + if (node is! Line || (node.isNotEmpty && node.first is! leaf.Text)) { + return insertTabCharacter(); + } + + if (node.isNotEmpty && (node.first as leaf.Text).value.isNotEmpty) { + return insertTabCharacter(); + } + + final parentBlock = parent; + if (parentBlock.style.containsKey(Attribute.ol.key) || + parentBlock.style.containsKey(Attribute.ul.key) || + parentBlock.style.containsKey(Attribute.checked.key)) { + controller.indentSelection(!event.isShiftPressed); + return KeyEventResult.handled; + } + + return insertTabCharacter(); + } + + void _moveCursor(int chars) { + final selection = controller.selection; + controller.updateSelection( + controller.selection.copyWith( + baseOffset: selection.baseOffset + chars, + extentOffset: selection.baseOffset + chars), + ChangeSource.LOCAL); + } + + void _updateSelectionForKeyPhrase(String phrase, Attribute attribute) { + controller + ..formatSelection(attribute) + ..replaceText(controller.selection.baseOffset - phrase.length, + phrase.length, '', null); + + // It is unclear why the selection moves forward the edit distance. + _moveCursor(-2); + } + void _handleSelectionChanged( TextSelection selection, SelectionChangedCause cause) { final oldSelection = controller.selection; @@ -2076,26 +2198,7 @@ class _IndentSelectionAction extends Action { @override void invoke(IndentSelectionIntent intent, [BuildContext? context]) { - final indent = - state.controller.getSelectionStyle().attributes[Attribute.indent.key]; - if (indent == null) { - if (intent.isIncrease) { - state.controller.formatSelection(Attribute.indentL1); - } - return; - } - if (indent.value == 1 && !intent.isIncrease) { - state.controller - .formatSelection(Attribute.clone(Attribute.indentL1, null)); - return; - } - if (intent.isIncrease) { - state.controller - .formatSelection(Attribute.getIndentLevel(indent.value + 1)); - return; - } - state.controller - .formatSelection(Attribute.getIndentLevel(indent.value - 1)); + state.controller.indentSelection(intent.isIncrease); } @override diff --git a/lib/src/widgets/toolbar/indent_button.dart b/lib/src/widgets/toolbar/indent_button.dart index 227c56e9..24dac736 100644 --- a/lib/src/widgets/toolbar/indent_button.dart +++ b/lib/src/widgets/toolbar/indent_button.dart @@ -42,27 +42,7 @@ class _IndentButtonState extends State { fillColor: iconFillColor, borderRadius: widget.iconTheme?.borderRadius ?? 2, onPressed: () { - final indent = widget.controller - .getSelectionStyle() - .attributes[Attribute.indent.key]; - if (indent == null) { - if (widget.isIncrease) { - widget.controller.formatSelection(Attribute.indentL1); - } - return; - } - if (indent.value == 1 && !widget.isIncrease) { - widget.controller - .formatSelection(Attribute.clone(Attribute.indentL1, null)); - return; - } - if (widget.isIncrease) { - widget.controller - .formatSelection(Attribute.getIndentLevel(indent.value + 1)); - return; - } - widget.controller - .formatSelection(Attribute.getIndentLevel(indent.value - 1)); + widget.controller.indentSelection(widget.isIncrease); }, afterPressed: widget.afterButtonPressed, );