mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement floating shortcut
This commit is contained in:
parent
2f86cac8af
commit
0bf1e61d55
@ -97,6 +97,21 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
return FlowyEditor(
|
return FlowyEditor(
|
||||||
editorState: _editorState,
|
editorState: _editorState,
|
||||||
keyEventHandler: const [],
|
keyEventHandler: const [],
|
||||||
|
shortCuts: [
|
||||||
|
// TODO: this won't work, just a example for now.
|
||||||
|
{
|
||||||
|
'heading': (editorState, eventName) =>
|
||||||
|
debugPrint('shortcut => $eventName')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bold': (editorState, eventName) =>
|
||||||
|
debugPrint('shortcut => $eventName')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'underline': (editorState, eventName) =>
|
||||||
|
debugPrint('shortcut => $eventName')
|
||||||
|
},
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,8 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FlowyCursorWidget extends StatefulWidget {
|
class CursorWidget extends StatefulWidget {
|
||||||
const FlowyCursorWidget({
|
const CursorWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.layerLink,
|
required this.layerLink,
|
||||||
required this.rect,
|
required this.rect,
|
||||||
@ -17,10 +17,10 @@ class FlowyCursorWidget extends StatefulWidget {
|
|||||||
final LayerLink layerLink;
|
final LayerLink layerLink;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FlowyCursorWidget> createState() => _FlowyCursorWidgetState();
|
State<CursorWidget> createState() => _CursorWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FlowyCursorWidgetState extends State<FlowyCursorWidget> {
|
class _CursorWidgetState extends State<CursorWidget> {
|
||||||
bool showCursor = true;
|
bool showCursor = true;
|
||||||
late Timer timer;
|
late Timer timer;
|
||||||
|
|
@ -0,0 +1,58 @@
|
|||||||
|
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];
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FlowySelectionWidget extends StatefulWidget {
|
class SelectionWidget extends StatefulWidget {
|
||||||
const FlowySelectionWidget({
|
const SelectionWidget({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.layerLink,
|
required this.layerLink,
|
||||||
required this.rect,
|
required this.rect,
|
||||||
@ -13,10 +13,10 @@ class FlowySelectionWidget extends StatefulWidget {
|
|||||||
final LayerLink layerLink;
|
final LayerLink layerLink;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FlowySelectionWidget> createState() => _FlowySelectionWidgetState();
|
State<SelectionWidget> createState() => _SelectionWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FlowySelectionWidgetState extends State<FlowySelectionWidget> {
|
class _SelectionWidgetState extends State<SelectionWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Positioned.fromRect(
|
return Positioned.fromRect(
|
@ -1,6 +1,9 @@
|
|||||||
|
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
|
||||||
|
import 'package:flowy_editor/service/floating_shortcut_service.dart';
|
||||||
import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart';
|
import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart';
|
||||||
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart';
|
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart';
|
||||||
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart';
|
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart';
|
||||||
|
import 'package:flowy_editor/service/flowy_key_event_handlers/shortcut_handler.dart';
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/service/selection_service.dart';
|
import 'package:flowy_editor/service/selection_service.dart';
|
||||||
|
|
||||||
@ -12,10 +15,12 @@ class FlowyEditor extends StatefulWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.keyEventHandler,
|
required this.keyEventHandler,
|
||||||
|
required this.shortCuts,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final List<FlowyKeyEventHandler> keyEventHandler;
|
final List<FlowyKeyEventHandler> keyEventHandler;
|
||||||
|
final FloatingShortCuts shortCuts;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FlowyEditor> createState() => _FlowyEditorState();
|
State<FlowyEditor> createState() => _FlowyEditorState();
|
||||||
@ -32,13 +37,20 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
child: FlowyKeyboard(
|
child: FlowyKeyboard(
|
||||||
key: editorState.service.keyboardServiceKey,
|
key: editorState.service.keyboardServiceKey,
|
||||||
handlers: [
|
handlers: [
|
||||||
|
slashShortcutHandler,
|
||||||
flowyDeleteNodesHandler,
|
flowyDeleteNodesHandler,
|
||||||
deleteSingleTextNodeHandler,
|
deleteSingleTextNodeHandler,
|
||||||
arrowKeysHandler,
|
arrowKeysHandler,
|
||||||
...widget.keyEventHandler,
|
...widget.keyEventHandler,
|
||||||
],
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: editorState.build(context),
|
child: FloatingShortCut(
|
||||||
|
key: editorState.service.floatingShortcutServiceKey,
|
||||||
|
size: const Size(200, 150), // TODO: support customize size.
|
||||||
|
editorState: editorState,
|
||||||
|
floatingShortCuts: widget.shortCuts,
|
||||||
|
child: editorState.build(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
mixin FlowyFloatingShortCutService {
|
||||||
|
void showInOffset(Offset offset, LayerLink layerLink);
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
import 'package:flowy_editor/document/node.dart';
|
import 'package:flowy_editor/document/node.dart';
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
|
||||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/extensions/object_extensions.dart';
|
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||||
import 'package:flowy_editor/service/selection_service.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
|
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// type '/' to trigger shortcut widget
|
||||||
|
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
||||||
|
if (event.logicalKey != LogicalKeyboardKey.slash) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedNodes = editorState.selectedNodes;
|
||||||
|
if (selectedNodes.length != 1) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final textNode = selectedNodes.first.unwrapOrNull<TextNode>();
|
||||||
|
final selectable = textNode?.key?.currentState?.unwrapOrNull<Selectable>();
|
||||||
|
final textSelection = selectable?.getTextSelection();
|
||||||
|
if (textNode != null && selectable != null && textSelection != null) {
|
||||||
|
final offset = selectable.getOffsetByTextSelection(textSelection);
|
||||||
|
final rect = selectable.getCursorRect(offset);
|
||||||
|
editorState.service.floatingToolbarService
|
||||||
|
.showInOffset(rect.topLeft, textNode.layerLink);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:flowy_editor/render/selection/flowy_cursor_widget.dart';
|
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
||||||
import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
|
import 'package:flowy_editor/render/selection/selection_widget.dart';
|
||||||
|
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||||
|
import 'package:flowy_editor/service/floating_shortcut_service.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -120,7 +122,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final selectionRects = selectable.getSelectionRectsInRange(start, end);
|
final selectionRects = selectable.getSelectionRectsInRange(start, end);
|
||||||
for (final rect in selectionRects) {
|
for (final rect in selectionRects) {
|
||||||
final overlay = OverlayEntry(
|
final overlay = OverlayEntry(
|
||||||
builder: ((context) => FlowySelectionWidget(
|
builder: ((context) => SelectionWidget(
|
||||||
color: Colors.yellow.withAlpha(100),
|
color: Colors.yellow.withAlpha(100),
|
||||||
layerLink: node.layerLink,
|
layerLink: node.layerLink,
|
||||||
rect: rect,
|
rect: rect,
|
||||||
@ -149,7 +151,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final selectable = selectedNode.key?.currentState as Selectable;
|
final selectable = selectedNode.key?.currentState as Selectable;
|
||||||
final rect = selectable.getCursorRect(start);
|
final rect = selectable.getCursorRect(start);
|
||||||
final cursor = OverlayEntry(
|
final cursor = OverlayEntry(
|
||||||
builder: ((context) => FlowyCursorWidget(
|
builder: ((context) => CursorWidget(
|
||||||
key: _cursorKey,
|
key: _cursorKey,
|
||||||
rect: rect,
|
rect: rect,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
@ -275,6 +277,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
void _clearAllOverlayEntries() {
|
void _clearAllOverlayEntries() {
|
||||||
_clearSelection();
|
_clearSelection();
|
||||||
_clearCursor();
|
_clearCursor();
|
||||||
|
_clearFloatingShorts();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearSelection() {
|
void _clearSelection() {
|
||||||
@ -288,4 +291,11 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
..forEach((overlay) => overlay.remove())
|
..forEach((overlay) => overlay.remove())
|
||||||
..clear();
|
..clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearFloatingShorts() {
|
||||||
|
final shortCutService = editorState
|
||||||
|
.service.floatingShortcutServiceKey.currentState
|
||||||
|
?.unwrapOrNull<FlowyFloatingShortCutService>();
|
||||||
|
shortCutService?.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flowy_editor/service/floating_shortcut_service.dart';
|
||||||
import 'package:flowy_editor/service/selection_service.dart';
|
import 'package:flowy_editor/service/selection_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -12,4 +13,15 @@ class FlowyService {
|
|||||||
|
|
||||||
// keyboard service
|
// keyboard service
|
||||||
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
|
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
|
||||||
|
|
||||||
|
// floating toolbar 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user