mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement context menu
This commit is contained in:
parent
78728bb6ff
commit
30700cd513
@ -65,6 +65,20 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
body: _buildEditor(context),
|
body: _buildEditor(context),
|
||||||
|
// body: Center(
|
||||||
|
// child: ContextMenu(editorState: EditorState.empty(), items: [
|
||||||
|
// [
|
||||||
|
// ContextMenuItem(name: 'ABCDEFGHIJKLM', onPressed: (editorState) {}),
|
||||||
|
// ContextMenuItem(name: 'A', onPressed: (editorState) {}),
|
||||||
|
// ContextMenuItem(name: 'A', onPressed: (editorState) {})
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// ContextMenuItem(name: 'B', onPressed: (editorState) {}),
|
||||||
|
// ContextMenuItem(name: 'B', onPressed: (editorState) {}),
|
||||||
|
// ContextMenuItem(name: 'B', onPressed: (editorState) {})
|
||||||
|
// ]
|
||||||
|
// ]),
|
||||||
|
// ),
|
||||||
floatingActionButton: _buildExpandableFab(),
|
floatingActionButton: _buildExpandableFab(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
||||||
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
|
|
||||||
|
final builtInContextMenuItems = [
|
||||||
|
[
|
||||||
|
// cut
|
||||||
|
ContextMenuItem(
|
||||||
|
name: 'Cut',
|
||||||
|
onPressed: (editorState) {
|
||||||
|
cutEventHandler(editorState, null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// copy
|
||||||
|
ContextMenuItem(
|
||||||
|
name: 'Copy',
|
||||||
|
onPressed: (editorState) {
|
||||||
|
copyEventHandler(editorState, null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Paste
|
||||||
|
ContextMenuItem(
|
||||||
|
name: 'Paste',
|
||||||
|
onPressed: (editorState) {
|
||||||
|
pasteEventHandler(editorState, null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
];
|
@ -0,0 +1,90 @@
|
|||||||
|
import 'package:appflowy_editor/src/editor_state.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ContextMenuItem {
|
||||||
|
ContextMenuItem({
|
||||||
|
required this.name,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final void Function(EditorState editorState) onPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContextMenu extends StatelessWidget {
|
||||||
|
const ContextMenu({
|
||||||
|
Key? key,
|
||||||
|
required this.position,
|
||||||
|
required this.editorState,
|
||||||
|
required this.items,
|
||||||
|
required this.onPressed,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Offset position;
|
||||||
|
final EditorState editorState;
|
||||||
|
final List<List<ContextMenuItem>> items;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final children = <Widget>[];
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
for (var j = 0; j < items[i].length; j++) {
|
||||||
|
children.add(
|
||||||
|
Material(
|
||||||
|
child: InkWell(
|
||||||
|
hoverColor: const Color(0xFFE0F8FF),
|
||||||
|
customBorder: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
items[i][j].onPressed(editorState);
|
||||||
|
onPressed();
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
items[i][j].name,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (i != items.length - 1) {
|
||||||
|
children.add(const Divider());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
top: position.dy,
|
||||||
|
left: position.dx,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 140,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 5,
|
||||||
|
spreadRadius: 1,
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
),
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -217,7 +217,7 @@ ShortcutEventHandler cursorEndSelect = (editorState, event) {
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
};
|
};
|
||||||
|
|
||||||
KeyEventResult cursorUp(EditorState editorState, RawKeyEvent event) {
|
ShortcutEventHandler cursorUp = (editorState, event) {
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final selection =
|
final selection =
|
||||||
editorState.service.selectionService.currentSelection.value?.normalized;
|
editorState.service.selectionService.currentSelection.value?.normalized;
|
||||||
@ -229,9 +229,9 @@ KeyEventResult cursorUp(EditorState editorState, RawKeyEvent event) {
|
|||||||
upPosition == null ? null : Selection.collapsed(upPosition),
|
upPosition == null ? null : Selection.collapsed(upPosition),
|
||||||
);
|
);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
};
|
||||||
|
|
||||||
KeyEventResult cursorDown(EditorState editorState, RawKeyEvent event) {
|
ShortcutEventHandler cursorDown = (editorState, event) {
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final selection =
|
final selection =
|
||||||
editorState.service.selectionService.currentSelection.value?.normalized;
|
editorState.service.selectionService.currentSelection.value?.normalized;
|
||||||
@ -243,9 +243,9 @@ KeyEventResult cursorDown(EditorState editorState, RawKeyEvent event) {
|
|||||||
downPosition == null ? null : Selection.collapsed(downPosition),
|
downPosition == null ? null : Selection.collapsed(downPosition),
|
||||||
);
|
);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
};
|
||||||
|
|
||||||
KeyEventResult cursorLeft(EditorState editorState, RawKeyEvent event) {
|
ShortcutEventHandler cursorLeft = (editorState, event) {
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final selection =
|
final selection =
|
||||||
editorState.service.selectionService.currentSelection.value?.normalized;
|
editorState.service.selectionService.currentSelection.value?.normalized;
|
||||||
@ -265,9 +265,9 @@ KeyEventResult cursorLeft(EditorState editorState, RawKeyEvent event) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
};
|
||||||
|
|
||||||
KeyEventResult cursorRight(EditorState editorState, RawKeyEvent event) {
|
ShortcutEventHandler cursorRight = (editorState, event) {
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
final selection =
|
final selection =
|
||||||
editorState.service.selectionService.currentSelection.value?.normalized;
|
editorState.service.selectionService.currentSelection.value?.normalized;
|
||||||
@ -287,7 +287,7 @@ KeyEventResult cursorRight(EditorState editorState, RawKeyEvent event) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
};
|
||||||
|
|
||||||
extension on Position {
|
extension on Position {
|
||||||
Position? goLeft(EditorState editorState) {
|
Position? goLeft(EditorState editorState) {
|
||||||
|
@ -1,22 +1,9 @@
|
|||||||
import 'package:appflowy_editor/src/infra/infra.dart';
|
import 'package:appflowy_editor/src/infra/infra.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
// Handle delete text.
|
ShortcutEventHandler backspaceEventHandler = (editorState, event) {
|
||||||
ShortcutEventHandler deleteTextHandler = (editorState, event) {
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
|
||||||
return _handleBackspace(editorState, event);
|
|
||||||
}
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.delete) {
|
|
||||||
return _handleDelete(editorState, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
};
|
|
||||||
|
|
||||||
KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
|
||||||
var selection = editorState.service.selectionService.currentSelection.value;
|
var selection = editorState.service.selectionService.currentSelection.value;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
@ -122,7 +109,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
};
|
||||||
|
|
||||||
KeyEventResult _backDeleteToPreviousTextNode(
|
KeyEventResult _backDeleteToPreviousTextNode(
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
@ -182,7 +169,7 @@ KeyEventResult _backDeleteToPreviousTextNode(
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
|
ShortcutEventHandler deleteEventHandler = (editorState, event) {
|
||||||
var selection = editorState.service.selectionService.currentSelection.value;
|
var selection = editorState.service.selectionService.currentSelection.value;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
@ -238,7 +225,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
};
|
||||||
|
|
||||||
KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState,
|
KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState,
|
||||||
TextNode textNode, Transaction transaction, Selection selection) {
|
TextNode textNode, Transaction transaction, Selection selection) {
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
int _endOffsetOfNode(Node node) {
|
|
||||||
if (node is TextNode) {
|
|
||||||
return node.delta.length;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on Position {
|
|
||||||
Position? goLeft(EditorState editorState) {
|
|
||||||
final node = editorState.document.nodeAtPath(path)!;
|
|
||||||
if (offset == 0) {
|
|
||||||
final prevNode = node.previous;
|
|
||||||
if (prevNode != null) {
|
|
||||||
return Position(
|
|
||||||
path: prevNode.path, offset: _endOffsetOfNode(prevNode));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node is TextNode) {
|
|
||||||
return Position(path: path, offset: node.delta.prevRunePosition(offset));
|
|
||||||
} else {
|
|
||||||
return Position(path: path, offset: offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Position? goRight(EditorState editorState) {
|
|
||||||
final node = editorState.document.nodeAtPath(path)!;
|
|
||||||
final lengthOfNode = _endOffsetOfNode(node);
|
|
||||||
if (offset >= lengthOfNode) {
|
|
||||||
final nextNode = node.next;
|
|
||||||
if (nextNode != null) {
|
|
||||||
return Position(path: nextNode.path, offset: 0);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node is TextNode) {
|
|
||||||
return Position(path: path, offset: node.delta.nextRunePosition(offset));
|
|
||||||
} else {
|
|
||||||
return Position(path: path, offset: offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Position? _goUp(EditorState editorState) {
|
|
||||||
final rects = editorState.service.selectionService.selectionRects;
|
|
||||||
if (rects.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final first = rects.first;
|
|
||||||
final firstOffset = Offset(first.left, first.top);
|
|
||||||
final hitOffset = firstOffset - Offset(0, first.height * 0.5);
|
|
||||||
return editorState.service.selectionService.getPositionInOffset(hitOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
Position? _goDown(EditorState editorState) {
|
|
||||||
final rects = editorState.service.selectionService.selectionRects;
|
|
||||||
if (rects.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final first = rects.last;
|
|
||||||
final firstOffset = Offset(first.right, first.bottom);
|
|
||||||
final hitOffset = firstOffset + Offset(0, first.height * 0.5);
|
|
||||||
return editorState.service.selectionService.getPositionInOffset(hitOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
|
|
||||||
final currentSelection = editorState.cursorSelection;
|
|
||||||
if (currentSelection == null) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
|
||||||
final leftPosition = currentSelection.end.goLeft(editorState);
|
|
||||||
editorState.updateCursorSelection(leftPosition == null
|
|
||||||
? null
|
|
||||||
: Selection(start: currentSelection.start, end: leftPosition));
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
|
||||||
final rightPosition = currentSelection.start.goRight(editorState);
|
|
||||||
editorState.updateCursorSelection(rightPosition == null
|
|
||||||
? null
|
|
||||||
: Selection(start: rightPosition, end: currentSelection.end));
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
|
||||||
final position = _goUp(editorState);
|
|
||||||
editorState.updateCursorSelection(position == null
|
|
||||||
? null
|
|
||||||
: Selection(start: position, end: currentSelection.end));
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
||||||
final position = _goDown(editorState);
|
|
||||||
editorState.updateCursorSelection(position == null
|
|
||||||
? null
|
|
||||||
: Selection(start: currentSelection.start, end: position));
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShortcutEventHandler arrowKeysHandler = (editorState, event) {
|
|
||||||
if (event.isShiftPressed) {
|
|
||||||
return _handleShiftKey(editorState, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
final currentSelection = editorState.cursorSelection;
|
|
||||||
if (currentSelection == null) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
|
||||||
if (currentSelection.isCollapsed) {
|
|
||||||
final leftPosition = currentSelection.start.goLeft(editorState);
|
|
||||||
if (leftPosition != null) {
|
|
||||||
editorState.updateCursorSelection(Selection.collapsed(leftPosition));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
editorState.updateCursorSelection(
|
|
||||||
currentSelection.collapse(atStart: currentSelection.isBackward),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
|
||||||
if (currentSelection.isCollapsed) {
|
|
||||||
final rightPosition = currentSelection.end.goRight(editorState);
|
|
||||||
if (rightPosition != null) {
|
|
||||||
editorState.updateCursorSelection(Selection.collapsed(rightPosition));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
editorState.updateCursorSelection(
|
|
||||||
currentSelection.collapse(atStart: !currentSelection.isBackward),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
|
||||||
final position = _goUp(editorState);
|
|
||||||
editorState.updateCursorSelection(
|
|
||||||
position == null ? null : Selection.collapsed(position));
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
||||||
final position = _goDown(editorState);
|
|
||||||
editorState.updateCursorSelection(
|
|
||||||
position == null ? null : Selection.collapsed(position));
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
};
|
|
@ -4,14 +4,9 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service
|
|||||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
SelectionMenuService? _selectionMenuService;
|
SelectionMenuService? _selectionMenuService;
|
||||||
ShortcutEventHandler slashShortcutHandler = (editorState, event) {
|
ShortcutEventHandler slashShortcutHandler = (editorState, event) {
|
||||||
if (event.logicalKey != LogicalKeyboardKey.slash) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
.whereType<TextNode>();
|
.whereType<TextNode>();
|
||||||
if (textNodes.length != 1) {
|
if (textNodes.length != 1) {
|
||||||
@ -26,8 +21,12 @@ ShortcutEventHandler slashShortcutHandler = (editorState, event) {
|
|||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
..replaceText(textNode, selection.start.offset,
|
..replaceText(
|
||||||
selection.end.offset - selection.start.offset, event.character ?? '');
|
textNode,
|
||||||
|
selection.start.offset,
|
||||||
|
selection.end.offset - selection.start.offset,
|
||||||
|
'/',
|
||||||
|
);
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
|
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
|
||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
import 'package:appflowy_editor/src/core/location/position.dart';
|
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||||
@ -24,10 +23,6 @@ const _unCheckboxListSymbols = ['[]', '-[]'];
|
|||||||
final _numberRegex = RegExp(r'^(\d+)\.');
|
final _numberRegex = RegExp(r'^(\d+)\.');
|
||||||
|
|
||||||
ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
|
ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
|
||||||
if (event.logicalKey != LogicalKeyboardKey.space) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process markdown input style.
|
/// Process markdown input style.
|
||||||
///
|
///
|
||||||
/// like, #, *, -, 1., -[],
|
/// like, #, *, -, 1., -[],
|
||||||
|
@ -13,6 +13,7 @@ class SelectionGestureDetector extends StatefulWidget {
|
|||||||
this.onTapDown,
|
this.onTapDown,
|
||||||
this.onDoubleTapDown,
|
this.onDoubleTapDown,
|
||||||
this.onTripleTapDown,
|
this.onTripleTapDown,
|
||||||
|
this.onSecondaryTapDown,
|
||||||
this.onPanStart,
|
this.onPanStart,
|
||||||
this.onPanUpdate,
|
this.onPanUpdate,
|
||||||
this.onPanEnd,
|
this.onPanEnd,
|
||||||
@ -27,6 +28,7 @@ class SelectionGestureDetector extends StatefulWidget {
|
|||||||
final GestureTapDownCallback? onTapDown;
|
final GestureTapDownCallback? onTapDown;
|
||||||
final GestureTapDownCallback? onDoubleTapDown;
|
final GestureTapDownCallback? onDoubleTapDown;
|
||||||
final GestureTapDownCallback? onTripleTapDown;
|
final GestureTapDownCallback? onTripleTapDown;
|
||||||
|
final GestureTapDownCallback? onSecondaryTapDown;
|
||||||
final GestureDragStartCallback? onPanStart;
|
final GestureDragStartCallback? onPanStart;
|
||||||
final GestureDragUpdateCallback? onPanUpdate;
|
final GestureDragUpdateCallback? onPanUpdate;
|
||||||
final GestureDragEndCallback? onPanEnd;
|
final GestureDragEndCallback? onPanEnd;
|
||||||
@ -60,6 +62,7 @@ class SelectionGestureDetectorState extends State<SelectionGestureDetector> {
|
|||||||
() => TapGestureRecognizer(),
|
() => TapGestureRecognizer(),
|
||||||
(recognizer) {
|
(recognizer) {
|
||||||
recognizer.onTapDown = _tapDownDelegate;
|
recognizer.onTapDown = _tapDownDelegate;
|
||||||
|
recognizer.onSecondaryTapDown = widget.onSecondaryTapDown;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:appflowy_editor/src/infra/log.dart';
|
import 'package:appflowy_editor/src/infra/log.dart';
|
||||||
|
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
||||||
|
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||||
@ -108,6 +110,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
final List<Rect> selectionRects = [];
|
final List<Rect> selectionRects = [];
|
||||||
final List<OverlayEntry> _selectionAreas = [];
|
final List<OverlayEntry> _selectionAreas = [];
|
||||||
final List<OverlayEntry> _cursorAreas = [];
|
final List<OverlayEntry> _cursorAreas = [];
|
||||||
|
final List<OverlayEntry> _contextMenuAreas = [];
|
||||||
|
|
||||||
// OverlayEntry? _debugOverlay;
|
// OverlayEntry? _debugOverlay;
|
||||||
|
|
||||||
@ -156,6 +159,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
onPanUpdate: _onPanUpdate,
|
onPanUpdate: _onPanUpdate,
|
||||||
onPanEnd: _onPanEnd,
|
onPanEnd: _onPanEnd,
|
||||||
onTapDown: _onTapDown,
|
onTapDown: _onTapDown,
|
||||||
|
onSecondaryTapDown: _onSecondaryTapDown,
|
||||||
onDoubleTapDown: _onDoubleTapDown,
|
onDoubleTapDown: _onDoubleTapDown,
|
||||||
onTripleTapDown: _onTripleTapDown,
|
onTripleTapDown: _onTripleTapDown,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
@ -232,6 +236,9 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
|
|
||||||
// hide toolbar
|
// hide toolbar
|
||||||
editorState.service.toolbarService?.hide();
|
editorState.service.toolbarService?.hide();
|
||||||
|
|
||||||
|
// clear context menu
|
||||||
|
_clearContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -242,6 +249,12 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
..clear();
|
..clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearContextMenu() {
|
||||||
|
_contextMenuAreas
|
||||||
|
..forEach((overlay) => overlay.remove())
|
||||||
|
..clear();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Node? getNodeInOffset(Offset offset) {
|
Node? getNodeInOffset(Offset offset) {
|
||||||
final sortedNodes =
|
final sortedNodes =
|
||||||
@ -311,6 +324,20 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
_enableInteraction();
|
_enableInteraction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onSecondaryTapDown(TapDownDetails details) {
|
||||||
|
// if selection is null, or
|
||||||
|
// selection.isCollapsedand and the selected node is TextNode.
|
||||||
|
// try to select the word.
|
||||||
|
final selection = currentSelection.value;
|
||||||
|
if (selection == null ||
|
||||||
|
(selection.isCollapsed == true &&
|
||||||
|
currentSelectedNodes.first is TextNode)) {
|
||||||
|
_onDoubleTapDown(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
_showContextMenu(details);
|
||||||
|
}
|
||||||
|
|
||||||
void _onPanStart(DragStartDetails details) {
|
void _onPanStart(DragStartDetails details) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
@ -477,6 +504,20 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
|||||||
_cursorKey.currentState?.unwrapOrNull<CursorWidgetState>()?.show();
|
_cursorKey.currentState?.unwrapOrNull<CursorWidgetState>()?.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showContextMenu(TapDownDetails details) {
|
||||||
|
final contextMenu = OverlayEntry(
|
||||||
|
builder: (context) => ContextMenu(
|
||||||
|
position: details.globalPosition,
|
||||||
|
editorState: editorState,
|
||||||
|
items: builtInContextMenuItems,
|
||||||
|
onPressed: () => _clearContextMenu(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_contextMenuAreas.add(contextMenu);
|
||||||
|
Overlay.of(context)?.insert(contextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
void _scrollUpOrDownIfNeeded() {
|
void _scrollUpOrDownIfNeeded() {
|
||||||
final dy = editorState.service.scrollService?.dy;
|
final dy = editorState.service.scrollService?.dy;
|
||||||
final selectNodes = currentSelectedNodes;
|
final selectNodes = currentSelectedNodes;
|
||||||
|
@ -208,12 +208,15 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
|||||||
command: 'end',
|
command: 'end',
|
||||||
handler: cursorEnd,
|
handler: cursorEnd,
|
||||||
),
|
),
|
||||||
|
ShortcutEvent(
|
||||||
// TODO: split the keys.
|
key: 'Delete Text by backspace',
|
||||||
|
command: 'backspace',
|
||||||
|
handler: backspaceEventHandler,
|
||||||
|
),
|
||||||
ShortcutEvent(
|
ShortcutEvent(
|
||||||
key: 'Delete Text',
|
key: 'Delete Text',
|
||||||
command: 'delete,backspace',
|
command: 'delete',
|
||||||
handler: deleteTextHandler,
|
handler: deleteEventHandler,
|
||||||
),
|
),
|
||||||
ShortcutEvent(
|
ShortcutEvent(
|
||||||
key: 'selection menu',
|
key: 'selection menu',
|
||||||
|
@ -3,5 +3,5 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
typedef ShortcutEventHandler = KeyEventResult Function(
|
typedef ShortcutEventHandler = KeyEventResult Function(
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
RawKeyEvent event,
|
RawKeyEvent? event,
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user