Merge pull request #768 from LucasXu0/feat/auto_scroll_service
implement autoscrolling on edge touch
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6.5 4L12.5 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6.5 8H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6.5 12H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="4" cy="4" r="0.5" fill="#333333"/>
|
||||||
|
<circle cx="4" cy="8" r="0.5" fill="#333333"/>
|
||||||
|
<circle cx="4" cy="12" r="0.5" fill="#333333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 512 B |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 561 B |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.25344 3.59998H9.63344V12H8.25344V8.36398H4.65344V12H3.27344V3.59998H4.65344V7.04398H8.25344V3.59998Z" fill="#333333"/>
|
||||||
|
<path d="M12.0325 6.39998H12.9925V12H11.8885V7.56798L10.8325 7.86398L10.5605 6.91998L12.0325 6.39998Z" fill="#333333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 354 B |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.67531 3.59998H9.05531V12H7.67531V8.36398H4.07531V12H2.69531V3.59998H4.07531V7.04398H7.67531V3.59998Z" fill="#333333"/>
|
||||||
|
<path d="M10.1104 12V11.176L12.0224 9.20798C12.449 8.75998 12.6624 8.38664 12.6624 8.08798C12.6624 7.86931 12.593 7.69331 12.4544 7.55998C12.321 7.42664 12.1477 7.35998 11.9344 7.35998C11.513 7.35998 11.201 7.57864 10.9984 8.01598L10.0704 7.47198C10.2464 7.08798 10.4997 6.79464 10.8304 6.59198C11.161 6.38931 11.5237 6.28798 11.9184 6.28798C12.425 6.28798 12.8597 6.44798 13.2224 6.76798C13.585 7.08264 13.7664 7.50931 13.7664 8.04798C13.7664 8.62931 13.4597 9.22664 12.8464 9.83998L11.7504 10.936H13.8544V12H10.1104Z" fill="#333333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 771 B |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.62063 3.59998H9.00063V12H7.62063V8.36398H4.02062V12H2.64062V3.59998H4.02062V7.04398H7.62063V3.59998Z" fill="#333333"/>
|
||||||
|
<path d="M12.6637 8.67198C13.0424 8.78398 13.349 8.98131 13.5837 9.26398C13.8237 9.54131 13.9437 9.87731 13.9437 10.272C13.9437 10.848 13.749 11.2986 13.3597 11.624C12.9757 11.9493 12.5037 12.112 11.9437 12.112C11.5064 12.112 11.1144 12.0133 10.7677 11.816C10.4264 11.6133 10.1784 11.3173 10.0237 10.928L10.9677 10.384C11.1064 10.816 11.4317 11.032 11.9437 11.032C12.2264 11.032 12.445 10.9653 12.5997 10.832C12.7597 10.6933 12.8397 10.5066 12.8397 10.272C12.8397 10.0426 12.7597 9.85864 12.5997 9.71998C12.445 9.58131 12.2264 9.51198 11.9437 9.51198H11.7037L11.2797 8.87198L12.3837 7.43198H10.1917V6.39998H13.7117V7.31198L12.6637 8.67198Z" fill="#333333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 892 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z" fill="#333333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.15625 11.8359L6.43768 9.85414H2.46662L1.74805 11.8359H0.5L3.7903 3H5.11399L8.4043 11.8359H7.15625ZM2.87003 8.75596H6.03427L4.44584 4.40112L2.87003 8.75596Z" fill="#333333"/>
|
||||||
|
<path d="M14.4032 5.52454H15.5V11.8359H14.4032V10.7504C13.8569 11.5835 13.0627 12 12.0206 12C11.1381 12 10.386 11.6802 9.76403 11.0407C9.14211 10.3927 8.83114 9.60589 8.83114 8.68022C8.83114 7.75456 9.14211 6.97195 9.76403 6.3324C10.386 5.68443 11.1381 5.36045 12.0206 5.36045C13.0627 5.36045 13.8569 5.777 14.4032 6.6101V5.52454ZM12.1593 10.9397C12.798 10.9397 13.3317 10.7251 13.7603 10.2959C14.1889 9.85835 14.4032 9.31978 14.4032 8.68022C14.4032 8.04067 14.1889 7.50631 13.7603 7.07714C13.3317 6.63955 12.798 6.42076 12.1593 6.42076C11.5289 6.42076 10.9995 6.63955 10.5708 7.07714C10.1422 7.50631 9.92791 8.04067 9.92791 8.68022C9.92791 9.31978 10.1422 9.85835 10.5708 10.2959C10.9995 10.7251 11.5289 10.9397 12.1593 10.9397Z" fill="#333333"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 4L14 8L10 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6 4L2 8L6 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 282 B |
@ -241,6 +241,62 @@
|
|||||||
"subtype": "number-list",
|
"subtype": "number-list",
|
||||||
"number": 3
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:example/expandable_floating_action_button.dart';
|
import 'package:example/expandable_floating_action_button.dart';
|
||||||
@ -80,12 +81,21 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
icon: const Icon(Icons.note_add),
|
icon: const Icon(Icons.note_add),
|
||||||
),
|
),
|
||||||
ActionButton(
|
ActionButton(
|
||||||
|
icon: const Icon(Icons.document_scanner),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (page == 1) return;
|
if (page == 1) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
page = 1;
|
page = 1;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
ActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (page == 2) return;
|
||||||
|
setState(() {
|
||||||
|
page = 2;
|
||||||
|
});
|
||||||
|
},
|
||||||
icon: const Icon(Icons.text_fields),
|
icon: const Icon(Icons.text_fields),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -97,11 +107,41 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
if (page == 0) {
|
if (page == 0) {
|
||||||
return _buildFlowyEditor();
|
return _buildFlowyEditor();
|
||||||
} else if (page == 1) {
|
} else if (page == 1) {
|
||||||
|
return _buildFlowyEditorWithEmptyDocument();
|
||||||
|
} else if (page == 2) {
|
||||||
return _buildTextField();
|
return _buildTextField();
|
||||||
}
|
}
|
||||||
return Container();
|
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() {
|
Widget _buildFlowyEditor() {
|
||||||
return FutureBuilder<String>(
|
return FutureBuilder<String>(
|
||||||
future: rootBundle.loadString('assets/example.json'),
|
future: rootBundle.loadString('assets/example.json'),
|
||||||
|
@ -83,6 +83,13 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -38,6 +38,7 @@ dependencies:
|
|||||||
path: ../
|
path: ../
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
|
flutter_inappwebview: ^5.4.3+7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -60,10 +60,8 @@ class EditorState {
|
|||||||
for (final op in transaction.operations) {
|
for (final op in transaction.operations) {
|
||||||
_applyOperation(op);
|
_applyOperation(op);
|
||||||
}
|
}
|
||||||
// updateCursorSelection(transaction.afterSelection);
|
|
||||||
|
|
||||||
// FIXME: don't use delay
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
Future.delayed(const Duration(milliseconds: 16), () {
|
|
||||||
updateCursorSelection(transaction.afterSelection);
|
updateCursorSelection(transaction.afterSelection);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ extension NodeExtensions on Node {
|
|||||||
RenderBox? get renderBox =>
|
RenderBox? get renderBox =>
|
||||||
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||||
|
|
||||||
|
BuildContext? get context => key?.currentContext;
|
||||||
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
|
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
|
||||||
|
|
||||||
bool inSelection(Selection selection) {
|
bool inSelection(Selection selection) {
|
||||||
|
@ -18,20 +18,20 @@ class FlowySvg extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
return SizedBox.fromSize(
|
return SvgPicture.asset(
|
||||||
size: size,
|
'assets/images/$name.svg',
|
||||||
child: SvgPicture.asset(
|
color: color,
|
||||||
'assets/images/$name.svg',
|
package: 'flowy_editor',
|
||||||
color: color,
|
width: size.width,
|
||||||
package: 'flowy_editor',
|
height: size.width,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else if (number != null) {
|
} else if (number != null) {
|
||||||
final numberText =
|
final numberText =
|
||||||
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
|
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
|
||||||
return SizedBox.fromSize(
|
return SvgPicture.string(
|
||||||
size: size,
|
numberText,
|
||||||
child: SvgPicture.string(numberText),
|
width: size.width,
|
||||||
|
height: size.width,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Container();
|
return Container();
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/document/node.dart';
|
import 'package:flowy_editor/document/node.dart';
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> {
|
class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||||
@override
|
@override
|
||||||
@ -31,28 +32,26 @@ class EditorNodeWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: node.children
|
||||||
children: node.children
|
.map(
|
||||||
.map(
|
(child) =>
|
||||||
(child) =>
|
editorState.service.renderPluginService.buildPluginWidget(
|
||||||
editorState.service.renderPluginService.buildPluginWidget(
|
child is TextNode
|
||||||
child is TextNode
|
? NodeWidgetContext<TextNode>(
|
||||||
? NodeWidgetContext<TextNode>(
|
context: context,
|
||||||
context: context,
|
node: child,
|
||||||
node: child,
|
editorState: editorState,
|
||||||
editorState: editorState,
|
)
|
||||||
)
|
: NodeWidgetContext<Node>(
|
||||||
: NodeWidgetContext<Node>(
|
context: context,
|
||||||
context: context,
|
node: child,
|
||||||
node: child,
|
editorState: editorState,
|
||||||
editorState: editorState,
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
.toList(),
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/node.dart';
|
||||||
|
import 'package:flowy_editor/document/path.dart';
|
||||||
import 'package:flowy_editor/document/position.dart';
|
import 'package:flowy_editor/document/position.dart';
|
||||||
import 'package:flowy_editor/document/selection.dart';
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
import 'package:flowy_editor/document/text_delta.dart';
|
import 'package:flowy_editor/document/text_delta.dart';
|
||||||
import 'package:flowy_editor/editor_state.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/rich_text/rich_text_style.dart';
|
||||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
|
|
||||||
class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
@override
|
@override
|
||||||
Widget build(NodeWidgetContext<TextNode> context) {
|
Widget build(NodeWidgetContext<TextNode> context) {
|
||||||
@ -129,7 +129,10 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRichText(BuildContext context) {
|
Widget _buildRichText(BuildContext context) {
|
||||||
return _buildSingleRichText(context);
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: _buildSingleRichText(context),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSingleRichText(BuildContext context) {
|
Widget _buildSingleRichText(BuildContext context) {
|
||||||
@ -170,11 +173,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextSpan get _textSpan => TextSpan(
|
TextSpan get _textSpan => TextSpan(
|
||||||
children: widget.textNode.delta.operations
|
children: widget.textNode.delta.operations
|
||||||
.whereType<TextInsert>()
|
.whereType<TextInsert>()
|
||||||
.map((insert) => RichTextStyle(
|
.map((insert) => RichTextStyle(
|
||||||
attributes: insert.attributes ?? {},
|
attributes: insert.attributes ?? {},
|
||||||
text: insert.content,
|
text: insert.content,
|
||||||
).toTextSpan())
|
).toTextSpan())
|
||||||
.toList(growable: false));
|
.toList(growable: false),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -201,6 +201,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
// TODO: disable scrolling.
|
||||||
Overlay.of(context)?.insert(_listToolbarOverlay!);
|
Overlay.of(context)?.insert(_listToolbarOverlay!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:flowy_editor/document/attributes.dart';
|
import 'package:flowy_editor/document/attributes.dart';
|
||||||
import 'package:flowy_editor/document/node.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/editor_state.dart';
|
||||||
import 'package:flowy_editor/extensions/text_node_extensions.dart';
|
import 'package:flowy_editor/extensions/text_node_extensions.dart';
|
||||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||||
@ -46,13 +48,20 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
|||||||
final builder = TransactionBuilder(editorState);
|
final builder = TransactionBuilder(editorState);
|
||||||
|
|
||||||
for (final textNode in textNodes) {
|
for (final textNode in textNodes) {
|
||||||
builder.updateNode(
|
builder
|
||||||
textNode,
|
..updateNode(
|
||||||
Attributes.fromIterable(
|
textNode,
|
||||||
StyleKey.globalStyleKeys,
|
Attributes.fromIterable(
|
||||||
value: (_) => null,
|
StyleKey.globalStyleKeys,
|
||||||
)..addAll(attributes),
|
value: (_) => null,
|
||||||
);
|
)..addAll(attributes),
|
||||||
|
)
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: textNode.path,
|
||||||
|
offset: textNode.toRawString().length,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.commit();
|
builder.commit();
|
||||||
|
@ -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:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/editor_state.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/bulleted_list_text.dart';
|
||||||
import 'package:flowy_editor/render/rich_text/checkbox_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/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/heading_text.dart';
|
||||||
import 'package:flowy_editor/render/rich_text/number_list_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/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';
|
import 'package:flowy_editor/service/toolbar_service.dart';
|
||||||
|
|
||||||
NodeWidgetBuilders defaultBuilders = {
|
NodeWidgetBuilders defaultBuilders = {
|
||||||
@ -62,6 +63,8 @@ class FlowyEditor extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FlowyEditorState extends State<FlowyEditor> {
|
class _FlowyEditorState extends State<FlowyEditor> {
|
||||||
|
late ScrollController _scrollController;
|
||||||
|
|
||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -71,6 +74,13 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
editorState.service.renderPluginService = _createRenderPlugin();
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant FlowyEditor oldWidget) {
|
void didUpdateWidget(covariant FlowyEditor oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
@ -82,33 +92,36 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowySelection(
|
return FlowyScroll(
|
||||||
key: editorState.service.selectionServiceKey,
|
key: editorState.service.scrollServiceKey,
|
||||||
editorState: editorState,
|
child: FlowySelection(
|
||||||
child: FlowyInput(
|
key: editorState.service.selectionServiceKey,
|
||||||
key: editorState.service.inputServiceKey,
|
|
||||||
editorState: editorState,
|
|
||||||
child: FlowyKeyboard(
|
|
||||||
key: editorState.service.keyboardServiceKey,
|
|
||||||
handlers: [
|
|
||||||
...defaultKeyEventHandler,
|
|
||||||
...widget.keyEventHandlers,
|
|
||||||
],
|
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FlowyToolbar(
|
child: FlowyInput(
|
||||||
key: editorState.service.toolbarServiceKey,
|
key: editorState.service.inputServiceKey,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: editorState.service.renderPluginService.buildPluginWidget(
|
child: FlowyKeyboard(
|
||||||
NodeWidgetContext(
|
key: editorState.service.keyboardServiceKey,
|
||||||
context: context,
|
handlers: [
|
||||||
node: editorState.document.root,
|
...defaultKeyEventHandler,
|
||||||
|
...widget.keyEventHandlers,
|
||||||
|
],
|
||||||
|
editorState: editorState,
|
||||||
|
child: FlowyToolbar(
|
||||||
|
key: editorState.service.toolbarServiceKey,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
child:
|
||||||
|
editorState.service.renderPluginService.buildPluginWidget(
|
||||||
|
NodeWidgetContext(
|
||||||
|
context: context,
|
||||||
|
node: editorState.document.root,
|
||||||
|
editorState: editorState,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
|
FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
|
||||||
|
@ -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;
|
|
||||||
};
|
|
@ -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<PopupListItem> _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<TextNode>();
|
||||||
|
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<PopupListItem> items;
|
||||||
|
final int maxItemInRow;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PopupListWidget> createState() => _PopupListWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopupListWidgetState extends State<PopupListWidget> {
|
||||||
|
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<Widget> _buildColumns(List<PopupListItem> items, int selectedIndex) {
|
||||||
|
List<Widget> columns = [];
|
||||||
|
List<Widget> 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),
|
||||||
|
);
|
@ -3,6 +3,11 @@ import 'package:flutter/services.dart';
|
|||||||
import '../editor_state.dart';
|
import '../editor_state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
mixin FlowyKeyboardService<T extends StatefulWidget> on State<T> {
|
||||||
|
void enable();
|
||||||
|
void disable();
|
||||||
|
}
|
||||||
|
|
||||||
typedef FlowyKeyEventHandler = KeyEventResult Function(
|
typedef FlowyKeyEventHandler = KeyEventResult Function(
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
RawKeyEvent event,
|
RawKeyEvent event,
|
||||||
@ -25,9 +30,12 @@ class FlowyKeyboard extends StatefulWidget {
|
|||||||
State<FlowyKeyboard> createState() => _FlowyKeyboardState();
|
State<FlowyKeyboard> createState() => _FlowyKeyboardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FlowyKeyboardState extends State<FlowyKeyboard> {
|
class _FlowyKeyboardState extends State<FlowyKeyboard>
|
||||||
|
with FlowyKeyboardService {
|
||||||
final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service');
|
final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service');
|
||||||
|
|
||||||
|
bool isFocus = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Focus(
|
return Focus(
|
||||||
@ -38,7 +46,30 @@ class _FlowyKeyboardState extends State<FlowyKeyboard> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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) {
|
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||||
|
if (!isFocus) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
debugPrint('on keyboard event $event');
|
debugPrint('on keyboard event $event');
|
||||||
|
|
||||||
if (event is! RawKeyDownEvent) {
|
if (event is! RawKeyDownEvent) {
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
mixin FlowyScrollService<T extends StatefulWidget> on State<T> {
|
||||||
|
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<FlowyScroll> createState() => _FlowyScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlowyScrollState extends State<FlowyScroll> 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flowy_editor/document/node_iterator.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flowy_editor/document/state_tree.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/document/node.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/position.dart';
|
||||||
import 'package:flowy_editor/document/selection.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/editor_state.dart';
|
||||||
import 'package:flowy_editor/extensions/node_extensions.dart';
|
import 'package:flowy_editor/extensions/node_extensions.dart';
|
||||||
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
||||||
@ -101,102 +102,18 @@ class FlowySelection extends StatefulWidget {
|
|||||||
State<FlowySelection> createState() => _FlowySelectionState();
|
State<FlowySelection> 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>(
|
|
||||||
() => PanGestureRecognizer(),
|
|
||||||
(recognizer) {
|
|
||||||
recognizer
|
|
||||||
..onStart = widget.onPanStart
|
|
||||||
..onUpdate = widget.onPanUpdate
|
|
||||||
..onEnd = widget.onPanEnd;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TapGestureRecognizer:
|
|
||||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
|
||||||
() => 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<FlowySelection>
|
class _FlowySelectionState extends State<FlowySelection>
|
||||||
with FlowySelectionService, WidgetsBindingObserver {
|
with FlowySelectionService, WidgetsBindingObserver {
|
||||||
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
||||||
|
|
||||||
final List<OverlayEntry> _selectionOverlays = [];
|
final List<OverlayEntry> _selectionOverlays = [];
|
||||||
final List<OverlayEntry> _cursorOverlays = [];
|
final List<OverlayEntry> _cursorOverlays = [];
|
||||||
|
OverlayEntry? _debugOverlay;
|
||||||
|
|
||||||
/// [Pan] and [Tap] must be mutually exclusive.
|
/// [Pan] and [Tap] must be mutually exclusive.
|
||||||
/// Pan
|
/// Pan
|
||||||
Offset? panStartOffset;
|
Offset? panStartOffset;
|
||||||
|
double? panStartScrollDy;
|
||||||
Offset? panEndOffset;
|
Offset? panEndOffset;
|
||||||
|
|
||||||
/// Tap
|
/// Tap
|
||||||
@ -261,7 +178,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
@override
|
@override
|
||||||
void updateSelection(Selection selection) {
|
void updateSelection(Selection selection) {
|
||||||
_rects.clear();
|
_rects.clear();
|
||||||
_clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed) {
|
||||||
@ -275,7 +192,19 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void clearSelection() {
|
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
|
@override
|
||||||
@ -327,7 +256,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final child in node.children) {
|
for (final child in node.children) {
|
||||||
result.addAll(computeNodesInRange(child, start, end));
|
result.addAll(_computeNodesInRange(child, start, end));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -413,12 +342,24 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
panStartOffset = details.globalPosition;
|
panStartOffset = details.globalPosition;
|
||||||
|
panStartScrollDy = editorState.service.scrollService?.dy;
|
||||||
|
|
||||||
|
debugPrint('[_onPanStart] panStartOffset = $panStartOffset');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanUpdate(DragUpdateDetails details) {
|
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) {
|
if (nodes.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -429,40 +370,30 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
if (first != null && last != null) {
|
if (first != null && last != null) {
|
||||||
bool isDownward;
|
bool isDownward;
|
||||||
if (first == last) {
|
if (first == last) {
|
||||||
isDownward = panStartOffset!.dx < panEndOffset!.dx;
|
isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx;
|
||||||
} else {
|
} else {
|
||||||
isDownward = panStartOffset!.dy < panEndOffset!.dy;
|
isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy;
|
||||||
}
|
}
|
||||||
final start =
|
final start = first
|
||||||
first.getSelectionInRange(panStartOffset!, panEndOffset!).start;
|
.getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
||||||
final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end;
|
.start;
|
||||||
|
final end = last
|
||||||
|
.getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
||||||
|
.end;
|
||||||
final selection = Selection(
|
final selection = Selection(
|
||||||
start: isDownward ? start : end, end: isDownward ? end : start);
|
start: isDownward ? start : end, end: isDownward ? end : start);
|
||||||
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
||||||
editorState.updateCursorSelection(selection);
|
editorState.updateCursorSelection(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_scrollUpOrDownIfNeeded(panEndOffset!);
|
||||||
|
_showDebugLayerIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanEnd(DragEndDetails details) {
|
void _onPanEnd(DragEndDetails details) {
|
||||||
// do nothing
|
// 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) {
|
void _updateSelection(Selection selection) {
|
||||||
final nodes = _selectedNodesInSelection(editorState.document, selection);
|
final nodes = _selectedNodesInSelection(editorState.document, selection);
|
||||||
|
|
||||||
@ -555,12 +486,12 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
if (rect != null) {
|
if (rect != null) {
|
||||||
_rects.add(_transformRectToGlobal(selectable!, rect));
|
_rects.add(_transformRectToGlobal(selectable!, rect));
|
||||||
final cursor = OverlayEntry(
|
final cursor = OverlayEntry(
|
||||||
builder: ((context) => CursorWidget(
|
builder: (context) => CursorWidget(
|
||||||
key: _cursorKey,
|
key: _cursorKey,
|
||||||
rect: rect,
|
rect: rect,
|
||||||
color: widget.cursorColor,
|
color: widget.cursorColor,
|
||||||
layerLink: node.layerLink,
|
layerLink: node.layerLink,
|
||||||
)),
|
),
|
||||||
);
|
);
|
||||||
_cursorOverlays.add(cursor);
|
_cursorOverlays.add(cursor);
|
||||||
Overlay.of(context)?.insertAll(_cursorOverlays);
|
Overlay.of(context)?.insertAll(_cursorOverlays);
|
||||||
@ -579,4 +510,139 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final endNode = stateTree.nodeAtPath(selection.end.path)!;
|
final endNode = stateTree.nodeAtPath(selection.end.path)!;
|
||||||
return NodeIterator(stateTree, startNode, endNode).toList();
|
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>(
|
||||||
|
() => PanGestureRecognizer(),
|
||||||
|
(recognizer) {
|
||||||
|
recognizer
|
||||||
|
..onStart = widget.onPanStart
|
||||||
|
..onUpdate = widget.onPanUpdate
|
||||||
|
..onEnd = widget.onPanEnd;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TapGestureRecognizer:
|
||||||
|
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
|
() => 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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: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 {
|
class FlowyService {
|
||||||
// selection service
|
// selection service
|
||||||
final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service');
|
final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service');
|
||||||
@ -14,6 +17,13 @@ class FlowyService {
|
|||||||
|
|
||||||
// keyboard service
|
// keyboard service
|
||||||
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_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
|
// input service
|
||||||
final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service');
|
final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service');
|
||||||
@ -23,10 +33,20 @@ class FlowyService {
|
|||||||
|
|
||||||
// toolbar service
|
// toolbar service
|
||||||
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
|
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
|
||||||
ToolbarService? get toolbarService {
|
FlowyToolbarService? get toolbarService {
|
||||||
if (toolbarServiceKey.currentState != null &&
|
if (toolbarServiceKey.currentState != null &&
|
||||||
toolbarServiceKey.currentState is ToolbarService) {
|
toolbarServiceKey.currentState is FlowyToolbarService) {
|
||||||
return toolbarServiceKey.currentState! as ToolbarService;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -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';
|
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.
|
/// Show the toolbar widget beside the offset.
|
||||||
void showInOffset(Offset offset, LayerLink layerLink);
|
void showInOffset(Offset offset, LayerLink layerLink);
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ class FlowyToolbar extends StatefulWidget {
|
|||||||
State<FlowyToolbar> createState() => _FlowyToolbarState();
|
State<FlowyToolbar> createState() => _FlowyToolbarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FlowyToolbarState extends State<FlowyToolbar> with ToolbarService {
|
class _FlowyToolbarState extends State<FlowyToolbar> with FlowyToolbarService {
|
||||||
OverlayEntry? _toolbarOverlay;
|
OverlayEntry? _toolbarOverlay;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -29,6 +29,7 @@ flutter:
|
|||||||
# To add assets to your package, add an assets section, like this:
|
# To add assets to your package, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- assets/images/toolbar/
|
- assets/images/toolbar/
|
||||||
|
- assets/images/popup_list/
|
||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/document.json
|
- assets/document.json
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
|