From 9b00a25004eadf4a511f9914d100298be9ce9195 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 10:59:59 +0800 Subject: [PATCH 1/7] fix: the height of the selection rects in same line is not same --- .../flowy_editor/example/lib/main.dart | 6 ----- .../src/render/rich_text/flowy_rich_text.dart | 24 +++++++++++++------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 5db7288c76..19c732f6a0 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -21,7 +21,6 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // @@ -64,11 +63,6 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), body: _buildBody(), floatingActionButton: _buildExpandableFab(), ); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index fb0009ad73..01949aa99c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -17,7 +19,7 @@ class FlowyRichText extends StatefulWidget { const FlowyRichText({ Key? key, this.cursorHeight, - this.cursorWidth = 2.0, + this.cursorWidth = 1.0, this.textSpanDecorator, this.placeholderText = ' ', this.placeholderTextSpanDecorator, @@ -41,7 +43,8 @@ class _FlowyRichTextState extends State with Selectable { final _textKey = GlobalKey(); final _placeholderTextKey = GlobalKey(); - final lineHeight = 1.5; + final _lineHeight = 1.5; + double? _cursorHeight; RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @@ -54,6 +57,13 @@ class _FlowyRichTextState extends State with Selectable { return _buildRichText(context); } + @override + void didUpdateWidget(covariant FlowyRichText oldWidget) { + super.didUpdateWidget(oldWidget); + + _cursorHeight = null; + } + @override Position start() => Position(path: widget.textNode.path, offset: 0); @@ -66,7 +76,7 @@ class _FlowyRichTextState extends State with Selectable { final textPosition = TextPosition(offset: position.offset); final cursorOffset = _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); - final cursorHeight = widget.cursorHeight ?? + _cursorHeight ??= widget.cursorHeight ?? _renderParagraph.getFullHeightForCaret(textPosition) ?? _placeholderRenderParagraph.getFullHeightForCaret(textPosition) ?? 18.0; // default height @@ -74,7 +84,7 @@ class _FlowyRichTextState extends State with Selectable { cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, - cursorHeight, + _cursorHeight!, ); } @@ -105,7 +115,7 @@ class _FlowyRichTextState extends State with Selectable { extentOffset: selection.end.offset, ); return _renderParagraph - .getBoxesForSelection(textSelection) + .getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max) .map((box) => box.toRect()) .toList(); } @@ -147,7 +157,7 @@ class _FlowyRichTextState extends State with Selectable { ? Colors.transparent : Colors.grey, fontSize: baseFontSize, - height: lineHeight, + height: _lineHeight, ), ), ], @@ -203,7 +213,7 @@ class _FlowyRichTextState extends State with Selectable { .map((insert) => RichTextStyle( attributes: insert.attributes ?? {}, text: insert.content, - height: lineHeight, + height: _lineHeight, ).toTextSpan()) .toList(growable: false), ); From fe2790fb684526ae121b9cacf2fd90c2375fc2d6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 11:49:54 +0800 Subject: [PATCH 2/7] fix: #811 The height of selection areas in same line is not same. --- .../lib/src/render/rich_text/flowy_rich_text.dart | 2 ++ .../lib/src/render/rich_text/rich_text_style.dart | 12 +----------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 01949aa99c..bdfae73d66 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -174,6 +174,8 @@ class _FlowyRichTextState extends State with Selectable { final textSpan = _textSpan; return RichText( key: _textKey, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, applyHeightToLastDescent: false), text: widget.textSpanDecorator != null ? widget.textSpanDecorator!(textSpan) : textSpan, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart index c924709948..4ac2adc39f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -192,17 +192,7 @@ class RichTextStyle { TextSpan toTextSpan() => _toTextSpan(height); double get topPadding { - if (height == 1.0) { - return 0; - } - // TODO: Need to be optimized. - final painter = - TextPainter(text: _toTextSpan(height), textDirection: TextDirection.ltr) - ..layout(); - final basePainter = - TextPainter(text: _toTextSpan(null), textDirection: TextDirection.ltr) - ..layout(); - return painter.height - basePainter.height; + return 0; } TextSpan _toTextSpan(double? height) { From 3087594b3c4cb79b5391da83070258da68f9f72a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 12:28:12 +0800 Subject: [PATCH 3/7] fix: #814 --- .../flowy_editor/example/lib/main.dart | 5 +- .../src/render/rich_text/flowy_rich_text.dart | 49 ++++++++----------- .../whitespace_handler.dart | 16 +++--- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 19c732f6a0..d33a010b55 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -63,7 +63,10 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: _buildBody(), + body: Container( + alignment: Alignment.topCenter, + child: _buildBody(), + ), floatingActionButton: _buildExpandableFab(), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index bdfae73d66..c77b52b25f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -44,7 +44,6 @@ class _FlowyRichTextState extends State with Selectable { final _placeholderTextKey = GlobalKey(); final _lineHeight = 1.5; - double? _cursorHeight; RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @@ -57,13 +56,6 @@ class _FlowyRichTextState extends State with Selectable { return _buildRichText(context); } - @override - void didUpdateWidget(covariant FlowyRichText oldWidget) { - super.didUpdateWidget(oldWidget); - - _cursorHeight = null; - } - @override Position start() => Position(path: widget.textNode.path, offset: 0); @@ -76,16 +68,18 @@ class _FlowyRichTextState extends State with Selectable { final textPosition = TextPosition(offset: position.offset); final cursorOffset = _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); - _cursorHeight ??= widget.cursorHeight ?? + final cursorHeight = widget.cursorHeight ?? _renderParagraph.getFullHeightForCaret(textPosition) ?? _placeholderRenderParagraph.getFullHeightForCaret(textPosition) ?? - 18.0; // default height - return Rect.fromLTWH( + 16.0; // default height + + final rect = Rect.fromLTWH( cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, - _cursorHeight!, + cursorHeight, ); + return rect; } @override @@ -148,24 +142,13 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildPlaceholderText(BuildContext context) { - final textSpan = TextSpan( - children: [ - TextSpan( - text: widget.placeholderText, - style: TextStyle( - color: widget.textNode.toRawString().isNotEmpty - ? Colors.transparent - : Colors.grey, - fontSize: baseFontSize, - height: _lineHeight, - ), - ), - ], - ); + final textSpan = _placeholderTextSpan; return RichText( key: _placeholderTextKey, - text: widget.placeholderTextSpanDecorator != null - ? widget.placeholderTextSpanDecorator!(textSpan) + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, applyHeightToLastDescent: false), + text: widget.textSpanDecorator != null + ? widget.textSpanDecorator!(textSpan) : textSpan, ); } @@ -219,4 +202,14 @@ class _FlowyRichTextState extends State with Selectable { ).toTextSpan()) .toList(growable: false), ); + + TextSpan get _placeholderTextSpan => TextSpan(children: [ + RichTextStyle( + text: widget.placeholderText, + attributes: { + StyleKey.color: '0xFF707070', + }, + height: _lineHeight, + ).toTextSpan() + ]); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index 41574e6aaa..0deb3d44d2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -39,8 +39,8 @@ FlowyKeyEventHandler whiteSpaceHandler = (editorState, event) { return _toCheckboxList(editorState, textNode); } else if (_bulletedListSymbols.any(text.startsWith)) { return _toBulletedList(editorState, textNode); - } else if (_countOfSign(text) != 0) { - return _toHeadingStyle(editorState, textNode); + } else if (_countOfSign(text, selection) != 0) { + return _toHeadingStyle(editorState, textNode, selection); } return KeyEventResult.ignored; @@ -99,8 +99,12 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) { return KeyEventResult.handled; } -KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) { - final x = _countOfSign(textNode.toRawString()); +KeyEventResult _toHeadingStyle( + EditorState editorState, TextNode textNode, Selection selection) { + final x = _countOfSign( + textNode.toRawString(), + selection, + ); final hX = 'h$x'; if (textNode.attributes.heading == hX) { return KeyEventResult.ignored; @@ -121,9 +125,9 @@ KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) { return KeyEventResult.handled; } -int _countOfSign(String text) { +int _countOfSign(String text, Selection selection) { for (var i = 6; i >= 0; i--) { - if (text.startsWith('#' * i)) { + if (text.substring(0, selection.end.offset).startsWith('#' * i)) { return i; } } From 19838227d9292b7ec38b480a38da002c996cc8e4 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:02:04 +0800 Subject: [PATCH 4/7] feat: #818 improve user experience of the slash command --- .../flowy_editor/lib/src/document/node.dart | 5 +- .../format_rich_text_style.dart | 55 +++++++ .../slash_handler.dart | 155 ++++++++++++++---- 3 files changed, 183 insertions(+), 32 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index 0b6b941aaa..97571663b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -1,7 +1,6 @@ import 'dart:collection'; import 'package:flowy_editor/src/document/path.dart'; import 'package:flowy_editor/src/document/text_delta.dart'; -import 'package:flowy_editor/src/operation/operation.dart'; import 'package:flutter/material.dart'; import './attributes.dart'; @@ -182,12 +181,12 @@ class TextNode extends Node { }) : _delta = delta, super(children: children ?? LinkedList(), attributes: attributes ?? {}); - TextNode.empty() + TextNode.empty({Attributes? attributes}) : _delta = Delta([TextInsert('')]), super( type: 'text', children: LinkedList(), - attributes: {}, + attributes: attributes ?? {}, ); Delta get delta { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index 3dcc519274..6830dd62e4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -4,9 +4,64 @@ import 'package:flowy_editor/src/document/position.dart'; import 'package:flowy_editor/src/document/selection.dart'; import 'package:flowy_editor/src/editor_state.dart'; import 'package:flowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:flowy_editor/src/extensions/path_extensions.dart'; import 'package:flowy_editor/src/operation/transaction_builder.dart'; import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +void insertHeadingAfterSelection(EditorState editorState, String heading) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: heading, + }); +} + +void insertQuoteAfterSelection(EditorState editorState) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.quote, + }); +} + +void insertCheckboxAfterSelection(EditorState editorState) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.checkbox, + StyleKey.checkbox: false, + }); +} + +void insertBulletedListAfterSelection(EditorState editorState) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.bulletedList, + }); +} + +bool insertTextNodeAfterSelection( + EditorState editorState, Attributes attributes) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + if (selection == null || nodes.isEmpty) { + return false; + } + + final node = nodes.first; + if (node is TextNode && node.delta.length == 0) { + formatTextNodes(editorState, attributes); + } else { + final next = selection.end.path.next; + final builder = TransactionBuilder(editorState); + builder + ..insertNode( + next, + TextNode.empty(attributes: attributes), + ) + ..afterSelection = Selection.collapsed( + Position(path: next, offset: 0), + ) + ..commit(); + } + + return true; +} + void formatText(EditorState editorState) { formatTextNodes(editorState, {}); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 6a265808fe..fd0df50fb8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -14,43 +14,56 @@ import 'package:flutter/services.dart'; final List _popupListItems = [ PopupListItem( text: 'Text', + keywords: ['text'], icon: _popupListIcon('text'), - handler: (editorState) => formatText(editorState), + handler: (editorState) { + insertTextNodeAfterSelection(editorState, {}); + }, ), PopupListItem( text: 'Heading 1', + keywords: ['h1', 'heading 1'], icon: _popupListIcon('h1'), - handler: (editorState) => formatHeading(editorState, StyleKey.h1), + handler: (editorState) => + insertHeadingAfterSelection(editorState, StyleKey.h1), ), PopupListItem( text: 'Heading 2', + keywords: ['h2', 'heading 2'], icon: _popupListIcon('h2'), - handler: (editorState) => formatHeading(editorState, StyleKey.h2), + handler: (editorState) => + insertHeadingAfterSelection(editorState, StyleKey.h2), ), PopupListItem( text: 'Heading 3', + keywords: ['h3', 'heading 3'], icon: _popupListIcon('h3'), - handler: (editorState) => formatHeading(editorState, StyleKey.h3), + handler: (editorState) => + insertHeadingAfterSelection(editorState, StyleKey.h3), ), PopupListItem( - text: 'Bullets', + text: 'Bulleted List', + keywords: ['bulleted list'], icon: _popupListIcon('bullets'), - handler: (editorState) => formatBulletedList(editorState), + handler: (editorState) => insertBulletedListAfterSelection(editorState), ), PopupListItem( text: 'Numbered list', + keywords: ['numbered list'], icon: _popupListIcon('number'), handler: (editorState) => debugPrint('Not implement yet!'), ), PopupListItem( text: 'Checkboxes', + keywords: ['checkbox'], icon: _popupListIcon('checkbox'), - handler: (editorState) => formatCheckbox(editorState), + handler: (editorState) => insertCheckboxAfterSelection(editorState), ), ]; OverlayEntry? _popupListOverlay; EditorState? _editorState; +bool _selectionChangeBySlash = false; FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.slash) { return KeyEventResult.ignored; @@ -78,7 +91,7 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { TransactionBuilder(editorState) ..replaceText(textNode, selection.start.offset, - selection.end.offset - selection.start.offset, '/') + selection.end.offset - selection.start.offset, event.character ?? '') ..commit(); _editorState = editorState; @@ -94,7 +107,7 @@ void showPopupList( _popupListOverlay?.remove(); _popupListOverlay = OverlayEntry( builder: (context) => Positioned( - top: offset.dy + 15.0, + top: offset.dy + 20.0, left: offset.dx + 5.0, child: PopupListWidget( editorState: editorState, @@ -117,6 +130,15 @@ void clearPopupList() { if (_popupListOverlay == null || _editorState == null) { return; } + final selection = + _editorState?.service.selectionService.currentSelection.value; + if (selection == null) { + return; + } + if (_selectionChangeBySlash) { + _selectionChangeBySlash = false; + return; + } _popupListOverlay?.remove(); _popupListOverlay = null; @@ -142,21 +164,35 @@ class PopupListWidget extends StatefulWidget { } class _PopupListWidgetState extends State { - final focusNode = FocusNode(debugLabel: 'popup_list_widget'); - var selectedIndex = 0; + final _focusNode = FocusNode(debugLabel: 'popup_list_widget'); + int _selectedIndex = 0; + List _items = []; + String __keyword = ''; + String get _keyword => __keyword; + set _keyword(String keyword) { + __keyword = keyword; + setState(() { + _items = widget.items + .where((item) => + item.keywords.any((keyword) => keyword.contains(_keyword))) + .toList(growable: false); + }); + } @override void initState() { super.initState(); + _items = widget.items; + WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); + _focusNode.requestFocus(); }); } @override void dispose() { - focusNode.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -164,7 +200,7 @@ class _PopupListWidgetState extends State { @override Widget build(BuildContext context) { return Focus( - focusNode: focusNode, + focusNode: _focusNode, onKey: _onKey, child: Container( decoration: BoxDecoration( @@ -178,10 +214,25 @@ class _PopupListWidgetState extends State { ], borderRadius: BorderRadius.circular(6.0), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildColumns(widget.items, selectedIndex), - ), + child: _items.isEmpty + ? Align( + alignment: Alignment.centerLeft, + child: _buildNoResultsWidget(context), + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumns(_items, _selectedIndex), + ), + ), + ); + } + + Widget _buildNoResultsWidget(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'No results', + style: TextStyle(color: Colors.grey, fontSize: 15.0), ), ); } @@ -214,26 +265,52 @@ class _PopupListWidgetState extends State { } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + debugPrint('slash on key $event'); if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } + final arrowKeys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown + ]; + if (event.logicalKey == LogicalKeyboardKey.enter) { - if (0 <= selectedIndex && selectedIndex < widget.items.length) { - _deleteSlash(); - widget.items[selectedIndex].handler(widget.editorState); + if (0 <= _selectedIndex && _selectedIndex < _items.length) { + _deleteLastCharacters(length: _keyword.length + 1); + _items[_selectedIndex].handler(widget.editorState); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { clearPopupList(); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - clearPopupList(); - _deleteSlash(); + if (_keyword.isEmpty) { + clearPopupList(); + } else { + _keyword = _keyword.substring(0, _keyword.length - 1); + } + _deleteLastCharacters(); + return KeyEventResult.handled; + } else if (event.character != null && + !arrowKeys.contains(event.logicalKey)) { + _keyword += event.character!; + _insertText(event.character!); + var maxKeywordLength = 0; + for (final item in _items) { + for (final keyword in item.keywords) { + maxKeywordLength = max(keyword.length, maxKeywordLength); + } + } + if (_keyword.length >= maxKeywordLength + 2) { + clearPopupList(); + } return KeyEventResult.handled; } - var newSelectedIndex = selectedIndex; + var newSelectedIndex = _selectedIndex; if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { newSelectedIndex -= widget.maxItemInRow; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { @@ -243,26 +320,44 @@ class _PopupListWidgetState extends State { } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { newSelectedIndex += 1; } - if (newSelectedIndex != selectedIndex) { + if (newSelectedIndex != _selectedIndex) { setState(() { - selectedIndex = max(0, min(widget.items.length - 1, newSelectedIndex)); + _selectedIndex = max(0, min(_items.length - 1, newSelectedIndex)); }); return KeyEventResult.handled; } return KeyEventResult.ignored; } - void _deleteSlash() { + void _deleteLastCharacters({int length = 1}) { final selection = widget.editorState.service.selectionService.currentSelection.value; final nodes = widget.editorState.service.selectionService.currentSelectedNodes; if (selection != null && nodes.length == 1) { + _selectionChangeBySlash = true; TransactionBuilder(widget.editorState) ..deleteText( nodes.first as TextNode, - selection.start.offset - 1, - 1, + selection.start.offset - length, + length, + ) + ..commit(); + } + } + + void _insertText(String text) { + final selection = + widget.editorState.service.selectionService.currentSelection.value; + final nodes = + widget.editorState.service.selectionService.currentSelectedNodes; + if (selection != null && nodes.length == 1) { + _selectionChangeBySlash = true; + TransactionBuilder(widget.editorState) + ..insertText( + nodes.first as TextNode, + selection.end.offset, + text, ) ..commit(); } @@ -318,12 +413,14 @@ class _PopupListItemWidget extends StatelessWidget { class PopupListItem { PopupListItem({ required this.text, + required this.keywords, this.message = '', required this.icon, required this.handler, }); final String text; + final List keywords; final String message; final Widget icon; final void Function(EditorState editorState) handler; From 1667d14e90e38a27d5be0f36bd5fa3c1f63ef940 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:08:34 +0800 Subject: [PATCH 5/7] chore: rename checkbox to to-do list --- .../internal_key_event_handlers/slash_handler.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index fd0df50fb8..8316ea85d5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -47,14 +47,14 @@ final List _popupListItems = [ icon: _popupListIcon('bullets'), handler: (editorState) => insertBulletedListAfterSelection(editorState), ), + // PopupListItem( + // text: 'Numbered list', + // keywords: ['numbered list'], + // icon: _popupListIcon('number'), + // handler: (editorState) => debugPrint('Not implement yet!'), + // ), PopupListItem( - text: 'Numbered list', - keywords: ['numbered list'], - icon: _popupListIcon('number'), - handler: (editorState) => debugPrint('Not implement yet!'), - ), - PopupListItem( - text: 'Checkboxes', + text: 'To-do List', keywords: ['checkbox'], icon: _popupListIcon('checkbox'), handler: (editorState) => insertCheckboxAfterSelection(editorState), From 6913550f929eee834ed9c90ffa722cc68d96dd48 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:24:25 +0800 Subject: [PATCH 6/7] fix: the popup list position is too lower --- .../internal_key_event_handlers/slash_handler.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 8316ea85d5..2023454a79 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -82,13 +82,10 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { if (selection == null || context == null || selectable == null) { return KeyEventResult.ignored; } - - final rect = selectable.getCursorRectInPosition(selection.start); - if (rect == null) { + final selectionRects = editorState.service.selectionService.selectionRects; + if (selectionRects.isEmpty) { return KeyEventResult.ignored; } - final offset = selectable.localToGlobal(rect.topLeft); - TransactionBuilder(editorState) ..replaceText(textNode, selection.start.offset, selection.end.offset - selection.start.offset, event.character ?? '') @@ -96,7 +93,8 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { _editorState = editorState; WidgetsBinding.instance.addPostFrameCallback((_) { - showPopupList(context, editorState, offset); + _selectionChangeBySlash = false; + showPopupList(context, editorState, selectionRects.first.bottomRight); }); return KeyEventResult.handled; @@ -107,8 +105,8 @@ void showPopupList( _popupListOverlay?.remove(); _popupListOverlay = OverlayEntry( builder: (context) => Positioned( - top: offset.dy + 20.0, - left: offset.dx + 5.0, + top: offset.dy, + left: offset.dx, child: PopupListWidget( editorState: editorState, items: _popupListItems, From 508b276a79cd2f0a3cf4022abd9f312226bb7f27 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:46:20 +0800 Subject: [PATCH 7/7] feat: dismiss popup list if no results --- .../slash_handler.dart | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 2023454a79..83f1b9e13a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -55,7 +55,7 @@ final List _popupListItems = [ // ), PopupListItem( text: 'To-do List', - keywords: ['checkbox'], + keywords: ['checkbox', 'todo'], icon: _popupListIcon('checkbox'), handler: (editorState) => insertCheckboxAfterSelection(editorState), ), @@ -165,16 +165,36 @@ class _PopupListWidgetState extends State { final _focusNode = FocusNode(debugLabel: 'popup_list_widget'); int _selectedIndex = 0; List _items = []; + + int _maxKeywordLength = 0; + String __keyword = ''; String get _keyword => __keyword; set _keyword(String keyword) { __keyword = keyword; - setState(() { - _items = widget.items - .where((item) => - item.keywords.any((keyword) => keyword.contains(_keyword))) - .toList(growable: false); - }); + + final items = widget.items + .where((item) => + item.keywords.any((keyword) => keyword.contains(_keyword))) + .toList(growable: false); + if (items.isNotEmpty) { + var maxKeywordLength = 0; + for (var item in _items) { + for (var keyword in item.keywords) { + maxKeywordLength = max(maxKeywordLength, keyword.length); + } + } + _maxKeywordLength = maxKeywordLength; + } + + if (keyword.length >= _maxKeywordLength + 2) { + clearPopupList(); + } else { + setState(() { + _selectedIndex = 0; + _items = items; + }); + } } @override @@ -213,10 +233,7 @@ class _PopupListWidgetState extends State { borderRadius: BorderRadius.circular(6.0), ), child: _items.isEmpty - ? Align( - alignment: Alignment.centerLeft, - child: _buildNoResultsWidget(context), - ) + ? _buildNoResultsWidget(context) : Row( crossAxisAlignment: CrossAxisAlignment.start, children: _buildColumns(_items, _selectedIndex), @@ -226,11 +243,16 @@ class _PopupListWidgetState extends State { } Widget _buildNoResultsWidget(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'No results', - style: TextStyle(color: Colors.grey, fontSize: 15.0), + return const Align( + alignment: Alignment.centerLeft, + child: Material( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Text( + 'No results', + style: TextStyle(color: Colors.grey), + ), + ), ), ); } @@ -296,15 +318,6 @@ class _PopupListWidgetState extends State { !arrowKeys.contains(event.logicalKey)) { _keyword += event.character!; _insertText(event.character!); - var maxKeywordLength = 0; - for (final item in _items) { - for (final keyword in item.keywords) { - maxKeywordLength = max(keyword.length, maxKeywordLength); - } - } - if (_keyword.length >= maxKeywordLength + 2) { - clearPopupList(); - } return KeyEventResult.handled; }