feat: support remote selections (#753)
* chore: delete deprecated code * feat: support remote seleciton * feat: support selection flag
This commit is contained in:
parent
a571f2bc9d
commit
b927ec0685
20 changed files with 424 additions and 210 deletions
|
|
@ -5,6 +5,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:example/pages/collab_editor.dart';
|
import 'package:example/pages/collab_editor.dart';
|
||||||
|
import 'package:example/pages/collab_selection_editor.dart';
|
||||||
import 'package:example/pages/customize_theme_for_editor.dart';
|
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||||
import 'package:example/pages/editor.dart';
|
import 'package:example/pages/editor.dart';
|
||||||
import 'package:example/pages/editor_list.dart';
|
import 'package:example/pages/editor_list.dart';
|
||||||
|
|
@ -168,6 +169,14 @@ class _HomePageState extends State<HomePage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
_buildListTile(context, 'Collab Selection', () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const CollabSelectionEditor(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
_buildListTile(context, 'Custom Theme', () {
|
_buildListTile(context, 'Custom Theme', () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
141
example/lib/pages/collab_selection_editor.dart
Normal file
141
example/lib/pages/collab_selection_editor.dart
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class CollabSelectionEditor extends StatefulWidget {
|
||||||
|
const CollabSelectionEditor({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CollabSelectionEditor> createState() => _CollabSelectionEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollabSelectionEditorState extends State<CollabSelectionEditor> {
|
||||||
|
late final Future<String> future;
|
||||||
|
|
||||||
|
EditorState? editorState;
|
||||||
|
|
||||||
|
late final List<RemoteSelection> remoteSelections = [
|
||||||
|
RemoteSelection(
|
||||||
|
id: '1',
|
||||||
|
selection: Selection(
|
||||||
|
start: Position(path: [0], offset: 0),
|
||||||
|
end: Position(path: [0], offset: 5),
|
||||||
|
),
|
||||||
|
selectionColor: Colors.red,
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
builder: _buildSelectionFlag,
|
||||||
|
),
|
||||||
|
// collapsed selection
|
||||||
|
RemoteSelection(
|
||||||
|
id: '2',
|
||||||
|
selection: Selection.collapsed(
|
||||||
|
Position(path: [1], offset: 3),
|
||||||
|
),
|
||||||
|
selectionColor: Colors.yellow,
|
||||||
|
cursorColor: Colors.yellow,
|
||||||
|
builder: _buildSelectionFlag,
|
||||||
|
),
|
||||||
|
// multi-line selection
|
||||||
|
RemoteSelection(
|
||||||
|
id: '3',
|
||||||
|
selection: Selection(
|
||||||
|
start: Position(path: [2], offset: 0),
|
||||||
|
end: Position(path: [3], offset: 5),
|
||||||
|
),
|
||||||
|
selectionColor: Colors.green,
|
||||||
|
cursorColor: Colors.green,
|
||||||
|
builder: _buildSelectionFlag,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
future = PlatformExtension.isDesktopOrWeb
|
||||||
|
? rootBundle.loadString('assets/example.json')
|
||||||
|
: rootBundle.loadString('assets/mobile_example.json').then((value) {
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
editorState?.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FutureBuilder<String>(
|
||||||
|
future: future,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
editorState?.dispose();
|
||||||
|
editorState ??= EditorState(
|
||||||
|
document: Document.fromJson(jsonDecode(snapshot.data!)),
|
||||||
|
);
|
||||||
|
return AppFlowyEditor(
|
||||||
|
editorState: editorState!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
editorState?.remoteSelections.value = [...remoteSelections];
|
||||||
|
},
|
||||||
|
child: const Text('Add Selection'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSelectionFlag(
|
||||||
|
BuildContext context,
|
||||||
|
RemoteSelection remoteSelection,
|
||||||
|
Rect rect,
|
||||||
|
) {
|
||||||
|
final selection = remoteSelection.selection;
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
return Positioned(
|
||||||
|
top: rect.top - 5.0,
|
||||||
|
left: rect.right,
|
||||||
|
child: const Text(
|
||||||
|
'Lucas.Xu',
|
||||||
|
style: TextStyle(
|
||||||
|
backgroundColor: Colors.yellow,
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
top: rect.top - 5.0,
|
||||||
|
left: rect.left,
|
||||||
|
child: const Text(
|
||||||
|
'Guest',
|
||||||
|
style: TextStyle(
|
||||||
|
backgroundColor: Colors.yellow,
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ class BlockSelectionContainer extends StatelessWidget {
|
||||||
required this.node,
|
required this.node,
|
||||||
required this.delegate,
|
required this.delegate,
|
||||||
required this.listenable,
|
required this.listenable,
|
||||||
|
this.remoteSelection,
|
||||||
this.cursorColor = Colors.black,
|
this.cursorColor = Colors.black,
|
||||||
this.selectionColor = Colors.blue,
|
this.selectionColor = Colors.blue,
|
||||||
this.blockColor = Colors.blue,
|
this.blockColor = Colors.blue,
|
||||||
|
|
@ -24,6 +25,9 @@ class BlockSelectionContainer extends StatelessWidget {
|
||||||
// get the selection from the listenable
|
// get the selection from the listenable
|
||||||
final ValueListenable<Selection?> listenable;
|
final ValueListenable<Selection?> listenable;
|
||||||
|
|
||||||
|
// remote selection
|
||||||
|
final ValueListenable<List<RemoteSelection>>? remoteSelection;
|
||||||
|
|
||||||
// the color of the cursor
|
// the color of the cursor
|
||||||
final Color cursorColor;
|
final Color cursorColor;
|
||||||
|
|
||||||
|
|
@ -50,6 +54,17 @@ class BlockSelectionContainer extends StatelessWidget {
|
||||||
? AlignmentDirectional.topStart
|
? AlignmentDirectional.topStart
|
||||||
: AlignmentDirectional.topEnd,
|
: AlignmentDirectional.topEnd,
|
||||||
children: [
|
children: [
|
||||||
|
if (remoteSelection != null)
|
||||||
|
RemoteBlockSelectionsArea(
|
||||||
|
node: node,
|
||||||
|
delegate: delegate,
|
||||||
|
remoteSelections: remoteSelection!,
|
||||||
|
supportTypes: supportTypes
|
||||||
|
.where(
|
||||||
|
(element) => element != BlockSelectionType.cursor,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
// block selection or selection area
|
// block selection or selection area
|
||||||
BlockSelectionArea(
|
BlockSelectionArea(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
@ -66,6 +81,16 @@ class BlockSelectionContainer extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child,
|
child,
|
||||||
// cursor
|
// cursor
|
||||||
|
// remote cursor
|
||||||
|
if (supportTypes.contains(BlockSelectionType.cursor) &&
|
||||||
|
remoteSelection != null)
|
||||||
|
RemoteBlockSelectionsArea(
|
||||||
|
node: node,
|
||||||
|
delegate: delegate,
|
||||||
|
remoteSelections: remoteSelection!,
|
||||||
|
supportTypes: const [BlockSelectionType.cursor],
|
||||||
|
),
|
||||||
|
// local cursor
|
||||||
if (supportTypes.contains(BlockSelectionType.cursor))
|
if (supportTypes.contains(BlockSelectionType.cursor))
|
||||||
BlockSelectionArea(
|
BlockSelectionArea(
|
||||||
node: node,
|
node: node,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/block_component/base_component/selection/selection_area_painter.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/selection/cursor.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final _deepEqual = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
class RemoteBlockSelectionsArea extends StatelessWidget {
|
||||||
|
const RemoteBlockSelectionsArea({
|
||||||
|
super.key,
|
||||||
|
required this.node,
|
||||||
|
required this.delegate,
|
||||||
|
required this.remoteSelections,
|
||||||
|
this.supportTypes = const [
|
||||||
|
BlockSelectionType.cursor,
|
||||||
|
BlockSelectionType.selection,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the cursor rect or selection rects from the delegate
|
||||||
|
final SelectableMixin delegate;
|
||||||
|
|
||||||
|
final ValueListenable<List<RemoteSelection>> remoteSelections;
|
||||||
|
|
||||||
|
// the node of the block
|
||||||
|
final Node node;
|
||||||
|
|
||||||
|
final List<BlockSelectionType> supportTypes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: remoteSelections,
|
||||||
|
builder: (_, value, child) {
|
||||||
|
child ??= const SizedBox.shrink();
|
||||||
|
final selections = value
|
||||||
|
.where((e) => node.inSelection(e.selection.normalized))
|
||||||
|
.toList();
|
||||||
|
if (selections.isEmpty) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return Positioned.fill(
|
||||||
|
child: Stack(
|
||||||
|
children: selections
|
||||||
|
.map(
|
||||||
|
(e) => RemoteBlockSelectionArea(
|
||||||
|
node: node,
|
||||||
|
delegate: delegate,
|
||||||
|
remoteSelection: e,
|
||||||
|
supportTypes: supportTypes,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [RemoteBlockSelectionArea] is a widget that renders the selection area or the cursor of a block from remote.
|
||||||
|
class RemoteBlockSelectionArea extends StatefulWidget {
|
||||||
|
const RemoteBlockSelectionArea({
|
||||||
|
super.key,
|
||||||
|
required this.node,
|
||||||
|
required this.delegate,
|
||||||
|
required this.remoteSelection,
|
||||||
|
this.supportTypes = const [
|
||||||
|
BlockSelectionType.cursor,
|
||||||
|
BlockSelectionType.selection,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// get the cursor rect or selection rects from the delegate
|
||||||
|
final SelectableMixin delegate;
|
||||||
|
|
||||||
|
final RemoteSelection remoteSelection;
|
||||||
|
|
||||||
|
// the node of the block
|
||||||
|
final Node node;
|
||||||
|
|
||||||
|
final List<BlockSelectionType> supportTypes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RemoteBlockSelectionArea> createState() =>
|
||||||
|
_RemoteBlockSelectionAreaState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RemoteBlockSelectionAreaState extends State<RemoteBlockSelectionArea> {
|
||||||
|
// keep the previous cursor rect to avoid unnecessary rebuild
|
||||||
|
Rect? prevCursorRect;
|
||||||
|
// keep the previous selection rects to avoid unnecessary rebuild
|
||||||
|
List<Rect>? prevSelectionRects;
|
||||||
|
// keep the block selection rect to avoid unnecessary rebuild
|
||||||
|
Rect? prevBlockRect;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_updateSelectionIfNeeded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const child = SizedBox.shrink();
|
||||||
|
final selection = widget.remoteSelection.selection;
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
// show the cursor when the selection is collapsed
|
||||||
|
if (!widget.supportTypes.contains(BlockSelectionType.cursor) ||
|
||||||
|
prevCursorRect == null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
const shouldBlink = false;
|
||||||
|
final cursor = Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Cursor(
|
||||||
|
rect: prevCursorRect!,
|
||||||
|
shouldBlink: shouldBlink,
|
||||||
|
cursorStyle: widget.delegate.cursorStyle,
|
||||||
|
color: widget.remoteSelection.cursorColor,
|
||||||
|
),
|
||||||
|
widget.remoteSelection.builder?.call(
|
||||||
|
context,
|
||||||
|
widget.remoteSelection,
|
||||||
|
prevCursorRect!,
|
||||||
|
) ??
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return cursor;
|
||||||
|
} else {
|
||||||
|
// show the selection area when the selection is not collapsed
|
||||||
|
if (!widget.supportTypes.contains(BlockSelectionType.selection) ||
|
||||||
|
prevSelectionRects == null ||
|
||||||
|
prevSelectionRects!.isEmpty ||
|
||||||
|
(prevSelectionRects!.length == 1 &&
|
||||||
|
prevSelectionRects!.first.width == 0)) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
SelectionAreaPaint(
|
||||||
|
rects: prevSelectionRects!,
|
||||||
|
selectionColor: widget.remoteSelection.selectionColor,
|
||||||
|
),
|
||||||
|
if (selection.start.path.equals(widget.node.path))
|
||||||
|
widget.remoteSelection.builder?.call(
|
||||||
|
context,
|
||||||
|
widget.remoteSelection,
|
||||||
|
prevSelectionRects!.first,
|
||||||
|
) ??
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateSelectionIfNeeded() {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = widget.remoteSelection.selection.normalized;
|
||||||
|
final path = widget.node.path;
|
||||||
|
|
||||||
|
// the current path is in the selection
|
||||||
|
if (path.inSelection(selection)) {
|
||||||
|
if (widget.supportTypes.contains(BlockSelectionType.cursor) &&
|
||||||
|
selection.isCollapsed) {
|
||||||
|
final rect = widget.delegate.getCursorRectInPosition(selection.start);
|
||||||
|
if (rect != prevCursorRect) {
|
||||||
|
setState(() {
|
||||||
|
prevCursorRect = rect;
|
||||||
|
prevBlockRect = null;
|
||||||
|
prevSelectionRects = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (widget.supportTypes.contains(BlockSelectionType.selection)) {
|
||||||
|
final rects = widget.delegate.getRectsInSelection(selection);
|
||||||
|
if (!_deepEqual(rects, prevSelectionRects)) {
|
||||||
|
setState(() {
|
||||||
|
prevSelectionRects = rects;
|
||||||
|
prevCursorRect = null;
|
||||||
|
prevBlockRect = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (prevBlockRect != null ||
|
||||||
|
prevSelectionRects != null ||
|
||||||
|
prevCursorRect != null) {
|
||||||
|
setState(() {
|
||||||
|
prevBlockRect = null;
|
||||||
|
prevSelectionRects = null;
|
||||||
|
prevCursorRect = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
_updateSelectionIfNeeded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RemoteSelection {
|
||||||
|
const RemoteSelection({
|
||||||
|
required this.id,
|
||||||
|
required this.selection,
|
||||||
|
required this.selectionColor,
|
||||||
|
required this.cursorColor,
|
||||||
|
this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final Selection selection;
|
||||||
|
final Color selectionColor;
|
||||||
|
final Color cursorColor;
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
RemoteSelection selection,
|
||||||
|
Rect rect,
|
||||||
|
)? builder;
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,8 @@ export 'base_component/outdent_command.dart';
|
||||||
// selection
|
// selection
|
||||||
export 'base_component/selection/block_selection_area.dart';
|
export 'base_component/selection/block_selection_area.dart';
|
||||||
export 'base_component/selection/block_selection_container.dart';
|
export 'base_component/selection/block_selection_container.dart';
|
||||||
|
export 'base_component/selection/remote_block_selection_area.dart';
|
||||||
|
export 'base_component/selection/remote_selection.dart';
|
||||||
export 'base_component/text_direction_mixin.dart';
|
export 'base_component/text_direction_mixin.dart';
|
||||||
export 'base_component/text_style_configuration.dart';
|
export 'base_component/text_style_configuration.dart';
|
||||||
export 'base_component/widget/full_screen_overlay_entry.dart';
|
export 'base_component/widget/full_screen_overlay_entry.dart';
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ class _BulletedListBlockComponentWidgetState
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ class _DividerBlockComponentWidgetState
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
cursorColor: editorState.editorStyle.cursorColor,
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
selectionColor: editorState.editorStyle.selectionColor,
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,7 @@ class _HeadingBlockComponentWidgetState
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
@ -196,6 +197,7 @@ class ImageBlockComponentWidgetState extends State<ImageBlockComponentWidget>
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
cursorColor: editorState.editorStyle.cursorColor,
|
cursorColor: editorState.editorStyle.cursorColor,
|
||||||
selectionColor: editorState.editorStyle.selectionColor,
|
selectionColor: editorState.editorStyle.selectionColor,
|
||||||
child: child!,
|
child: child!,
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@ class _NumberedListBlockComponentWidgetState
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@ class _ParagraphBlockComponentWidgetState
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ class _AppFlowyRichTextState extends State<AppFlowyRichText>
|
||||||
return BlockSelectionContainer(
|
return BlockSelectionContainer(
|
||||||
delegate: widget.delegate,
|
delegate: widget.delegate,
|
||||||
listenable: widget.editorState.selectionNotifier,
|
listenable: widget.editorState.selectionNotifier,
|
||||||
|
remoteSelection: widget.editorState.remoteSelections,
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
cursorColor: widget.cursorColor,
|
cursorColor: widget.cursorColor,
|
||||||
selectionColor: widget.selectionColor,
|
selectionColor: widget.selectionColor,
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ class _TableBlockComponentWidgetState extends State<TableBlockComponentWidget>
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ class _TodoListBlockComponentWidgetState
|
||||||
node: node,
|
node: node,
|
||||||
delegate: this,
|
delegate: this,
|
||||||
listenable: editorState.selectionNotifier,
|
listenable: editorState.selectionNotifier,
|
||||||
|
remoteSelection: editorState.remoteSelections,
|
||||||
blockColor: editorState.editorStyle.selectionColor,
|
blockColor: editorState.editorStyle.selectionColor,
|
||||||
supportTypes: const [
|
supportTypes: const [
|
||||||
BlockSelectionType.block,
|
BlockSelectionType.block,
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,10 @@ class EditorState {
|
||||||
/// The selection of the editor.
|
/// The selection of the editor.
|
||||||
Selection? get selection => selectionNotifier.value;
|
Selection? get selection => selectionNotifier.value;
|
||||||
|
|
||||||
|
/// Remote selection is the selection from other users.
|
||||||
|
final PropertyValueNotifier<List<RemoteSelection>> remoteSelections =
|
||||||
|
PropertyValueNotifier<List<RemoteSelection>>([]);
|
||||||
|
|
||||||
/// Sets the selection of the editor.
|
/// Sets the selection of the editor.
|
||||||
set selection(Selection? value) {
|
set selection(Selection? value) {
|
||||||
// clear the toggled style when the selection is changed.
|
// clear the toggled style when the selection is changed.
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class CursorWidget extends StatefulWidget {
|
|
||||||
const CursorWidget({
|
|
||||||
super.key,
|
|
||||||
required this.layerLink,
|
|
||||||
required this.rect,
|
|
||||||
required this.color,
|
|
||||||
this.blinkingInterval = 0.5,
|
|
||||||
this.shouldBlink = true,
|
|
||||||
this.cursorStyle = CursorStyle.verticalLine,
|
|
||||||
});
|
|
||||||
|
|
||||||
final double blinkingInterval; // milliseconds
|
|
||||||
final bool shouldBlink;
|
|
||||||
final CursorStyle cursorStyle;
|
|
||||||
final Color color;
|
|
||||||
final Rect rect;
|
|
||||||
final LayerLink layerLink;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CursorWidget> createState() => CursorWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class CursorWidgetState extends State<CursorWidget> {
|
|
||||||
bool showCursor = true;
|
|
||||||
late Timer timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
timer = _initTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
timer.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer _initTimer() {
|
|
||||||
return Timer.periodic(
|
|
||||||
Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()),
|
|
||||||
(timer) => setState(() => showCursor = !showCursor),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// force the cursor widget to show for a while
|
|
||||||
void show() {
|
|
||||||
setState(() {
|
|
||||||
showCursor = true;
|
|
||||||
});
|
|
||||||
timer.cancel();
|
|
||||||
timer = _initTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Positioned.fromRect(
|
|
||||||
rect: widget.rect,
|
|
||||||
child: CompositedTransformFollower(
|
|
||||||
link: widget.layerLink,
|
|
||||||
offset: widget.rect.topCenter,
|
|
||||||
showWhenUnlinked: false,
|
|
||||||
// Ignore the gestures in cursor
|
|
||||||
// to solve the problem that cursor area cannot be selected.
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: _buildCursor(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCursor(BuildContext context) {
|
|
||||||
var color = widget.color;
|
|
||||||
if (widget.shouldBlink && !showCursor) {
|
|
||||||
color = Colors.transparent;
|
|
||||||
}
|
|
||||||
switch (widget.cursorStyle) {
|
|
||||||
case CursorStyle.verticalLine:
|
|
||||||
return Container(
|
|
||||||
color: color,
|
|
||||||
);
|
|
||||||
case CursorStyle.borderLine:
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: color, width: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
case CursorStyle.cover:
|
|
||||||
final size = widget.rect.size;
|
|
||||||
return Container(
|
|
||||||
width: size.width,
|
|
||||||
height: size.height,
|
|
||||||
color: color.withOpacity(0.2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SelectionWidget extends StatefulWidget {
|
|
||||||
const SelectionWidget({
|
|
||||||
super.key,
|
|
||||||
required this.layerLink,
|
|
||||||
required this.rect,
|
|
||||||
required this.color,
|
|
||||||
this.decoration,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Color color;
|
|
||||||
final Rect rect;
|
|
||||||
final LayerLink layerLink;
|
|
||||||
final BoxDecoration? decoration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SelectionWidget> createState() => _SelectionWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SelectionWidgetState extends State<SelectionWidget> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Positioned.fromRect(
|
|
||||||
rect: widget.rect,
|
|
||||||
child: CompositedTransformFollower(
|
|
||||||
link: widget.layerLink,
|
|
||||||
offset: widget.rect.topLeft,
|
|
||||||
showWhenUnlinked: false,
|
|
||||||
// Ignore the gestures in selection overlays
|
|
||||||
// to solve the problem that selection areas cannot overlap.
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: Container(
|
|
||||||
color: widget.decoration == null ? widget.color : null,
|
|
||||||
decoration: widget.decoration,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:appflowy_editor/src/render/selection/cursor_widget.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import '../../test_helper.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('CursorWidget', () {
|
|
||||||
testWidgets('can render verticalLine', (tester) async {
|
|
||||||
final node = Node(type: 'paragraph');
|
|
||||||
await tester.buildAndPump(
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
CursorWidget(
|
|
||||||
layerLink: node.layerLink,
|
|
||||||
rect: const Rect.fromLTWH(0, 0, 50, 50),
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byType(CursorWidget), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('can render borderLine', (tester) async {
|
|
||||||
final node = Node(type: 'paragraph');
|
|
||||||
await tester.buildAndPump(
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
CursorWidget(
|
|
||||||
cursorStyle: CursorStyle.borderLine,
|
|
||||||
layerLink: node.layerLink,
|
|
||||||
rect: const Rect.fromLTWH(0, 0, 50, 50),
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byType(CursorWidget), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('can render cover', (tester) async {
|
|
||||||
final node = Node(type: 'paragraph');
|
|
||||||
await tester.buildAndPump(
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
CursorWidget(
|
|
||||||
cursorStyle: CursorStyle.cover,
|
|
||||||
layerLink: node.layerLink,
|
|
||||||
rect: const Rect.fromLTWH(0, 0, 50, 50),
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byType(CursorWidget), findsOneWidget);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue