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 =
'';
- 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