Merge pull request #752 from LucasXu0/feat/flowy_editor_input_service

feat: implement toolbar UI part.
This commit is contained in:
Nathan.fooo 2022-08-02 09:55:10 +08:00 committed by GitHub
commit 598a910c0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 528 additions and 248 deletions

View File

@ -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="M9 8C9.66667 8 11 8.4 11 10C11 11.6 9.66667 12 9 12H6V8M9 8H6M9 8C9.5 8 10.5171 6.97616 10.5 6C10.4806 4.8956 9.5 4 8.5 4H6V8" stroke="white" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@ -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="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 8H12.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 12H12.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="4" r="0.5" fill="white"/>
<circle cx="4" cy="8" r="0.5" fill="white"/>
<circle cx="4" cy="12" r="0.5" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@ -0,0 +1,3 @@
<svg width="1" height="16" viewBox="0 0 1 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1" height="16" rx="0.5" fill="#4F4F4F"/>
</svg>

After

Width:  |  Height:  |  Size: 155 B

View File

@ -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="M8.7 4L7.3 12M8.7 4H11.5M8.7 4H5.9M7.3 12H10.1M7.3 12H4.5" stroke="white" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@ -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="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4742 9.35161C12.8007 8.99566 13 8.52111 13 8C13 6.89543 12.1046 6 11 6C9.89543 6 9 6.89543 9 8C9 9.04413 9.80011 9.90137 10.8207 9.99207L10.0124 11.1682L10.8365 11.7346L12.4742 9.35161Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.47395 7.35186C6.80061 6.99588 7 6.52123 7 6C7 4.89543 6.10457 4 5 4C3.89543 4 3 4.89543 3 6C3 7.04411 3.80008 7.90134 4.82061 7.99206L4.01231 9.16823L4.83645 9.73461L6.47395 7.35186Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99994 3C6.78324 3 5.57768 3.89295 5.26683 5.05868C5.14348 5.52122 5.16418 6.01807 5.36988 6.5H6.53454C6.50039 6.45893 6.46931 6.41816 6.44111 6.37779C6.18329 6.00862 6.14534 5.64531 6.23306 5.31634C6.4222 4.60706 7.21665 4 7.99994 4C8.69325 4 9.21448 4.21587 9.55371 4.46532C9.7248 4.59113 9.84481 4.72187 9.91824 4.83203C9.98388 4.93049 9.99678 4.98806 9.99929 4.99927C9.99983 5.00168 9.99989 5.00194 9.99989 5H10.9999C10.9999 4.73903 10.8893 4.4858 10.7503 4.27735C10.605 4.05938 10.4 3.84637 10.1461 3.65968C9.63538 3.28413 8.90664 3 7.99994 3ZM10.63 9.5H9.4653C9.49948 9.54108 9.53057 9.58188 9.55877 9.62226C9.8166 9.99142 9.85455 10.3547 9.76683 10.6837C9.57769 11.393 8.78324 12 7.99994 12C7.30664 12 6.78541 11.7842 6.44617 11.5347C6.27508 11.4089 6.15508 11.2781 6.08165 11.168C6.01601 11.0695 6.00311 11.012 6.0006 11.0008C6.00006 10.9983 6 10.9981 6 11H5C5 11.261 5.11062 11.5142 5.24958 11.7227C5.39489 11.9406 5.59988 12.1537 5.85377 12.3403C6.36451 12.7159 7.09325 13 7.99994 13C9.21665 13 10.4222 12.1071 10.7331 10.9414C10.8564 10.4788 10.8357 9.98194 10.63 9.5Z" fill="white"/>
<rect width="8" height="1" transform="matrix(1 0 0 -1 4 8.5)" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="12" width="8" height="1" fill="white"/>
<path d="M10.1623 10.2595C9.60377 10.7532 8.88302 11 8 11C7.11698 11 6.39623 10.7532 5.83774 10.2595C5.27925 9.7583 5 9.08883 5 8.25105V3H6.30189V8.17251C6.30189 8.65124 6.44151 9.03273 6.72075 9.31697C7.00755 9.60122 7.43396 9.74334 8 9.74334C8.56604 9.74334 8.98868 9.60122 9.26792 9.31697C9.55472 9.03273 9.69811 8.65124 9.69811 8.17251V3H11V8.25105C11 9.08883 10.7208 9.7583 10.1623 10.2595Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 570 B

View File

@ -123,32 +123,32 @@ class _MyHomePageState extends State<MyHomePage> {
customBuilders: {
'image': ImageNodeBuilder(),
},
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')
},
],
// 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')
// },
// ],
);
}
},

View File

@ -0,0 +1,88 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/document/text_delta.dart';
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
extension TextNodeExtension on TextNode {
bool allSatisfyBoldInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.bold, selection);
bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.italic, selection);
bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.underline, selection);
bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.strikethrough, selection);
bool allSatisfyInSelection(String styleKey, Selection selection) {
final ops = delta.operations.whereType<TextInsert>();
var start = 0;
for (final op in ops) {
if (start >= selection.end.offset) {
break;
}
final length = op.length;
if (start < selection.end.offset &&
start + length > selection.start.offset) {
if (op.attributes == null ||
!op.attributes!.containsKey(styleKey) ||
op.attributes![styleKey] == false) {
return false;
}
}
start += length;
}
return true;
}
}
extension TextNodesExtension on List<TextNode> {
bool allSatisfyBoldInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.bold, selection);
bool allSatisfyItalicInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.italic, selection);
bool allSatisfyUnderlineInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.underline, selection);
bool allSatisfyStrikethroughInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.strikethrough, selection);
bool allSatisfyInSelection(String styleKey, Selection selection) {
if (isEmpty) {
return false;
}
if (length == 1) {
return first.allSatisfyInSelection(styleKey, selection);
} else {
for (var i = 0; i < length; i++) {
final node = this[i];
final Selection newSelection;
if (i == 0 && pathEquals(node.path, selection.start.path)) {
newSelection = selection.copyWith(
end: Position(path: node.path, offset: node.toRawString().length),
);
} else if (i == length - 1 &&
pathEquals(node.path, selection.end.path)) {
newSelection = selection.copyWith(
start: Position(path: node.path, offset: 0),
);
} else {
newSelection = Selection(
start: Position(path: node.path, offset: 0),
end: Position(path: node.path, offset: node.toRawString().length),
);
}
if (!node.allSatisfyInSelection(styleKey, newSelection)) {
return false;
}
}
return true;
}
}
}

View File

@ -70,7 +70,7 @@ class TransactionBuilder {
}
textEdit(TextNode node, Delta Function() f) {
beforeSelection = state.cursorSelection;
beforeSelection = state.service.selectionService.currentSelection;
final path = node.path;
final delta = f();
@ -108,6 +108,7 @@ class TransactionBuilder {
formatText(TextNode node, int index, int length, Attributes attributes) {
textEdit(node, () => Delta().retain(index).retain(length, attributes));
afterSelection = beforeSelection;
}
deleteText(TextNode node, int index, int length) {

View File

@ -1,58 +0,0 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flutter/material.dart';
typedef FloatingShortcutHandler = void Function(
EditorState editorState, String eventName);
typedef FloatingShortcuts = List<Map<String, FloatingShortcutHandler>>;
class FloatingShortcutWidget extends StatelessWidget {
const FloatingShortcutWidget({
Key? key,
required this.editorState,
required this.layerLink,
required this.rect,
required this.floatingShortcuts,
}) : super(key: key);
final EditorState editorState;
final LayerLink layerLink;
final Rect rect;
final FloatingShortcuts floatingShortcuts;
List<String> get _shortcutNames =>
floatingShortcuts.map((shortcut) => shortcut.keys.first).toList();
List<FloatingShortcutHandler> get _shortcutHandlers =>
floatingShortcuts.map((shortcut) => shortcut.values.first).toList();
@override
Widget build(BuildContext context) {
return Positioned.fromRect(
rect: rect,
child: CompositedTransformFollower(
link: layerLink,
offset: rect.topLeft,
showWhenUnlinked: true,
child: Container(
color: Colors.white,
child: ListView.builder(
itemCount: floatingShortcuts.length,
itemBuilder: ((context, index) {
final name = _shortcutNameInIndex(index);
final handler = _shortcutHandlerInIndex(index);
return Card(
child: GestureDetector(
onTap: () => handler(editorState, name),
child: ListTile(title: Text(name)),
),
);
}),
),
),
),
);
}
String _shortcutNameInIndex(int index) => _shortcutNames[index];
FloatingShortcutHandler _shortcutHandlerInIndex(int index) =>
_shortcutHandlers[index];
}

View File

@ -0,0 +1,217 @@
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/infra/flowy_svg.dart';
import 'package:flutter/material.dart';
typedef ToolbarEventHandler = void Function(
EditorState editorState, String eventName);
typedef ToolbarEventHandlers = List<Map<String, ToolbarEventHandler>>;
ToolbarEventHandlers defaultToolbarEventHandlers = [
{
'bold': ((editorState, eventName) {}),
'italic': ((editorState, eventName) {}),
'strikethrough': ((editorState, eventName) {}),
'underline': ((editorState, eventName) {}),
'quote': ((editorState, eventName) {}),
'number_list': ((editorState, eventName) {}),
'bulleted_list': ((editorState, eventName) {}),
}
];
ToolbarEventHandlers defaultListToolbarEventHandlers = [
{
'h1': ((editorState, eventName) {}),
},
{
'h2': ((editorState, eventName) {}),
},
{
'h3': ((editorState, eventName) {}),
},
{
'bulleted_list': ((editorState, eventName) {}),
},
{
'quote': ((editorState, eventName) {}),
}
];
class ToolbarWidget extends StatefulWidget {
ToolbarWidget({
Key? key,
required this.editorState,
required this.layerLink,
required this.offset,
required this.handlers,
}) : super(key: key);
final EditorState editorState;
final LayerLink layerLink;
final Offset offset;
final ToolbarEventHandlers handlers;
@override
State<ToolbarWidget> createState() => _ToolbarWidgetState();
}
class _ToolbarWidgetState extends State<ToolbarWidget> {
final GlobalKey _listToolbarKey = GlobalKey();
final toolbarHeight = 32.0;
final topPadding = 5.0;
final listToolbarWidth = 60.0;
final listToolbarHeight = 120.0;
final cornerRadius = 8.0;
OverlayEntry? _listToolbarOverlay;
@override
void initState() {
super.initState();
widget.editorState.service.selectionService.currentSelectedNodes
.addListener(_onSelectionChange);
}
@override
void dispose() {
widget.editorState.service.selectionService.currentSelectedNodes
.removeListener(_onSelectionChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned(
top: widget.offset.dx,
left: widget.offset.dy,
child: CompositedTransformFollower(
link: widget.layerLink,
showWhenUnlinked: true,
offset: widget.offset,
child: _buildToolbar(context),
),
);
}
Widget _buildToolbar(BuildContext context) {
return Material(
borderRadius: BorderRadius.circular(cornerRadius),
color: const Color(0xFF333333),
child: SizedBox(
height: toolbarHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_listToolbar(context),
_centerToolbarIcon('divider', width: 10),
_centerToolbarIcon('bold'),
_centerToolbarIcon('italic'),
_centerToolbarIcon('strikethrough'),
_centerToolbarIcon('underline'),
_centerToolbarIcon('divider', width: 10),
_centerToolbarIcon('quote'),
_centerToolbarIcon('number_list'),
_centerToolbarIcon('bulleted_list'),
],
),
),
);
}
Widget _listToolbar(BuildContext context) {
return _centerToolbarIcon(
'quote',
key: _listToolbarKey,
width: listToolbarWidth,
onTap: () => _onTapListToolbar(context),
);
}
Widget _centerToolbarIcon(String name,
{Key? key, double? width, VoidCallback? onTap}) {
return Tooltip(
key: key,
preferBelow: false,
message: name,
child: GestureDetector(
onTap: onTap ?? () => debugPrint('toolbar tap $name'),
child: SizedBox.fromSize(
size: width != null
? Size(width, toolbarHeight)
: Size.square(toolbarHeight),
child: Center(
child: FlowySvg(
name: 'toolbar/$name',
),
),
),
),
);
}
void _onTapListToolbar(BuildContext context) {
// TODO: implement more detailed UI.
final items = defaultListToolbarEventHandlers
.map((handler) => handler.keys.first)
.toList(growable: false);
final renderBox =
_listToolbarKey.currentContext?.findRenderObject() as RenderBox;
final offset = renderBox
.localToGlobal(Offset.zero)
.translate(0, toolbarHeight - cornerRadius);
final rect = offset & Size(listToolbarWidth, listToolbarHeight);
_listToolbarOverlay?.remove();
_listToolbarOverlay = OverlayEntry(builder: (context) {
return Positioned.fromRect(
rect: rect,
child: Material(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(cornerRadius),
bottomRight: Radius.circular(cornerRadius),
),
color: const Color(0xFF333333),
child: SingleChildScrollView(
child: ListView.builder(
itemExtent: toolbarHeight,
padding: const EdgeInsets.only(bottom: 10.0),
shrinkWrap: true,
itemCount: items.length,
itemBuilder: ((context, index) {
return ListTile(
contentPadding: const EdgeInsets.only(
left: 3.0,
right: 3.0,
),
minVerticalPadding: 0.0,
title: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
items[index],
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
),
),
),
onTap: () {
debugPrint('tap on $index');
},
);
}),
),
),
),
);
});
Overlay.of(context)?.insert(_listToolbarOverlay!);
}
void _onSelectionChange() {
_listToolbarOverlay?.remove();
_listToolbarOverlay = null;
}
}

View File

@ -0,0 +1,49 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
bool formatRichTextStyle(
EditorState editorState, Map<String, dynamic> attributes) {
final selection = editorState.service.selectionService.currentSelection;
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
final textNodes = nodes.whereType<TextNode>().toList();
if (selection == null || textNodes.isEmpty) {
return false;
}
final builder = TransactionBuilder(editorState);
// 1. All nodes are text nodes.
// 2. The first node is not TextNode.
// 3. The last node is not TextNode.
for (var i = 0; i < textNodes.length; i++) {
final textNode = textNodes[i];
if (i == 0 && textNode == nodes.first) {
builder.formatText(
textNode,
selection.start.offset,
textNode.toRawString().length - selection.start.offset,
attributes,
);
} else if (i == textNodes.length - 1 && textNode == nodes.last) {
builder.formatText(
textNode,
0,
selection.end.offset,
attributes,
);
} else {
builder.formatText(
textNode,
0,
textNode.toRawString().length,
attributes,
);
}
}
builder.commit();
return true;
}

View File

@ -1,4 +1,4 @@
import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart';
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';
@ -10,7 +10,6 @@ import 'package:flowy_editor/render/rich_text/flowy_rich_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/quoted_text.dart';
import 'package:flowy_editor/render/selection/floating_shortcut_widget.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/delete_nodes_handler.dart';
@ -19,7 +18,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handle
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flowy_editor/service/render_plugin_service.dart';
import 'package:flowy_editor/service/selection_service.dart';
import 'package:flowy_editor/service/shortcut_service.dart';
import 'package:flowy_editor/service/toolbar_service.dart';
NodeWidgetBuilders defaultBuilders = {
'editor': EditorEntryWidgetBuilder(),
@ -46,7 +45,6 @@ class FlowyEditor extends StatefulWidget {
required this.editorState,
this.customBuilders = const {},
this.keyEventHandlers = const [],
this.shortcuts = const [],
}) : super(key: key);
final EditorState editorState;
@ -57,9 +55,6 @@ class FlowyEditor extends StatefulWidget {
/// Keyboard event handlers.
final List<FlowyKeyEventHandler> keyEventHandlers;
/// Shortcuts
final FloatingShortcuts shortcuts;
@override
State<FlowyEditor> createState() => _FlowyEditorState();
}
@ -98,11 +93,9 @@ class _FlowyEditorState extends State<FlowyEditor> {
...widget.keyEventHandlers,
],
editorState: editorState,
child: FloatingShortcut(
key: editorState.service.floatingShortcutServiceKey,
size: const Size(200, 150), // TODO: support customize size.
child: FlowyToolbar(
key: editorState.service.toolbarServiceKey,
editorState: editorState,
floatingShortcuts: widget.shortcuts,
child: editorState.service.renderPluginService.buildPluginWidget(
NodeWidgetContext(
context: context,

View File

@ -1,22 +1,21 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
import 'package:flutter/material.dart';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/extensions/text_node_extensions.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';
FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
if (!event.isMetaPressed || event.character == null) {
return KeyEventResult.ignored;
}
final selection = editorState.service.selectionService.currentSelection;
final nodes = editorState.service.selectionService.currentSelectedNodes.value
.whereType<TextNode>()
.toList();
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
if (selection == null || nodes.isEmpty) {
if (selection == null || textNodes.isEmpty) {
return KeyEventResult.ignored;
}
@ -24,7 +23,9 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
// bold
case 'B':
case 'b':
_makeBold(editorState, nodes, selection);
formatRichTextStyle(editorState, {
StyleKey.bold: !textNodes.allSatisfyBoldInSelection(selection),
});
return KeyEventResult.handled;
default:
break;
@ -32,52 +33,3 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
return KeyEventResult.ignored;
};
// TODO: implement unBold.
void _makeBold(
EditorState editorState, List<TextNode> nodes, Selection selection) {
final builder = TransactionBuilder(editorState);
if (nodes.length == 1) {
builder.formatText(
nodes.first,
selection.start.offset,
selection.end.offset - selection.start.offset,
{
'bold': true,
},
);
} else {
for (var i = 0; i < nodes.length; i++) {
final node = nodes[i];
if (i == 0) {
builder.formatText(
node,
selection.start.offset,
node.toRawString().length - selection.start.offset,
{
'bold': true,
},
);
} else if (i == nodes.length - 1) {
builder.formatText(
node,
0,
selection.end.offset,
{
'bold': true,
},
);
} else {
builder.formatText(
node,
0,
node.toRawString().length,
{
'bold': true,
},
);
}
}
}
builder.commit();
}

View File

@ -1,18 +1,16 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.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/render/selection/selectable.dart';
import 'package:flowy_editor/render/selection/cursor_widget.dart';
import 'package:flowy_editor/render/selection/selection_widget.dart';
import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/extensions/node_extensions.dart';
import 'package:flutter/gestures.dart';
import 'package:flowy_editor/service/shortcut_service.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flutter/material.dart';
import 'package:flowy_editor/extensions/node_extensions.dart';
import 'package:flowy_editor/render/selection/cursor_widget.dart';
import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flowy_editor/render/selection/selection_widget.dart';
/// Process selection and cursor
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
@ -424,14 +422,19 @@ class _FlowySelectionState extends State<FlowySelection>
// compute the selection in range.
if (first != null && last != null) {
bool isDownward = panStartOffset!.dy <= panEndOffset!.dy;
bool isDownward;
if (first == last) {
isDownward = panStartOffset!.dx < panEndOffset!.dx;
} else {
isDownward = panStartOffset!.dy < panEndOffset!.dy;
}
final start =
first.getSelectionInRange(panStartOffset!, panEndOffset!).start;
final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end;
final selection = Selection(
start: isDownward ? start : end, end: isDownward ? end : start);
debugPrint('[_onPanUpdate] $selection');
editorState.updateCursorSelection(selection);
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
editorState.service.selectionService.updateSelection(selection);
}
}
@ -451,10 +454,8 @@ class _FlowySelectionState extends State<FlowySelection>
_cursorOverlays
..forEach((overlay) => overlay.remove())
..clear();
// clear floating shortcuts
editorState.service.floatingShortcutServiceKey.currentState
?.unwrapOrNull<FlowyFloatingShortcutService>()
?.hide();
// clear toolbar
editorState.service.toolbarService.hide();
}
void _updateSelection(Selection selection) {
@ -464,6 +465,9 @@ class _FlowySelectionState extends State<FlowySelection>
currentSelection = selection;
currentSelectedNodes.value = nodes;
Rect? topmostRect;
LayerLink? layerLink;
var index = 0;
for (final node in nodes) {
final selectable = node.selectable;
@ -502,19 +506,28 @@ class _FlowySelectionState extends State<FlowySelection>
final rects = selectable.getRectsInSelection(newSelection);
for (final rect in rects) {
// FIXME: Need to compute more precise location.
topmostRect ??= rect;
layerLink ??= node.layerLink;
_rects.add(_transformRectToGlobal(selectable, rect));
final overlay = OverlayEntry(
builder: ((context) => SelectionWidget(
color: widget.selectionColor,
layerLink: node.layerLink,
rect: rect,
)),
builder: (context) => SelectionWidget(
color: widget.selectionColor,
layerLink: node.layerLink,
rect: rect,
),
);
_selectionOverlays.add(overlay);
}
index += 1;
}
Overlay.of(context)?.insertAll(_selectionOverlays);
if (topmostRect != null && layerLink != null) {
editorState.service.toolbarService
.showInOffset(topmostRect.topLeft, layerLink);
}
}
Rect _transformRectToGlobal(Selectable selectable, Rect r) {

View File

@ -1,5 +1,5 @@
import 'package:flowy_editor/service/render_plugin_service.dart';
import 'package:flowy_editor/service/shortcut_service.dart';
import 'package:flowy_editor/service/toolbar_service.dart';
import 'package:flowy_editor/service/selection_service.dart';
import 'package:flutter/material.dart';
@ -21,14 +21,11 @@ class FlowyService {
// render plugin service
late FlowyRenderPlugin renderPluginService;
// floating shortcut service
final floatingShortcutServiceKey =
GlobalKey(debugLabel: 'flowy_floating_shortcut_service');
FlowyFloatingShortcutService get floatingToolbarService {
assert(floatingShortcutServiceKey.currentState != null &&
floatingShortcutServiceKey.currentState
is FlowyFloatingShortcutService);
return floatingShortcutServiceKey.currentState!
as FlowyFloatingShortcutService;
// toolbar service
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
ToolbarService get toolbarService {
assert(toolbarServiceKey.currentState != null &&
toolbarServiceKey.currentState is ToolbarService);
return toolbarServiceKey.currentState! as ToolbarService;
}
}

View File

@ -1,60 +0,0 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
import 'package:flutter/material.dart';
mixin FlowyFloatingShortcutService {
/// Show the floating shortcut widget beside the offset.
void showInOffset(Offset offset, LayerLink layerLink);
/// Hide the floating shortcut widget.
void hide();
}
class FloatingShortcut extends StatefulWidget {
const FloatingShortcut({
Key? key,
required this.size,
required this.editorState,
required this.floatingShortcuts,
required this.child,
}) : super(key: key);
final Size size;
final EditorState editorState;
final Widget child;
final FloatingShortcuts floatingShortcuts;
@override
State<FloatingShortcut> createState() => _FloatingShortcutState();
}
class _FloatingShortcutState extends State<FloatingShortcut>
with FlowyFloatingShortcutService {
OverlayEntry? _floatintShortcutOverlay;
@override
void showInOffset(Offset offset, LayerLink layerLink) {
_floatintShortcutOverlay?.remove();
_floatintShortcutOverlay = OverlayEntry(
builder: (context) => FloatingShortcutWidget(
editorState: widget.editorState,
layerLink: layerLink,
rect: offset.translate(10, 0) & widget.size,
floatingShortcuts: widget.floatingShortcuts),
);
Overlay.of(context)?.insert(_floatintShortcutOverlay!);
}
@override
void hide() {
_floatintShortcutOverlay?.remove();
_floatintShortcutOverlay = null;
}
@override
Widget build(BuildContext context) {
return Container(
child: widget.child,
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/render/selection/toolbar_widget.dart';
import 'package:flutter/material.dart';
mixin ToolbarService {
/// Show the toolbar widget beside the offset.
void showInOffset(Offset offset, LayerLink layerLink);
/// Hide the toolbar widget.
void hide();
}
class FlowyToolbar extends StatefulWidget {
const FlowyToolbar({
Key? key,
required this.editorState,
required this.child,
}) : super(key: key);
final EditorState editorState;
final Widget child;
@override
State<FlowyToolbar> createState() => _FlowyToolbarState();
}
class _FlowyToolbarState extends State<FlowyToolbar> with ToolbarService {
OverlayEntry? _toolbarOverlay;
@override
void showInOffset(Offset offset, LayerLink layerLink) {
_toolbarOverlay?.remove();
_toolbarOverlay = OverlayEntry(
builder: (context) => ToolbarWidget(
editorState: widget.editorState,
layerLink: layerLink,
offset: offset.translate(0, -37.0),
handlers: const [],
),
);
Overlay.of(context)?.insert(_toolbarOverlay!);
}
@override
void hide() {
_toolbarOverlay?.remove();
_toolbarOverlay = null;
}
@override
Widget build(BuildContext context) {
return Container(
child: widget.child,
);
}
}

View File

@ -26,7 +26,7 @@ dev_dependencies:
flutter:
# To add assets to your package, add an assets section, like this:
assets:
- assets/images/uncheck.svg
- assets/images/toolbar/
- assets/images/
- assets/document.json
# - images/a_dot_burr.jpeg