chore: adjust context menu visibility logic (#1158)
* chore: default example context menu * chore: adjust context menu visibility logic * chore: dart analyzer
This commit is contained in:
parent
aacec0801e
commit
15289103a2
6 changed files with 69 additions and 29 deletions
|
|
@ -97,6 +97,14 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
||||||
dropTargetStyle: const AppFlowyDropTargetStyle(
|
dropTargetStyle: const AppFlowyDropTargetStyle(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
|
contextMenuBuilder: (context, position, editorState, onPressed) {
|
||||||
|
return ContextMenu(
|
||||||
|
position: position,
|
||||||
|
editorState: editorState,
|
||||||
|
items: standardContextMenuItems,
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
},
|
||||||
header: Padding(
|
header: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 10.0),
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export 'src/plugins/plugins.dart';
|
||||||
export 'src/render/selection/selectable.dart';
|
export 'src/render/selection/selectable.dart';
|
||||||
export 'src/render/toolbar/toolbar_item.dart';
|
export 'src/render/toolbar/toolbar_item.dart';
|
||||||
export 'src/service/context_menu/context_menu.dart';
|
export 'src/service/context_menu/context_menu.dart';
|
||||||
|
export 'src/service/context_menu/built_in_context_menu_item.dart';
|
||||||
export 'src/service/default_text_operations/format_rich_text_style.dart';
|
export 'src/service/default_text_operations/format_rich_text_style.dart';
|
||||||
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
export 'src/service/shortcut_event/key_mapping.dart';
|
export 'src/service/shortcut_event/key_mapping.dart';
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
|
||||||
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
@ -125,8 +124,7 @@ class AppFlowyEditor extends StatefulWidget {
|
||||||
///
|
///
|
||||||
/// It will be shown when the user right click on the editor.
|
/// It will be shown when the user right click on the editor.
|
||||||
///
|
///
|
||||||
/// Defaults to a standard implementation containing
|
/// See the built-in [ContextMenu] implementation.
|
||||||
/// [standardContextMenuItems].
|
|
||||||
///
|
///
|
||||||
final ContextMenuWidgetBuilder? contextMenuBuilder;
|
final ContextMenuWidgetBuilder? contextMenuBuilder;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -286,15 +286,45 @@ class _DesktopSelectionServiceWidgetState
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSecondaryTapDown(TapDownDetails details) {
|
void _onSecondaryTapDown(TapDownDetails details) {
|
||||||
// if selection is null, or
|
final offset = details.globalPosition;
|
||||||
// selection.isCollapsed and the selected node is TextNode.
|
|
||||||
// try to select the word.
|
|
||||||
final selection = editorState.selectionNotifier.value;
|
final selection = editorState.selectionNotifier.value;
|
||||||
if (selection == null ||
|
final selectable = getNodeInOffset(offset)?.selectable;
|
||||||
(selection.isCollapsed == true &&
|
if (selectable == null) {
|
||||||
currentSelectedNodes.first.delta != null)) {
|
clearSelection();
|
||||||
_onDoubleTapDown(details);
|
return;
|
||||||
}
|
}
|
||||||
|
final wordBoundary = selectable.getWordBoundaryInOffset(offset);
|
||||||
|
|
||||||
|
final Selection? newSelection;
|
||||||
|
|
||||||
|
final isSelectionCollapsed = selection == null || selection.isCollapsed;
|
||||||
|
|
||||||
|
final isWordBoundaryWithinSelection = !isSelectionCollapsed &&
|
||||||
|
wordBoundary != null &&
|
||||||
|
(wordBoundary.start.path > selection.start.path ||
|
||||||
|
wordBoundary.start.path == selection.start.path &&
|
||||||
|
wordBoundary.start.offset >= selection.start.offset) &&
|
||||||
|
(wordBoundary.end.path < selection.end.path ||
|
||||||
|
wordBoundary.end.path == selection.end.path &&
|
||||||
|
wordBoundary.end.offset <= selection.end.offset);
|
||||||
|
|
||||||
|
if (isWordBoundaryWithinSelection) {
|
||||||
|
newSelection = selection;
|
||||||
|
} else if (selectable.cursorStyle == CursorStyle.verticalLine) {
|
||||||
|
newSelection = wordBoundary;
|
||||||
|
} else {
|
||||||
|
newSelection = Selection(
|
||||||
|
start: selectable.start(),
|
||||||
|
end: selectable.end(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
newSelection,
|
||||||
|
extraInfo: {
|
||||||
|
selectionExtraInfoDisableToolbar: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
_showContextMenu(details);
|
_showContextMenu(details);
|
||||||
}
|
}
|
||||||
|
|
@ -436,25 +466,22 @@ class _DesktopSelectionServiceWidgetState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isHitSelectionAreas = currentSelection.value?.isCollapsed == true ||
|
|
||||||
selectionRects.any((element) {
|
|
||||||
const threshold = 20;
|
|
||||||
final scaledArea = Rect.fromCenter(
|
|
||||||
center: element.center,
|
|
||||||
width: element.width + threshold,
|
|
||||||
height: element.height + threshold,
|
|
||||||
);
|
|
||||||
return scaledArea.contains(details.globalPosition);
|
|
||||||
});
|
|
||||||
if (!isHitSelectionAreas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, only support the text node.
|
// For now, only support the text node.
|
||||||
if (!currentSelectedNodes.every((element) => element.delta != null)) {
|
if (!currentSelectedNodes.every((element) => element.delta != null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final mask = OverlayEntry(
|
||||||
|
builder: (_) => Listener(
|
||||||
|
onPointerDown: (_) => _clearContextMenu(),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_contextMenuAreas.add(mask);
|
||||||
|
Overlay.of(context, rootOverlay: true).insert(mask);
|
||||||
|
|
||||||
final baseOffset =
|
final baseOffset =
|
||||||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
final offset = details.localPosition + const Offset(10, 10) + baseOffset;
|
final offset = details.localPosition + const Offset(10, 10) + baseOffset;
|
||||||
|
|
|
||||||
|
|
@ -135,16 +135,23 @@ class _FloatingToolbarState extends State<FloatingToolbar>
|
||||||
void _onSelectionChanged() {
|
void _onSelectionChanged() {
|
||||||
final selection = editorState.selection;
|
final selection = editorState.selection;
|
||||||
final selectionType = editorState.selectionType;
|
final selectionType = editorState.selectionType;
|
||||||
|
|
||||||
|
final disableToolbar =
|
||||||
|
editorState.selectionExtraInfo?[selectionExtraInfoDisableToolbar] ==
|
||||||
|
true;
|
||||||
|
|
||||||
|
if (disableToolbar) {
|
||||||
|
_clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (lastSelection == selection) return;
|
if (lastSelection == selection) return;
|
||||||
lastSelection = selection;
|
lastSelection = selection;
|
||||||
|
|
||||||
if (selection == null ||
|
if (selection == null ||
|
||||||
selection.isCollapsed ||
|
selection.isCollapsed ||
|
||||||
selectionType == SelectionType.block ||
|
selectionType == SelectionType.block) {
|
||||||
editorState.selectionExtraInfo?[selectionExtraInfoDisableToolbar] ==
|
|
||||||
true) {
|
|
||||||
_clear();
|
_clear();
|
||||||
} else {
|
} else if (!disableToolbar) {
|
||||||
// uses debounce to avoid the computing the rects too frequently.
|
// uses debounce to avoid the computing the rects too frequently.
|
||||||
_showAfterDelay(
|
_showAfterDelay(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/ime/text_input_service.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/ime/text_input_service.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue