mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #775 from LucasXu0/feat/selection_optimizatiion
feat: optimize selection implement by binary search
This commit is contained in:
commit
9e254f84d4
File diff suppressed because it is too large
Load Diff
@ -9,12 +9,6 @@
|
|||||||
"image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png"
|
"image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "youtube_link",
|
|
||||||
"attributes": {
|
|
||||||
"youtube_link": "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"delta": [
|
"delta": [
|
||||||
|
@ -56,7 +56,6 @@ class MyHomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
late EditorState _editorState;
|
|
||||||
final editorKey = GlobalKey();
|
final editorKey = GlobalKey();
|
||||||
int page = 0;
|
int page = 0;
|
||||||
|
|
||||||
@ -69,137 +68,138 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
floatingActionButton: ExpandableFab(
|
floatingActionButton: _buildExpandableFab(),
|
||||||
distance: 112.0,
|
|
||||||
children: [
|
|
||||||
ActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (page == 0) return;
|
|
||||||
setState(() {
|
|
||||||
page = 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
if (page == 0) {
|
if (page == 0) {
|
||||||
return _buildFlowyEditor();
|
return _buildFlowyEditorWithExample();
|
||||||
} else if (page == 1) {
|
} else if (page == 1) {
|
||||||
return _buildFlowyEditorWithEmptyDocument();
|
return _buildFlowyEditorWithEmptyDocument();
|
||||||
} else if (page == 2) {
|
} else if (page == 2) {
|
||||||
return _buildTextField();
|
return _buildTextField();
|
||||||
|
} else if (page == 3) {
|
||||||
|
return _buildFlowyEditorWithBigDocument();
|
||||||
}
|
}
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildExpandableFab() {
|
||||||
|
return ExpandableFab(
|
||||||
|
distance: 112.0,
|
||||||
|
children: [
|
||||||
|
ActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (page == 0) return;
|
||||||
|
setState(() {
|
||||||
|
page = 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
ActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (page == 3) return;
|
||||||
|
setState(() {
|
||||||
|
page = 3;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.email),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildFlowyEditorWithEmptyDocument() {
|
Widget _buildFlowyEditorWithEmptyDocument() {
|
||||||
return Container(
|
return _buildFlowyEditor(
|
||||||
padding: const EdgeInsets.only(left: 20, right: 20),
|
EditorState(
|
||||||
child: FlowyEditor(
|
document: StateTree(
|
||||||
key: editorKey,
|
root: Node(
|
||||||
editorState: EditorState(
|
type: 'editor',
|
||||||
document: StateTree(
|
children: LinkedList()
|
||||||
root: Node(
|
..add(
|
||||||
type: 'editor',
|
TextNode.empty()
|
||||||
children: LinkedList()
|
..delta = Delta(
|
||||||
..add(
|
[TextInsert('')],
|
||||||
TextNode.empty()
|
),
|
||||||
..delta = Delta(
|
),
|
||||||
[TextInsert('')],
|
attributes: {},
|
||||||
),
|
|
||||||
),
|
|
||||||
attributes: {},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
keyEventHandlers: const [],
|
|
||||||
customBuilders: {
|
|
||||||
'image': ImageNodeBuilder(),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFlowyEditor() {
|
Widget _buildFlowyEditorWithExample() {
|
||||||
return FutureBuilder<String>(
|
return FutureBuilder<String>(
|
||||||
future: rootBundle.loadString('assets/example.json'),
|
future: rootBundle.loadString('assets/example.json'),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
|
final data = Map<String, Object>.from(json.decode(snapshot.data!));
|
||||||
|
return _buildFlowyEditor(EditorState(
|
||||||
|
document: StateTree.fromJson(data),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFlowyEditorWithBigDocument() {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
future: rootBundle.loadString('assets/big_document.json'),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
final data = Map<String, Object>.from(json.decode(snapshot.data!));
|
final data = Map<String, Object>.from(json.decode(snapshot.data!));
|
||||||
final document = StateTree.fromJson(data);
|
return _buildFlowyEditor(EditorState(
|
||||||
_editorState = EditorState(
|
document: StateTree.fromJson(data),
|
||||||
document: document,
|
));
|
||||||
);
|
} else {
|
||||||
return Container(
|
return const Center(
|
||||||
padding: const EdgeInsets.only(left: 20, right: 20),
|
child: CircularProgressIndicator(),
|
||||||
child: FlowyEditor(
|
|
||||||
key: editorKey,
|
|
||||||
editorState: _editorState,
|
|
||||||
keyEventHandlers: const [],
|
|
||||||
customBuilders: {
|
|
||||||
'image': ImageNodeBuilder(),
|
|
||||||
'youtube_link': YouTubeLinkNodeBuilder()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// shortcuts: [
|
|
||||||
// // TODO: this won't work, just a example for now.
|
|
||||||
// {
|
|
||||||
// 'h1': (editorState, eventName) {
|
|
||||||
// debugPrint('shortcut => $eventName');
|
|
||||||
// final selectedNodes = editorState.selectedNodes;
|
|
||||||
// if (selectedNodes.isEmpty) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// final textNode = selectedNodes.first as TextNode;
|
|
||||||
// TransactionBuilder(editorState)
|
|
||||||
// ..formatText(textNode, 0, textNode.toRawString().length, {
|
|
||||||
// 'heading': 'h1',
|
|
||||||
// })
|
|
||||||
// ..commit();
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'bold': (editorState, eventName) =>
|
|
||||||
// debugPrint('shortcut => $eventName')
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'underline': (editorState, eventName) =>
|
|
||||||
// debugPrint('shortcut => $eventName')
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildFlowyEditor(EditorState editorState) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.only(left: 20, right: 20),
|
||||||
|
child: FlowyEditor(
|
||||||
|
key: editorKey,
|
||||||
|
editorState: editorState,
|
||||||
|
keyEventHandlers: const [],
|
||||||
|
customBuilders: {
|
||||||
|
'image': ImageNodeBuilder(),
|
||||||
|
'youtube_link': YouTubeLinkNodeBuilder()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildTextField() {
|
Widget _buildTextField() {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: TextField(),
|
child: TextField(),
|
||||||
|
@ -68,6 +68,7 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- document.json
|
- document.json
|
||||||
- example.json
|
- example.json
|
||||||
|
- big_document.json
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
@ -19,4 +19,12 @@ extension NodeExtensions on Node {
|
|||||||
return selection.end.path <= path && path <= selection.start.path;
|
return selection.end.path <= path && path <= selection.start.path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect get rect {
|
||||||
|
if (renderBox != null) {
|
||||||
|
final boxOffset = renderBox!.localToGlobal(Offset.zero);
|
||||||
|
return boxOffset & renderBox!.size;
|
||||||
|
}
|
||||||
|
return Rect.zero;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,13 +67,13 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
widget.editorState.service.selectionService.currentSelectedNodes
|
widget.editorState.service.selectionService.currentSelection
|
||||||
.addListener(_onSelectionChange);
|
.addListener(_onSelectionChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.editorState.service.selectionService.currentSelectedNodes
|
widget.editorState.service.selectionService.currentSelection
|
||||||
.removeListener(_onSelectionChange);
|
.removeListener(_onSelectionChange);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ void formatBulletedList(EditorState editorState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final textNodes = nodes.whereType<TextNode>().toList();
|
final textNodes = nodes.whereType<TextNode>().toList();
|
||||||
|
|
||||||
if (textNodes.isEmpty) {
|
if (textNodes.isEmpty) {
|
||||||
@ -85,8 +85,8 @@ bool formatStrikethrough(EditorState editorState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
|
bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
|
||||||
final selection = editorState.service.selectionService.currentSelection;
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
||||||
|
|
||||||
if (selection == null || textNodes.isEmpty) {
|
if (selection == null || textNodes.isEmpty) {
|
||||||
@ -107,8 +107,8 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
||||||
final selection = editorState.service.selectionService.currentSelection;
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final textNodes = nodes.whereType<TextNode>().toList();
|
final textNodes = nodes.whereType<TextNode>().toList();
|
||||||
|
|
||||||
if (selection == null || textNodes.isEmpty) {
|
if (selection == null || textNodes.isEmpty) {
|
||||||
|
@ -42,15 +42,15 @@ class _FlowyInputState extends State<FlowyInput>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_editorState.service.selectionService.currentSelectedNodes
|
_editorState.service.selectionService.currentSelection
|
||||||
.addListener(_onSelectedNodesChange);
|
.addListener(_onSelectionChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
close();
|
close();
|
||||||
_editorState.service.selectionService.currentSelectedNodes
|
_editorState.service.selectionService.currentSelection
|
||||||
.removeListener(_onSelectedNodesChange);
|
.removeListener(_onSelectionChange);
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -105,13 +105,12 @@ class _FlowyInputState extends State<FlowyInput>
|
|||||||
|
|
||||||
void _applyInsert(TextEditingDeltaInsertion delta) {
|
void _applyInsert(TextEditingDeltaInsertion delta) {
|
||||||
final selectionService = _editorState.service.selectionService;
|
final selectionService = _editorState.service.selectionService;
|
||||||
final currentSelection = selectionService.currentSelection;
|
final currentSelection = selectionService.currentSelection.value;
|
||||||
if (currentSelection == null) {
|
if (currentSelection == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentSelection.isSingle) {
|
if (currentSelection.isSingle) {
|
||||||
final textNode =
|
final textNode = selectionService.currentSelectedNodes.first as TextNode;
|
||||||
selectionService.currentSelectedNodes.value.first as TextNode;
|
|
||||||
TransactionBuilder(_editorState)
|
TransactionBuilder(_editorState)
|
||||||
..insertText(
|
..insertText(
|
||||||
textNode,
|
textNode,
|
||||||
@ -126,13 +125,12 @@ class _FlowyInputState extends State<FlowyInput>
|
|||||||
|
|
||||||
void _applyReplacement(TextEditingDeltaReplacement delta) {
|
void _applyReplacement(TextEditingDeltaReplacement delta) {
|
||||||
final selectionService = _editorState.service.selectionService;
|
final selectionService = _editorState.service.selectionService;
|
||||||
final currentSelection = selectionService.currentSelection;
|
final currentSelection = selectionService.currentSelection.value;
|
||||||
if (currentSelection == null) {
|
if (currentSelection == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentSelection.isSingle) {
|
if (currentSelection.isSingle) {
|
||||||
final textNode =
|
final textNode = selectionService.currentSelectedNodes.first as TextNode;
|
||||||
selectionService.currentSelectedNodes.value.first as TextNode;
|
|
||||||
final length = delta.replacedRange.end - delta.replacedRange.start;
|
final length = delta.replacedRange.end - delta.replacedRange.start;
|
||||||
TransactionBuilder(_editorState)
|
TransactionBuilder(_editorState)
|
||||||
..replaceText(
|
..replaceText(
|
||||||
@ -209,11 +207,11 @@ class _FlowyInputState extends State<FlowyInput>
|
|||||||
// TODO: implement updateFloatingCursor
|
// TODO: implement updateFloatingCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectedNodesChange() {
|
void _onSelectionChange() {
|
||||||
final textNodes = _editorState
|
final textNodes = _editorState.service.selectionService.currentSelectedNodes
|
||||||
.service.selectionService.currentSelectedNodes.value
|
|
||||||
.whereType<TextNode>();
|
.whereType<TextNode>();
|
||||||
final selection = _editorState.service.selectionService.currentSelection;
|
final selection =
|
||||||
|
_editorState.service.selectionService.currentSelection.value;
|
||||||
// FIXME: upward and selection update.
|
// FIXME: upward and selection update.
|
||||||
if (textNodes.isNotEmpty && selection != null) {
|
if (textNodes.isNotEmpty && selection != null) {
|
||||||
final text = textNodes.fold<String>(
|
final text = textNodes.fold<String>(
|
||||||
|
@ -9,12 +9,12 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
|
|||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final selection = editorState.service.selectionService.currentSelection;
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
// make sure all nodes is [TextNode].
|
// make sure all nodes is [TextNode].
|
||||||
final textNodes = nodes.whereType<TextNode>().toList();
|
final textNodes = nodes.whereType<TextNode>().toList();
|
||||||
if (textNodes.length != nodes.length) {
|
if (textNodes.length != nodes.length) {
|
||||||
|
@ -18,8 +18,8 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) {
|
|||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final selection = editorState.service.selectionService.currentSelection;
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
if (selection == null ||
|
if (selection == null ||
|
||||||
nodes.length != 1 ||
|
nodes.length != 1 ||
|
||||||
nodes.first is! TextNode ||
|
nodes.first is! TextNode ||
|
||||||
|
@ -56,14 +56,13 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final textNodes = editorState
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
.service.selectionService.currentSelectedNodes.value
|
|
||||||
.whereType<TextNode>();
|
.whereType<TextNode>();
|
||||||
if (textNodes.length != 1) {
|
if (textNodes.length != 1) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final selection = editorState.service.selectionService.currentSelection;
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
final textNode = textNodes.first;
|
final textNode = textNodes.first;
|
||||||
final context = textNode.context;
|
final context = textNode.context;
|
||||||
final selectable = textNode.selectable;
|
final selectable = textNode.selectable;
|
||||||
@ -97,9 +96,9 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|||||||
|
|
||||||
Overlay.of(context)?.insert(_popupListOverlay!);
|
Overlay.of(context)?.insert(_popupListOverlay!);
|
||||||
|
|
||||||
editorState.service.selectionService.currentSelectedNodes
|
editorState.service.selectionService.currentSelection
|
||||||
.removeListener(clearPopupListOverlay);
|
.removeListener(clearPopupListOverlay);
|
||||||
editorState.service.selectionService.currentSelectedNodes
|
editorState.service.selectionService.currentSelection
|
||||||
.addListener(clearPopupListOverlay);
|
.addListener(clearPopupListOverlay);
|
||||||
// editorState.service.keyboardService?.disable();
|
// editorState.service.keyboardService?.disable();
|
||||||
_editorState = editorState;
|
_editorState = editorState;
|
||||||
|
@ -9,8 +9,8 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
|
|||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
|
||||||
final selection = editorState.service.selectionService.currentSelection;
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
||||||
|
|
||||||
if (selection == null || textNodes.isEmpty) {
|
if (selection == null || textNodes.isEmpty) {
|
||||||
|
@ -17,20 +17,26 @@ import 'package:flowy_editor/render/selection/selection_widget.dart';
|
|||||||
|
|
||||||
/// Process selection and cursor
|
/// Process selection and cursor
|
||||||
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||||
/// Returns the currently selected [Node]s.
|
/// Returns the current [Selection]
|
||||||
|
ValueNotifier<Selection?> get currentSelection;
|
||||||
|
|
||||||
|
/// Returns the current selected [Node]s.
|
||||||
///
|
///
|
||||||
/// The order of the return is determined according to the selected order.
|
/// The order of the return is determined according to the selected order.
|
||||||
ValueNotifier<List<Node>> get currentSelectedNodes;
|
List<Node> get currentSelectedNodes;
|
||||||
Selection? get currentSelection;
|
|
||||||
|
|
||||||
/// ------------------ Selection ------------------------
|
|
||||||
|
|
||||||
|
/// Update the selection or cursor.
|
||||||
///
|
///
|
||||||
|
/// If selection is collapsed, this method will
|
||||||
|
/// update the position of the cursor.
|
||||||
|
/// Otherwise, will update the selection.
|
||||||
void updateSelection(Selection selection);
|
void updateSelection(Selection selection);
|
||||||
|
|
||||||
///
|
/// Clear the selection or cursor.
|
||||||
void clearSelection();
|
void clearSelection();
|
||||||
|
|
||||||
|
/// ------------------ Selection ------------------------
|
||||||
|
|
||||||
List<Rect> rects();
|
List<Rect> rects();
|
||||||
|
|
||||||
Position? hitTest(Offset? offset);
|
Position? hitTest(Offset? offset);
|
||||||
@ -42,34 +48,22 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
|||||||
|
|
||||||
/// ------------------ Offset ------------------------
|
/// ------------------ Offset ------------------------
|
||||||
|
|
||||||
/// Returns selected [Node]s. Empty list would be returned
|
|
||||||
/// if no nodes are being selected.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// [start] and [end] are the offsets under the global coordinate system.
|
|
||||||
///
|
|
||||||
/// If end is not null, it means multiple selection,
|
|
||||||
/// otherwise single selection.
|
|
||||||
List<Node> getNodesInRange(Offset start, [Offset? end]);
|
|
||||||
|
|
||||||
/// Return the [Node] or [Null] in single selection.
|
/// Return the [Node] or [Null] in single selection.
|
||||||
///
|
///
|
||||||
/// [start] is the offset under the global coordinate system.
|
/// [offset] is under the global coordinate system.
|
||||||
Node? computeNodeInOffset(Node node, Offset offset);
|
Node? getNodeInOffset(Offset offset);
|
||||||
|
|
||||||
/// Return the [Node]s in multiple selection. Empty list would be returned
|
/// Returns selected [Node]s. Empty list would be returned
|
||||||
/// if no nodes are in range.
|
/// if no nodes are in range.
|
||||||
///
|
///
|
||||||
/// [start] is the offset under the global coordinate system.
|
///
|
||||||
List<Node> computeNodesInRange(
|
/// [start] and [end] are under the global coordinate system.
|
||||||
Node node,
|
///
|
||||||
Offset start,
|
List<Node> getNodeInRange(Offset start, Offset end);
|
||||||
Offset end,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Return [bool] to identify the [Node] is in Range or not.
|
/// Return [bool] to identify the [Node] is in Range or not.
|
||||||
///
|
///
|
||||||
/// [start] and [end] are the offsets under the global coordinate system.
|
/// [start] and [end] are under the global coordinate system.
|
||||||
bool isNodeInRange(
|
bool isNodeInRange(
|
||||||
Node node,
|
Node node,
|
||||||
Offset start,
|
Offset start,
|
||||||
@ -78,7 +72,7 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
|||||||
|
|
||||||
/// Return [bool] to identify the [Node] contains [Offset] or not.
|
/// Return [bool] to identify the [Node] contains [Offset] or not.
|
||||||
///
|
///
|
||||||
/// [start] is the offset under the global coordinate system.
|
/// [offset] is under the global coordinate system.
|
||||||
bool isNodeInOffset(Node node, Offset offset);
|
bool isNodeInOffset(Node node, Offset offset);
|
||||||
|
|
||||||
/// ------------------ Offset ------------------------
|
/// ------------------ Offset ------------------------
|
||||||
@ -124,10 +118,10 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Selection? currentSelection;
|
ValueNotifier<Selection?> currentSelection = ValueNotifier(null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ValueNotifier<List<Node>> currentSelectedNodes = ValueNotifier([]);
|
List<Node> currentSelectedNodes = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Node> getNodesInSelection(Selection selection) =>
|
List<Node> getNodesInSelection(Selection selection) =>
|
||||||
@ -145,8 +139,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
super.didChangeMetrics();
|
super.didChangeMetrics();
|
||||||
|
|
||||||
// Need to refresh the selection when the metrics changed.
|
// Need to refresh the selection when the metrics changed.
|
||||||
if (currentSelection != null) {
|
if (currentSelection.value != null) {
|
||||||
updateSelection(currentSelection!);
|
updateSelection(currentSelection.value!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,8 +186,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
currentSelection = null;
|
currentSelectedNodes = [];
|
||||||
currentSelectedNodes.value = [];
|
currentSelection.value = null;
|
||||||
|
|
||||||
// clear selection
|
// clear selection
|
||||||
_selectionOverlays
|
_selectionOverlays
|
||||||
@ -208,57 +202,15 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Node> getNodesInRange(Offset start, [Offset? end]) {
|
Node? getNodeInOffset(Offset offset) {
|
||||||
if (end != null) {
|
return _lowerBoundInDocument(offset);
|
||||||
return computeNodesInRange(editorState.document.root, start, end);
|
|
||||||
} else {
|
|
||||||
final result = computeNodeInOffset(editorState.document.root, start);
|
|
||||||
if (result != null) {
|
|
||||||
return [result];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Node? computeNodeInOffset(Node node, Offset offset) {
|
List<Node> getNodeInRange(Offset start, Offset end) {
|
||||||
for (final child in node.children) {
|
final startNode = _lowerBoundInDocument(start);
|
||||||
final result = computeNodeInOffset(child, offset);
|
final endNode = _upperBoundInDocument(end);
|
||||||
if (result != null) {
|
return NodeIterator(editorState.document, startNode, endNode).toList();
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.parent != null && node.key != null) {
|
|
||||||
if (isNodeInOffset(node, offset)) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Node> computeNodesInRange(Node node, Offset start, Offset end) {
|
|
||||||
final result = _computeNodesInRange(node, start, end);
|
|
||||||
if (start.dy <= end.dy) {
|
|
||||||
// downward
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
// upward
|
|
||||||
return result.reversed.toList(growable: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Node> _computeNodesInRange(Node node, Offset start, Offset end) {
|
|
||||||
List<Node> result = [];
|
|
||||||
if (node.parent != null && node.key != null) {
|
|
||||||
if (isNodeInRange(node, start, end)) {
|
|
||||||
result.add(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final child in node.children) {
|
|
||||||
result.addAll(_computeNodesInRange(child, start, end));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -286,12 +238,12 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
void _onDoubleTapDown(TapDownDetails details) {
|
void _onDoubleTapDown(TapDownDetails details) {
|
||||||
final offset = details.globalPosition;
|
final offset = details.globalPosition;
|
||||||
final nodes = getNodesInRange(offset);
|
final node = getNodeInOffset(offset);
|
||||||
if (nodes.isEmpty) {
|
if (node == null) {
|
||||||
editorState.updateCursorSelection(null);
|
editorState.updateCursorSelection(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final selectable = nodes.first.selectable;
|
final selectable = node.selectable;
|
||||||
if (selectable == null) {
|
if (selectable == null) {
|
||||||
editorState.updateCursorSelection(null);
|
editorState.updateCursorSelection(null);
|
||||||
return;
|
return;
|
||||||
@ -321,13 +273,12 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
editorState.updateCursorSelection(null);
|
editorState.updateCursorSelection(null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final nodes = getNodesInRange(offset);
|
final node = getNodeInOffset(offset);
|
||||||
if (nodes.isEmpty) {
|
if (node == null) {
|
||||||
editorState.updateCursorSelection(null);
|
editorState.updateCursorSelection(null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
assert(nodes.length == 1);
|
final selectable = node.selectable;
|
||||||
final selectable = nodes.first.selectable;
|
|
||||||
if (selectable == null) {
|
if (selectable == null) {
|
||||||
editorState.updateCursorSelection(null);
|
editorState.updateCursorSelection(null);
|
||||||
return null;
|
return null;
|
||||||
@ -359,12 +310,14 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
panStartOffsetWithScrollDyGap =
|
panStartOffsetWithScrollDyGap =
|
||||||
panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy);
|
panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy);
|
||||||
}
|
}
|
||||||
final nodes = getNodesInRange(panStartOffsetWithScrollDyGap, panEndOffset!);
|
|
||||||
if (nodes.isEmpty) {
|
final sortedNodes =
|
||||||
return;
|
editorState.document.root.children.toList(growable: false);
|
||||||
}
|
final first = _lowerBound(
|
||||||
final first = nodes.first.selectable;
|
sortedNodes, panStartOffsetWithScrollDyGap, 0, sortedNodes.length)
|
||||||
final last = nodes.last.selectable;
|
.selectable;
|
||||||
|
final last = _upperBound(sortedNodes, panEndOffset!, 0, sortedNodes.length)
|
||||||
|
.selectable;
|
||||||
|
|
||||||
// compute the selection in range.
|
// compute the selection in range.
|
||||||
if (first != null && last != null) {
|
if (first != null && last != null) {
|
||||||
@ -397,8 +350,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
void _updateSelection(Selection selection) {
|
void _updateSelection(Selection selection) {
|
||||||
final nodes = _selectedNodesInSelection(editorState.document, selection);
|
final nodes = _selectedNodesInSelection(editorState.document, selection);
|
||||||
|
|
||||||
currentSelection = selection;
|
currentSelectedNodes = nodes;
|
||||||
currentSelectedNodes.value = nodes;
|
currentSelection.value = selection;
|
||||||
|
|
||||||
Rect? topmostRect;
|
Rect? topmostRect;
|
||||||
LayerLink? layerLink;
|
LayerLink? layerLink;
|
||||||
@ -478,8 +431,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSelection = Selection.collapsed(position);
|
currentSelectedNodes = [node];
|
||||||
currentSelectedNodes.value = [node];
|
currentSelection.value = Selection.collapsed(position);
|
||||||
|
|
||||||
final selectable = node.selectable;
|
final selectable = node.selectable;
|
||||||
final rect = selectable?.getCursorRectInPosition(position);
|
final rect = selectable?.getCursorRectInPosition(position);
|
||||||
@ -559,6 +512,57 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Node _lowerBoundInDocument(Offset offset) {
|
||||||
|
final sortedNodes =
|
||||||
|
editorState.document.root.children.toList(growable: false);
|
||||||
|
return _lowerBound(sortedNodes, offset, 0, sortedNodes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Node _upperBoundInDocument(Offset offset) {
|
||||||
|
final sortedNodes =
|
||||||
|
editorState.document.root.children.toList(growable: false);
|
||||||
|
return _upperBound(sortedNodes, offset, 0, sortedNodes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO: Supports multi-level nesting,
|
||||||
|
/// currently only single-level nesting is supported
|
||||||
|
// find the first node's rect.bottom <= offset.dy
|
||||||
|
Node _lowerBound(List<Node> sortedNodes, Offset offset, int start, int end) {
|
||||||
|
var min = start;
|
||||||
|
var max = end;
|
||||||
|
while (min <= max) {
|
||||||
|
final mid = min + ((max - min) >> 1);
|
||||||
|
if (sortedNodes[mid].rect.bottom <= offset.dy) {
|
||||||
|
min = mid + 1;
|
||||||
|
} else {
|
||||||
|
max = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortedNodes[min];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO: Supports multi-level nesting,
|
||||||
|
/// currently only single-level nesting is supported
|
||||||
|
// find the first node's rect.top < offset.dy
|
||||||
|
Node _upperBound(
|
||||||
|
List<Node> sortedNodes,
|
||||||
|
Offset offset,
|
||||||
|
int start,
|
||||||
|
int end,
|
||||||
|
) {
|
||||||
|
var min = start;
|
||||||
|
var max = end;
|
||||||
|
while (min <= max) {
|
||||||
|
final mid = min + ((max - min) >> 1);
|
||||||
|
if (sortedNodes[mid].rect.top < offset.dy) {
|
||||||
|
min = mid + 1;
|
||||||
|
} else {
|
||||||
|
max = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortedNodes[max];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
|
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user