feat: implement toolbar UI part.
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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')
|
||||
// },
|
||||
// ],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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];
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -9,7 +9,7 @@ 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/service/toolbar_service.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -452,9 +452,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
// clear floating shortcuts
|
||||
editorState.service.floatingShortcutServiceKey.currentState
|
||||
?.unwrapOrNull<FlowyFloatingShortcutService>()
|
||||
?.hide();
|
||||
editorState.service.toolbarService.hide();
|
||||
}
|
||||
|
||||
void _updateSelection(Selection selection) {
|
||||
@ -464,6 +462,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 +503,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) {
|
||||
|
@ -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';
|
||||
|
||||
@ -22,13 +22,11 @@ class FlowyService {
|
||||
late FlowyRenderPlugin renderPluginService;
|
||||
|
||||
// floating shortcut service
|
||||
final floatingShortcutServiceKey =
|
||||
final toolbarServiceKey =
|
||||
GlobalKey(debugLabel: 'flowy_floating_shortcut_service');
|
||||
FlowyFloatingShortcutService get floatingToolbarService {
|
||||
assert(floatingShortcutServiceKey.currentState != null &&
|
||||
floatingShortcutServiceKey.currentState
|
||||
is FlowyFloatingShortcutService);
|
||||
return floatingShortcutServiceKey.currentState!
|
||||
as FlowyFloatingShortcutService;
|
||||
ToolbarService get toolbarService {
|
||||
assert(toolbarServiceKey.currentState != null &&
|
||||
toolbarServiceKey.currentState is ToolbarService);
|
||||
return toolbarServiceKey.currentState! as ToolbarService;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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 floating shortcut widget beside the offset.
|
||||
void showInOffset(Offset offset, LayerLink layerLink);
|
||||
|
||||
/// Hide the floating shortcut 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? _floatingShortcutOverlay;
|
||||
|
||||
@override
|
||||
void showInOffset(Offset offset, LayerLink layerLink) {
|
||||
_floatingShortcutOverlay?.remove();
|
||||
_floatingShortcutOverlay = OverlayEntry(
|
||||
builder: (context) => ToolbarWidget(
|
||||
editorState: widget.editorState,
|
||||
layerLink: layerLink,
|
||||
offset: offset.translate(0, -37.0),
|
||||
handlers: const [],
|
||||
),
|
||||
);
|
||||
Overlay.of(context)?.insert(_floatingShortcutOverlay!);
|
||||
}
|
||||
|
||||
@override
|
||||
void hide() {
|
||||
_floatingShortcutOverlay?.remove();
|
||||
_floatingShortcutOverlay = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
|