diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg new file mode 100644 index 0000000000..6e97796956 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h2.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h2.svg new file mode 100644 index 0000000000..2c1d1d9d1c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h3.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h3.svg new file mode 100644 index 0000000000..8c6276263d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/number.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/number.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/number.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/text.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/code.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/code.svg new file mode 100644 index 0000000000..9b96b3c2dc --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index c69237f24f..b6fc3467dc 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -241,6 +241,62 @@ "subtype": "number-list", "number": 3 } + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] } ] } 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 1a68f38ead..856c07e900 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:convert'; import 'package:example/expandable_floating_action_button.dart'; @@ -80,12 +81,21 @@ class _MyHomePageState extends State { icon: const Icon(Icons.note_add), ), ActionButton( + icon: const Icon(Icons.document_scanner), onPressed: () { if (page == 1) return; setState(() { page = 1; }); }, + ), + ActionButton( + onPressed: () { + if (page == 2) return; + setState(() { + page = 2; + }); + }, icon: const Icon(Icons.text_fields), ), ], @@ -97,11 +107,41 @@ class _MyHomePageState extends State { if (page == 0) { return _buildFlowyEditor(); } else if (page == 1) { + return _buildFlowyEditorWithEmptyDocument(); + } else if (page == 2) { return _buildTextField(); } return Container(); } + Widget _buildFlowyEditorWithEmptyDocument() { + return Container( + padding: const EdgeInsets.only(left: 20, right: 20), + child: FlowyEditor( + key: editorKey, + editorState: EditorState( + document: StateTree( + root: Node( + type: 'editor', + children: LinkedList() + ..add( + TextNode.empty() + ..delta = Delta( + [TextInsert('')], + ), + ), + attributes: {}, + ), + ), + ), + keyEventHandlers: const [], + customBuilders: { + 'image': ImageNodeBuilder(), + }, + ), + ); + } + Widget _buildFlowyEditor() { return FutureBuilder( future: rootBundle.loadString('assets/example.json'), diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index 8a0f4ea223..a7eb6e1446 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock @@ -83,6 +83,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + url: "https://pub.dartlang.org" + source: hosted + version: "5.4.3+7" flutter_lints: dependency: "direct dev" description: diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 9a80a73a0a..0c58de8b7d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: path: ../ provider: ^6.0.3 url_launcher: ^6.1.5 + flutter_inappwebview: ^5.4.3+7 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 92a05fc880..5ea49c644d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -60,10 +60,8 @@ class EditorState { for (final op in transaction.operations) { _applyOperation(op); } - // updateCursorSelection(transaction.afterSelection); - // FIXME: don't use delay - Future.delayed(const Duration(milliseconds: 16), () { + WidgetsBinding.instance.addPostFrameCallback((_) { updateCursorSelection(transaction.afterSelection); }); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart index 52b7596240..b421b258b6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -9,6 +9,7 @@ extension NodeExtensions on Node { RenderBox? get renderBox => key?.currentContext?.findRenderObject()?.unwrapOrNull(); + BuildContext? get context => key?.currentContext; Selectable? get selectable => key?.currentState?.unwrapOrNull(); bool inSelection(Selection selection) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart index 136b5db4bc..12da5b5dc8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart @@ -18,20 +18,20 @@ class FlowySvg extends StatelessWidget { @override Widget build(BuildContext context) { if (name != null) { - return SizedBox.fromSize( - size: size, - child: SvgPicture.asset( - 'assets/images/$name.svg', - color: color, - package: 'flowy_editor', - ), + return SvgPicture.asset( + 'assets/images/$name.svg', + color: color, + package: 'flowy_editor', + width: size.width, + height: size.width, ); } else if (number != null) { final numberText = '$number.'; - return SizedBox.fromSize( - size: size, - child: SvgPicture.string(numberText), + return SvgPicture.string( + numberText, + width: size.width, + height: size.width, ); } return Container(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart index 650732f9f9..fa32743b02 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart @@ -1,7 +1,8 @@ +import 'package:flutter/material.dart'; + import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flutter/material.dart'; class EditorEntryWidgetBuilder extends NodeWidgetBuilder { @override @@ -31,28 +32,26 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (child) => - editorState.service.renderPluginService.buildPluginWidget( - child is TextNode - ? NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ) - : NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ), - ), - ) - .toList(), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (child) => + editorState.service.renderPluginService.buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ), + ), + ) + .toList(), ); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index f302fcaba8..83d809745c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -1,16 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { @override Widget build(NodeWidgetContext context) { @@ -129,7 +129,10 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildRichText(BuildContext context) { - return _buildSingleRichText(context); + return Align( + alignment: Alignment.centerLeft, + child: _buildSingleRichText(context), + ); } Widget _buildSingleRichText(BuildContext context) { @@ -170,11 +173,12 @@ class _FlowyRichTextState extends State with Selectable { } TextSpan get _textSpan => TextSpan( - children: widget.textNode.delta.operations - .whereType() - .map((insert) => RichTextStyle( - attributes: insert.attributes ?? {}, - text: insert.content, - ).toTextSpan()) - .toList(growable: false)); + children: widget.textNode.delta.operations + .whereType() + .map((insert) => RichTextStyle( + attributes: insert.attributes ?? {}, + text: insert.content, + ).toTextSpan()) + .toList(growable: false), + ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart index 91659e1d1f..1314260bca 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -201,6 +201,7 @@ class _ToolbarWidgetState extends State { ), ); }); + // TODO: disable scrolling. Overlay.of(context)?.insert(_listToolbarOverlay!); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 79e7bfe077..8e0e3e35b0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -1,5 +1,7 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/extensions/text_node_extensions.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; @@ -46,13 +48,20 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) { final builder = TransactionBuilder(editorState); for (final textNode in textNodes) { - builder.updateNode( - textNode, - Attributes.fromIterable( - StyleKey.globalStyleKeys, - value: (_) => null, - )..addAll(attributes), - ); + builder + ..updateNode( + textNode, + Attributes.fromIterable( + StyleKey.globalStyleKeys, + value: (_) => null, + )..addAll(attributes), + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: textNode.toRawString().length, + ), + ); } builder.commit(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 39382d3a9c..d1fb4aac9c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,5 +1,3 @@ -import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; @@ -7,18 +5,21 @@ import 'package:flowy_editor/render/editor/editor_entry.dart'; import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/service/input_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/service/selection_service.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/render/rich_text/quoted_text.dart'; +import 'package:flowy_editor/service/input_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/scroll_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; import 'package:flowy_editor/service/toolbar_service.dart'; NodeWidgetBuilders defaultBuilders = { @@ -62,6 +63,8 @@ class FlowyEditor extends StatefulWidget { } class _FlowyEditorState extends State { + late ScrollController _scrollController; + EditorState get editorState => widget.editorState; @override @@ -71,6 +74,13 @@ class _FlowyEditorState extends State { editorState.service.renderPluginService = _createRenderPlugin(); } + @override + void dispose() { + _scrollController.dispose(); + + super.dispose(); + } + @override void didUpdateWidget(covariant FlowyEditor oldWidget) { super.didUpdateWidget(oldWidget); @@ -82,33 +92,36 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { - return FlowySelection( - key: editorState.service.selectionServiceKey, - editorState: editorState, - child: FlowyInput( - key: editorState.service.inputServiceKey, - editorState: editorState, - child: FlowyKeyboard( - key: editorState.service.keyboardServiceKey, - handlers: [ - ...defaultKeyEventHandler, - ...widget.keyEventHandlers, - ], + return FlowyScroll( + key: editorState.service.scrollServiceKey, + child: FlowySelection( + key: editorState.service.selectionServiceKey, editorState: editorState, - child: FlowyToolbar( - key: editorState.service.toolbarServiceKey, + child: FlowyInput( + key: editorState.service.inputServiceKey, editorState: editorState, - child: editorState.service.renderPluginService.buildPluginWidget( - NodeWidgetContext( - context: context, - node: editorState.document.root, + child: FlowyKeyboard( + key: editorState.service.keyboardServiceKey, + handlers: [ + ...defaultKeyEventHandler, + ...widget.keyEventHandlers, + ], + editorState: editorState, + child: FlowyToolbar( + key: editorState.service.toolbarServiceKey, editorState: editorState, + child: + editorState.service.renderPluginService.buildPluginWidget( + NodeWidgetContext( + context: context, + node: editorState.document.root, + editorState: editorState, + ), + ), ), ), ), - ), - ), - ); + )); } FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart deleted file mode 100644 index f424bcf314..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -/// type '/' to trigger shortcut widget -FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.slash) { - return KeyEventResult.ignored; - } - - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart new file mode 100644 index 0000000000..db3db2e1ad --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart @@ -0,0 +1,300 @@ +import 'dart:math'; + +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +final List _popupListItems = [ + PopupListItem( + text: 'Text', + icon: _popupListIcon('text'), + handler: (editorState) => formatText(editorState), + ), + PopupListItem( + text: 'Heading 1', + icon: _popupListIcon('h1'), + handler: (editorState) => formatHeading(editorState, StyleKey.h1), + ), + PopupListItem( + text: 'Heading 2', + icon: _popupListIcon('h2'), + handler: (editorState) => formatHeading(editorState, StyleKey.h2), + ), + PopupListItem( + text: 'Heading 3', + icon: _popupListIcon('h3'), + handler: (editorState) => formatHeading(editorState, StyleKey.h3), + ), + PopupListItem( + text: 'Bullets', + icon: _popupListIcon('bullets'), + handler: (editorState) => formatBulletedList(editorState), + ), + PopupListItem( + text: 'Numbered list', + icon: _popupListIcon('number'), + handler: (editorState) => debugPrint('Not implement yet!'), + ), + PopupListItem( + text: 'Checkboxes', + icon: _popupListIcon('checkbox'), + handler: (editorState) => formatCheckbox(editorState), + ), +]; + +OverlayEntry? _popupListOverlay; +EditorState? _editorState; +FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.slash) { + return KeyEventResult.ignored; + } + + final textNodes = editorState + .service.selectionService.currentSelectedNodes.value + .whereType(); + if (textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection; + final textNode = textNodes.first; + final context = textNode.context; + final selectable = textNode.selectable; + if (selection == null || context == null || selectable == null) { + return KeyEventResult.ignored; + } + + final rect = selectable.getCursorRectInPosition(selection.start); + final offset = selectable.localToGlobal(rect.topLeft); + if (!selection.isCollapsed) { + TransactionBuilder(editorState) + ..deleteText( + textNode, + selection.start.offset, + selection.end.offset - selection.start.offset, + ) + ..commit(); + } + + _popupListOverlay?.remove(); + _popupListOverlay = OverlayEntry( + builder: (context) => Positioned( + top: offset.dy + 15.0, + left: offset.dx + 5.0, + child: PopupListWidget( + editorState: editorState, + items: _popupListItems, + ), + ), + ); + + Overlay.of(context)?.insert(_popupListOverlay!); + + editorState.service.selectionService.currentSelectedNodes + .removeListener(clearPopupListOverlay); + editorState.service.selectionService.currentSelectedNodes + .addListener(clearPopupListOverlay); + // editorState.service.keyboardService?.disable(); + _editorState = editorState; + + return KeyEventResult.handled; +}; + +void clearPopupListOverlay() { + _popupListOverlay?.remove(); + _popupListOverlay = null; + + _editorState?.service.keyboardService?.enable(); + _editorState = null; +} + +class PopupListWidget extends StatefulWidget { + const PopupListWidget({ + Key? key, + required this.editorState, + required this.items, + this.maxItemInRow = 5, + }) : super(key: key); + + final EditorState editorState; + final List items; + final int maxItemInRow; + + @override + State createState() => _PopupListWidgetState(); +} + +class _PopupListWidgetState extends State { + final focusNode = FocusNode(debugLabel: 'popup_list_widget'); + var selectedIndex = 0; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + } + + @override + void dispose() { + focusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: focusNode, + onKey: _onKey, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumns(widget.items, selectedIndex), + ), + ), + ); + } + + List _buildColumns(List items, int selectedIndex) { + List columns = []; + List itemWidgets = []; + for (var i = 0; i < items.length; i++) { + if (i != 0 && i % (widget.maxItemInRow) == 0) { + columns.add(Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: itemWidgets, + )); + itemWidgets = []; + } + itemWidgets.add(_PopupListItemWidget( + editorState: widget.editorState, + item: items[i], + highlight: selectedIndex == i, + )); + } + if (itemWidgets.isNotEmpty) { + columns.add(Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: itemWidgets, + )); + itemWidgets = []; + } + return columns; + } + + KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + if (event is! RawKeyDownEvent) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.enter) { + widget.items[selectedIndex].handler(widget.editorState); + return KeyEventResult.handled; + } + + var newSelectedIndex = selectedIndex; + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + newSelectedIndex -= widget.maxItemInRow; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + newSelectedIndex += widget.maxItemInRow; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + newSelectedIndex -= 1; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + newSelectedIndex += 1; + } + if (newSelectedIndex != selectedIndex) { + setState(() { + selectedIndex = max(0, min(widget.items.length - 1, newSelectedIndex)); + }); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } +} + +class _PopupListItemWidget extends StatelessWidget { + const _PopupListItemWidget({ + Key? key, + required this.highlight, + required this.item, + required this.editorState, + }) : super(key: key); + + final EditorState editorState; + final PopupListItem item; + final bool highlight; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0), + child: SizedBox( + width: 140, + child: TextButton.icon( + icon: item.icon, + style: ButtonStyle( + alignment: Alignment.centerLeft, + overlayColor: MaterialStateProperty.all( + const Color(0xFFE0F8FF), + ), + backgroundColor: highlight + ? MaterialStateProperty.all(const Color(0xFFE0F8FF)) + : MaterialStateProperty.all(Colors.transparent), + ), + label: Text( + item.text, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.black, + fontSize: 14.0, + ), + ), + onPressed: () { + item.handler(editorState); + }, + ), + ), + ); + } +} + +class PopupListItem { + PopupListItem({ + required this.text, + this.message = '', + required this.icon, + required this.handler, + }); + + final String text; + final String message; + final Widget icon; + final void Function(EditorState editorState) handler; +} + +Widget _popupListIcon(String name) => FlowySvg( + name: 'popup_list/$name', + color: Colors.black, + size: const Size.square(18.0), + ); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart index ebd66894a7..572babeb3a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart @@ -3,6 +3,11 @@ import 'package:flutter/services.dart'; import '../editor_state.dart'; import 'package:flutter/material.dart'; +mixin FlowyKeyboardService on State { + void enable(); + void disable(); +} + typedef FlowyKeyEventHandler = KeyEventResult Function( EditorState editorState, RawKeyEvent event, @@ -25,9 +30,12 @@ class FlowyKeyboard extends StatefulWidget { State createState() => _FlowyKeyboardState(); } -class _FlowyKeyboardState extends State { +class _FlowyKeyboardState extends State + with FlowyKeyboardService { final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service'); + bool isFocus = true; + @override Widget build(BuildContext context) { return Focus( @@ -38,7 +46,30 @@ class _FlowyKeyboardState extends State { ); } + @override + void dispose() { + focusNode.dispose(); + + super.dispose(); + } + + @override + void enable() { + isFocus = true; + focusNode.requestFocus(); + } + + @override + void disable() { + isFocus = false; + focusNode.unfocus(); + } + KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + if (!isFocus) { + return KeyEventResult.ignored; + } + debugPrint('on keyboard event $event'); if (event is! RawKeyDownEvent) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart new file mode 100644 index 0000000000..c3a0a6fedc --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart @@ -0,0 +1,65 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +mixin FlowyScrollService on State { + double get dy; + + void scrollTo(double dy); + + RenderObject? scrollRenderObject(); +} + +class FlowyScroll extends StatefulWidget { + const FlowyScroll({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + State createState() => _FlowyScrollState(); +} + +class _FlowyScrollState extends State with FlowyScrollService { + final _scrollController = ScrollController(); + final _scrollViewKey = GlobalKey(); + + @override + double get dy => _scrollController.position.pixels; + + @override + Widget build(BuildContext context) { + return Listener( + onPointerSignal: _onPointerSignal, + child: SingleChildScrollView( + key: _scrollViewKey, + physics: const NeverScrollableScrollPhysics(), + controller: _scrollController, + child: widget.child, + ), + ); + } + + @override + void scrollTo(double dy) { + _scrollController.position.jumpTo( + dy.clamp( + _scrollController.position.minScrollExtent, + _scrollController.position.maxScrollExtent, + ), + ); + } + + void _onPointerSignal(PointerSignalEvent event) { + if (event is PointerScrollEvent) { + final dy = (_scrollController.position.pixels + event.scrollDelta.dy); + scrollTo(dy); + } + } + + @override + RenderObject? scrollRenderObject() { + return _scrollViewKey.currentContext?.findRenderObject(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 55b08f9279..c54ef90f9f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,13 +1,14 @@ import 'dart:async'; -import 'package:flowy_editor/document/node_iterator.dart'; -import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/node_iterator.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/state_tree.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; @@ -101,102 +102,18 @@ class FlowySelection extends StatefulWidget { State createState() => _FlowySelectionState(); } -/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer] -/// for a while. So we need to implement our own GestureDetector. -@immutable -class _SelectionGestureDetector extends StatefulWidget { - const _SelectionGestureDetector( - {Key? key, - this.child, - this.onTapDown, - this.onDoubleTapDown, - this.onPanStart, - this.onPanUpdate, - this.onPanEnd}) - : super(key: key); - - @override - State<_SelectionGestureDetector> createState() => - _SelectionGestureDetectorState(); - - final Widget? child; - - final GestureTapDownCallback? onTapDown; - final GestureTapDownCallback? onDoubleTapDown; - final GestureDragStartCallback? onPanStart; - final GestureDragUpdateCallback? onPanUpdate; - final GestureDragEndCallback? onPanEnd; -} - -class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> { - bool _isDoubleTap = false; - Timer? _doubleTapTimer; - @override - Widget build(BuildContext context) { - return RawGestureDetector( - behavior: HitTestBehavior.translucent, - gestures: { - PanGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => PanGestureRecognizer(), - (recognizer) { - recognizer - ..onStart = widget.onPanStart - ..onUpdate = widget.onPanUpdate - ..onEnd = widget.onPanEnd; - }, - ), - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (recognizer) { - recognizer.onTapDown = _tapDownDelegate; - }, - ), - }, - child: widget.child, - ); - } - - _tapDownDelegate(TapDownDetails tapDownDetails) { - if (_isDoubleTap) { - _isDoubleTap = false; - _doubleTapTimer?.cancel(); - _doubleTapTimer = null; - if (widget.onDoubleTapDown != null) { - widget.onDoubleTapDown!(tapDownDetails); - } - } else { - if (widget.onTapDown != null) { - widget.onTapDown!(tapDownDetails); - } - - _isDoubleTap = true; - _doubleTapTimer?.cancel(); - _doubleTapTimer = Timer(kDoubleTapTimeout, () { - _isDoubleTap = false; - _doubleTapTimer = null; - }); - } - } - - @override - void dispose() { - _doubleTapTimer?.cancel(); - super.dispose(); - } -} - class _FlowySelectionState extends State with FlowySelectionService, WidgetsBindingObserver { final _cursorKey = GlobalKey(debugLabel: 'cursor'); final List _selectionOverlays = []; final List _cursorOverlays = []; + OverlayEntry? _debugOverlay; /// [Pan] and [Tap] must be mutually exclusive. /// Pan Offset? panStartOffset; + double? panStartScrollDy; Offset? panEndOffset; /// Tap @@ -261,7 +178,7 @@ class _FlowySelectionState extends State @override void updateSelection(Selection selection) { _rects.clear(); - _clearSelection(); + clearSelection(); // cursor if (selection.isCollapsed) { @@ -275,7 +192,19 @@ class _FlowySelectionState extends State @override void clearSelection() { - _clearSelection(); + currentSelection = null; + currentSelectedNodes.value = []; + + // clear selection + _selectionOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + // clear cursors + _cursorOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + // clear toolbar + editorState.service.toolbarService?.hide(); } @override @@ -327,7 +256,7 @@ class _FlowySelectionState extends State } } for (final child in node.children) { - result.addAll(computeNodesInRange(child, start, end)); + result.addAll(_computeNodesInRange(child, start, end)); } return result; } @@ -413,12 +342,24 @@ class _FlowySelectionState extends State clearSelection(); panStartOffset = details.globalPosition; + panStartScrollDy = editorState.service.scrollService?.dy; + + debugPrint('[_onPanStart] panStartOffset = $panStartOffset'); } void _onPanUpdate(DragUpdateDetails details) { - panEndOffset = details.globalPosition; + if (panStartOffset == null || panStartScrollDy == null) { + return; + } - final nodes = getNodesInRange(panStartOffset!, panEndOffset!); + panEndOffset = details.globalPosition; + final dy = editorState.service.scrollService?.dy; + var panStartOffsetWithScrollDyGap = panStartOffset!; + if (dy != null) { + panStartOffsetWithScrollDyGap = + panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy); + } + final nodes = getNodesInRange(panStartOffsetWithScrollDyGap, panEndOffset!); if (nodes.isEmpty) { return; } @@ -429,40 +370,30 @@ class _FlowySelectionState extends State if (first != null && last != null) { bool isDownward; if (first == last) { - isDownward = panStartOffset!.dx < panEndOffset!.dx; + isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx; } else { - isDownward = panStartOffset!.dy < panEndOffset!.dy; + isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy; } - final start = - first.getSelectionInRange(panStartOffset!, panEndOffset!).start; - final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; + final start = first + .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!) + .start; + final end = last + .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!) + .end; final selection = Selection( start: isDownward ? start : end, end: isDownward ? end : start); debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); editorState.updateCursorSelection(selection); } + + _scrollUpOrDownIfNeeded(panEndOffset!); + _showDebugLayerIfNeeded(); } void _onPanEnd(DragEndDetails details) { // do nothing } - void _clearSelection() { - currentSelection = null; - currentSelectedNodes.value = []; - - // clear selection - _selectionOverlays - ..forEach((overlay) => overlay.remove()) - ..clear(); - // clear cursors - _cursorOverlays - ..forEach((overlay) => overlay.remove()) - ..clear(); - // clear toolbar - editorState.service.toolbarService?.hide(); - } - void _updateSelection(Selection selection) { final nodes = _selectedNodesInSelection(editorState.document, selection); @@ -555,12 +486,12 @@ class _FlowySelectionState extends State if (rect != null) { _rects.add(_transformRectToGlobal(selectable!, rect)); final cursor = OverlayEntry( - builder: ((context) => CursorWidget( - key: _cursorKey, - rect: rect, - color: widget.cursorColor, - layerLink: node.layerLink, - )), + builder: (context) => CursorWidget( + key: _cursorKey, + rect: rect, + color: widget.cursorColor, + layerLink: node.layerLink, + ), ); _cursorOverlays.add(cursor); Overlay.of(context)?.insertAll(_cursorOverlays); @@ -579,4 +510,139 @@ class _FlowySelectionState extends State final endNode = stateTree.nodeAtPath(selection.end.path)!; return NodeIterator(stateTree, startNode, endNode).toList(); } + + void _scrollUpOrDownIfNeeded(Offset offset) { + final dy = editorState.service.scrollService?.dy; + if (dy == null) { + assert(false, 'Dy could not be null'); + return; + } + final topLimit = MediaQuery.of(context).size.height * 0.2; + final bottomLimit = MediaQuery.of(context).size.height * 0.8; + + /// TODO: It is necessary to calculate the relative speed + /// according to the gap and move forward more gently. + final distance = 10.0; + if (offset.dy <= topLimit) { + // up + editorState.service.scrollService?.scrollTo(dy - distance); + } else if (offset.dy >= bottomLimit) { + //down + editorState.service.scrollService?.scrollTo(dy + distance); + } + } + + void _showDebugLayerIfNeeded() { + // remove false to show debug overlay. + if (kDebugMode && false) { + _debugOverlay?.remove(); + if (panStartOffset != null) { + _debugOverlay = OverlayEntry( + builder: (context) => Positioned.fromRect( + rect: Rect.fromPoints( + panStartOffset?.translate( + 0, + -(editorState.service.scrollService!.dy - + panStartScrollDy!), + ) ?? + Offset.zero, + panEndOffset ?? Offset.zero) + .translate(0, 0), + child: Container( + color: Colors.red.withOpacity(0.2), + ), + ), + ); + Overlay.of(context)?.insert(_debugOverlay!); + } else { + _debugOverlay = null; + } + } + } +} + +/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer] +/// for a while. So we need to implement our own GestureDetector. +@immutable +class _SelectionGestureDetector extends StatefulWidget { + const _SelectionGestureDetector( + {Key? key, + this.child, + this.onTapDown, + this.onDoubleTapDown, + this.onPanStart, + this.onPanUpdate, + this.onPanEnd}) + : super(key: key); + + @override + State<_SelectionGestureDetector> createState() => + _SelectionGestureDetectorState(); + + final Widget? child; + + final GestureTapDownCallback? onTapDown; + final GestureTapDownCallback? onDoubleTapDown; + final GestureDragStartCallback? onPanStart; + final GestureDragUpdateCallback? onPanUpdate; + final GestureDragEndCallback? onPanEnd; +} + +class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> { + bool _isDoubleTap = false; + Timer? _doubleTapTimer; + @override + Widget build(BuildContext context) { + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + PanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(), + (recognizer) { + recognizer + ..onStart = widget.onPanStart + ..onUpdate = widget.onPanUpdate + ..onEnd = widget.onPanEnd; + }, + ), + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (recognizer) { + recognizer.onTapDown = _tapDownDelegate; + }, + ), + }, + child: widget.child, + ); + } + + _tapDownDelegate(TapDownDetails tapDownDetails) { + if (_isDoubleTap) { + _isDoubleTap = false; + _doubleTapTimer?.cancel(); + _doubleTapTimer = null; + if (widget.onDoubleTapDown != null) { + widget.onDoubleTapDown!(tapDownDetails); + } + } else { + if (widget.onTapDown != null) { + widget.onTapDown!(tapDownDetails); + } + + _isDoubleTap = true; + _doubleTapTimer?.cancel(); + _doubleTapTimer = Timer(kDoubleTapTimeout, () { + _isDoubleTap = false; + _doubleTapTimer = null; + }); + } + } + + @override + void dispose() { + _doubleTapTimer?.cancel(); + super.dispose(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 937a16044a..e36312d7f3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,8 +1,11 @@ -import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/toolbar_service.dart'; -import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/scroll_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; + class FlowyService { // selection service final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service'); @@ -14,6 +17,13 @@ class FlowyService { // keyboard service final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); + FlowyKeyboardService? get keyboardService { + if (keyboardServiceKey.currentState != null && + keyboardServiceKey.currentState is FlowyKeyboardService) { + return keyboardServiceKey.currentState! as FlowyKeyboardService; + } + return null; + } // input service final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service'); @@ -23,10 +33,20 @@ class FlowyService { // toolbar service final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); - ToolbarService? get toolbarService { + FlowyToolbarService? get toolbarService { if (toolbarServiceKey.currentState != null && - toolbarServiceKey.currentState is ToolbarService) { - return toolbarServiceKey.currentState! as ToolbarService; + toolbarServiceKey.currentState is FlowyToolbarService) { + return toolbarServiceKey.currentState! as FlowyToolbarService; + } + return null; + } + + // scroll service + final scrollServiceKey = GlobalKey(debugLabel: 'flowy_scroll_service'); + FlowyScrollService? get scrollService { + if (scrollServiceKey.currentState != null && + scrollServiceKey.currentState is FlowyScrollService) { + return scrollServiceKey.currentState! as FlowyScrollService; } return null; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart index feb293aad4..f2026acb23 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -1,8 +1,9 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/render/selection/toolbar_widget.dart'; import 'package:flutter/material.dart'; -mixin ToolbarService { +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/render/selection/toolbar_widget.dart'; + +mixin FlowyToolbarService { /// Show the toolbar widget beside the offset. void showInOffset(Offset offset, LayerLink layerLink); @@ -24,7 +25,7 @@ class FlowyToolbar extends StatefulWidget { State createState() => _FlowyToolbarState(); } -class _FlowyToolbarState extends State with ToolbarService { +class _FlowyToolbarState extends State with FlowyToolbarService { OverlayEntry? _toolbarOverlay; @override diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index e3a6aab187..05c87f8e33 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -29,6 +29,7 @@ flutter: # To add assets to your package, add an assets section, like this: assets: - assets/images/toolbar/ + - assets/images/popup_list/ - assets/images/ - assets/document.json # - images/a_dot_burr.jpeg