mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-08-05 07:16:58 +00:00
234 lines
6.7 KiB
Dart
234 lines
6.7 KiB
Dart
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'emoji_actions_command.dart';
|
|
import 'emoji_handler.dart';
|
|
|
|
abstract class EmojiMenuService {
|
|
void show(String character);
|
|
|
|
void dismiss();
|
|
}
|
|
|
|
class EmojiMenu extends EmojiMenuService {
|
|
EmojiMenu({
|
|
required this.context,
|
|
required this.editorState,
|
|
this.startCharAmount = 1,
|
|
this.cancelBySpaceHandler,
|
|
this.menuHeight = 400,
|
|
this.menuWidth = 300,
|
|
});
|
|
|
|
final BuildContext context;
|
|
final EditorState editorState;
|
|
final double menuHeight;
|
|
final double menuWidth;
|
|
final bool Function()? cancelBySpaceHandler;
|
|
|
|
final int startCharAmount;
|
|
Offset _offset = Offset.zero;
|
|
Alignment _alignment = Alignment.topLeft;
|
|
OverlayEntry? _menuEntry;
|
|
bool selectionChangedByMenu = false;
|
|
String initialCharacter = '';
|
|
|
|
@override
|
|
void dismiss() {
|
|
if (_menuEntry != null) {
|
|
editorState.service.keyboardService?.enable();
|
|
editorState.service.scrollService?.enable();
|
|
keepEditorFocusNotifier.decrease();
|
|
}
|
|
|
|
_menuEntry?.remove();
|
|
_menuEntry = null;
|
|
|
|
// workaround: SelectionService has been released after hot reload.
|
|
final isSelectionDisposed =
|
|
editorState.service.selectionServiceKey.currentState == null;
|
|
if (!isSelectionDisposed) {
|
|
final selectionService = editorState.service.selectionService;
|
|
selectionService.currentSelection.removeListener(_onSelectionChange);
|
|
}
|
|
emojiMenuService = null;
|
|
}
|
|
|
|
void _onSelectionUpdate() => selectionChangedByMenu = true;
|
|
|
|
@override
|
|
void show(String character) {
|
|
initialCharacter = character;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _show());
|
|
}
|
|
|
|
void _show() {
|
|
final selectionService = editorState.service.selectionService;
|
|
final selectionRects = selectionService.selectionRects;
|
|
if (selectionRects.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final Size editorSize = editorState.renderBox!.size;
|
|
|
|
calculateSelectionMenuOffset(selectionRects.first);
|
|
|
|
final (left, top, right, bottom) = _getPosition();
|
|
|
|
_menuEntry = OverlayEntry(
|
|
builder: (context) => SizedBox(
|
|
height: editorSize.height,
|
|
width: editorSize.width,
|
|
|
|
// GestureDetector handles clicks outside of the context menu,
|
|
// to dismiss the context menu.
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: dismiss,
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
top: top,
|
|
bottom: bottom,
|
|
left: left,
|
|
right: right,
|
|
child: EmojiHandler(
|
|
editorState: editorState,
|
|
menuService: this,
|
|
onDismiss: dismiss,
|
|
onSelectionUpdate: _onSelectionUpdate,
|
|
startCharAmount: startCharAmount,
|
|
cancelBySpaceHandler: cancelBySpaceHandler,
|
|
initialSearchText: initialCharacter,
|
|
onEmojiSelect: (
|
|
BuildContext context,
|
|
(int, int) replacement,
|
|
String emoji,
|
|
) async {
|
|
final selection = editorState.selection;
|
|
|
|
if (selection == null) return;
|
|
final node =
|
|
editorState.document.nodeAtPath(selection.end.path);
|
|
if (node == null) return;
|
|
final transaction = editorState.transaction
|
|
..deleteText(
|
|
node,
|
|
replacement.$1,
|
|
replacement.$2 - replacement.$1,
|
|
)
|
|
..insertText(
|
|
node,
|
|
replacement.$1,
|
|
emoji,
|
|
);
|
|
await editorState.apply(transaction);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
Overlay.of(context).insert(_menuEntry!);
|
|
|
|
editorState.service.keyboardService?.disable(showCursor: true);
|
|
editorState.service.scrollService?.disable();
|
|
selectionService.currentSelection.addListener(_onSelectionChange);
|
|
}
|
|
|
|
void _onSelectionChange() {
|
|
// workaround: SelectionService has been released after hot reload.
|
|
final isSelectionDisposed =
|
|
editorState.service.selectionServiceKey.currentState == null;
|
|
if (!isSelectionDisposed) {
|
|
final selectionService = editorState.service.selectionService;
|
|
if (selectionService.currentSelection.value == null) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!selectionChangedByMenu) {
|
|
return dismiss();
|
|
}
|
|
|
|
selectionChangedByMenu = false;
|
|
}
|
|
|
|
(double? left, double? top, double? right, double? bottom) _getPosition() {
|
|
double? left, top, right, bottom;
|
|
switch (_alignment) {
|
|
case Alignment.topLeft:
|
|
left = _offset.dx;
|
|
top = _offset.dy;
|
|
break;
|
|
case Alignment.bottomLeft:
|
|
left = _offset.dx;
|
|
bottom = _offset.dy;
|
|
break;
|
|
case Alignment.topRight:
|
|
right = _offset.dx;
|
|
top = _offset.dy;
|
|
break;
|
|
case Alignment.bottomRight:
|
|
right = _offset.dx;
|
|
bottom = _offset.dy;
|
|
break;
|
|
}
|
|
|
|
return (left, top, right, bottom);
|
|
}
|
|
|
|
void calculateSelectionMenuOffset(Rect rect) {
|
|
// Workaround: We can customize the padding through the [EditorStyle],
|
|
// but the coordinates of overlay are not properly converted currently.
|
|
// Just subtract the padding here as a result.
|
|
const menuOffset = Offset(0, 10);
|
|
final editorOffset =
|
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
|
final editorHeight = editorState.renderBox!.size.height;
|
|
final editorWidth = editorState.renderBox!.size.width;
|
|
|
|
// show below default
|
|
_alignment = Alignment.topLeft;
|
|
final bottomRight = rect.bottomRight;
|
|
final topRight = rect.topRight;
|
|
var offset = bottomRight + menuOffset;
|
|
_offset = Offset(
|
|
offset.dx,
|
|
offset.dy,
|
|
);
|
|
|
|
// show above
|
|
if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {
|
|
offset = topRight - menuOffset;
|
|
_alignment = Alignment.bottomLeft;
|
|
|
|
_offset = Offset(
|
|
offset.dx,
|
|
editorHeight + editorOffset.dy - offset.dy,
|
|
);
|
|
}
|
|
|
|
// show on right
|
|
if (_offset.dx + menuWidth < editorOffset.dx + editorWidth) {
|
|
_offset = Offset(
|
|
_offset.dx,
|
|
_offset.dy,
|
|
);
|
|
} else if (offset.dx - editorOffset.dx > menuWidth) {
|
|
// show on left
|
|
_alignment = _alignment == Alignment.topLeft
|
|
? Alignment.topRight
|
|
: Alignment.bottomRight;
|
|
|
|
_offset = Offset(
|
|
editorWidth - _offset.dx + editorOffset.dx,
|
|
_offset.dy,
|
|
);
|
|
}
|
|
}
|
|
}
|