-
Notifications
You must be signed in to change notification settings - Fork 210
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import 'package:appflowy_editor/appflowy_editor.dart'; | ||
import 'package:flutter/material.dart'; | ||
|
||
class MobileFloatingToolbarItem { | ||
const MobileFloatingToolbarItem({ | ||
required this.builder, | ||
}); | ||
|
||
final WidgetBuilder builder; | ||
} | ||
|
||
/// A mobile floating toolbar that displays at the top of the editor when the selection is not collapsed. | ||
/// and will be hidden when the selection is collapsed. | ||
/// | ||
/// Normally, it will show copy, cut, paste. | ||
class MobileFloatingToolbar extends StatefulWidget { | ||
const MobileFloatingToolbar({ | ||
super.key, | ||
required this.editorState, | ||
required this.editorScrollController, | ||
required this.child, | ||
}); | ||
|
||
final EditorState editorState; | ||
final EditorScrollController editorScrollController; | ||
final Widget child; | ||
|
||
@override | ||
State<MobileFloatingToolbar> createState() => _MobileFloatingToolbarState(); | ||
Check warning on line 29 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L28-L29
|
||
} | ||
|
||
class _MobileFloatingToolbarState extends State<MobileFloatingToolbar> | ||
with WidgetsBindingObserver { | ||
OverlayEntry? _toolbarContainer; | ||
|
||
EditorState get editorState => widget.editorState; | ||
|
||
bool _isToolbarVisible = false; | ||
// use for skipping the first build for the toolbar when the selection is collapsed. | ||
Selection? prevSelection; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
|
||
WidgetsBinding.instance.addObserver(this); | ||
editorState.selectionNotifier.addListener(_onSelectionChanged); | ||
widget.editorScrollController.offsetNotifier.addListener( | ||
_onScrollPositionChanged, | ||
Check warning on line 49 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L46-L49
|
||
); | ||
} | ||
|
||
@override | ||
void didUpdateWidget(MobileFloatingToolbar oldWidget) { | ||
super.didUpdateWidget(oldWidget); | ||
|
||
if (widget.editorState != oldWidget.editorState) { | ||
editorState.selectionNotifier.addListener(_onSelectionChanged); | ||
Check warning on line 58 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L57-L58
|
||
} | ||
} | ||
|
||
@override | ||
void dispose() { | ||
editorState.selectionNotifier.removeListener(_onSelectionChanged); | ||
widget.editorScrollController.offsetNotifier.removeListener( | ||
_onScrollPositionChanged, | ||
Check warning on line 66 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L64-L66
|
||
); | ||
WidgetsBinding.instance.removeObserver(this); | ||
|
||
_clear(); | ||
|
||
super.dispose(); | ||
} | ||
|
||
@override | ||
void reassemble() { | ||
super.reassemble(); | ||
|
||
_clear(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return widget.child; | ||
} | ||
|
||
void _onSelectionChanged() { | ||
final selection = editorState.selection; | ||
final selectionType = editorState.selectionType; | ||
if (selection == null || selectionType == SelectionType.block) { | ||
_clear(); | ||
} else if (selection.isCollapsed) { | ||
if (_isToolbarVisible) { | ||
_clear(); | ||
} else if (prevSelection == selection) { | ||
_showAfterDelay(const Duration(milliseconds: 400)); | ||
Check warning on line 96 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L87-L96
|
||
} | ||
prevSelection = selection; | ||
} else { | ||
// uses debounce to avoid the computing the rects too frequently. | ||
_showAfterDelay(const Duration(milliseconds: 400)); | ||
Check warning on line 101 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L101
|
||
} | ||
} | ||
|
||
void _onScrollPositionChanged() { | ||
_clear(); | ||
Check warning on line 106 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L105-L106
|
||
} | ||
|
||
final String _debounceKey = 'show the toolbar'; | ||
void _clear() { | ||
Debounce.cancel(_debounceKey); | ||
Check warning on line 111 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L110-L111
|
||
|
||
_toolbarContainer?.remove(); | ||
_toolbarContainer = null; | ||
_isToolbarVisible = false; | ||
prevSelection = null; | ||
Check warning on line 116 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L113-L116
|
||
} | ||
|
||
void _showAfterDelay([Duration duration = Duration.zero]) { | ||
Check warning on line 119 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L119
|
||
// uses debounce to avoid the computing the rects too frequently. | ||
Debounce.debounce( | ||
_debounceKey, | ||
Check warning on line 122 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L121-L122
|
||
duration, | ||
() { | ||
_clear(); // clear the previous toolbar. | ||
_showToolbar(); | ||
Check warning on line 126 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L124-L126
|
||
}, | ||
); | ||
} | ||
|
||
void _showToolbar() { | ||
final rects = editorState.selectionRects(); | ||
if (rects.isEmpty) { | ||
Check warning on line 133 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L131-L133
|
||
return; | ||
} | ||
|
||
final rect = _findSuitableRect(rects); | ||
_toolbarContainer = OverlayEntry( | ||
builder: (context) { | ||
return _buildToolbar( | ||
Check warning on line 140 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L137-L140
|
||
context, | ||
rect.topCenter, | ||
Check warning on line 142 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L142
|
||
); | ||
}, | ||
); | ||
Overlay.of(context).insert(_toolbarContainer!); | ||
_isToolbarVisible = true; | ||
Check warning on line 147 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L146-L147
|
||
} | ||
|
||
Widget _buildToolbar( | ||
Check warning on line 150 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L150
|
||
BuildContext context, | ||
Offset? offset, | ||
) { | ||
return AdaptiveTextSelectionToolbar.editable( | ||
Check warning on line 154 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L154
|
||
clipboardStatus: ClipboardStatus.pasteable, | ||
onCopy: () => copyCommand.execute(editorState), | ||
onCut: () => cutCommand.execute(editorState), | ||
onPaste: () => pasteCommand.execute(editorState), | ||
onSelectAll: () => selectAllCommand.execute(editorState), | ||
anchors: TextSelectionToolbarAnchors( | ||
Check warning on line 160 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L156-L160
|
||
primaryAnchor: offset ?? Offset.zero, | ||
), | ||
); | ||
} | ||
|
||
Rect _findSuitableRect(Iterable<Rect> rects) { | ||
assert(rects.isNotEmpty); | ||
Check warning on line 167 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L166-L167
|
||
|
||
final editorOffset = | ||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; | ||
Check warning on line 170 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L170
|
||
|
||
// find the min offset with non-negative dy. | ||
final rectsWithNonNegativeDy = rects.where( | ||
(element) => element.top >= editorOffset.dy, | ||
Check warning on line 174 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L173-L174
|
||
); | ||
if (rectsWithNonNegativeDy.isEmpty) { | ||
Check warning on line 176 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L176
|
||
// if all the rects offset is negative, then the selection is not visible. | ||
return Rect.zero; | ||
} | ||
|
||
final minRect = rectsWithNonNegativeDy.reduce((min, current) { | ||
if (min.top < current.top) { | ||
Check warning on line 182 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L181-L182
|
||
return min; | ||
} else if (min.top == current.top) { | ||
return min.top < current.top ? min : current; | ||
Check warning on line 185 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L184-L185
|
||
} else { | ||
return current; | ||
} | ||
}); | ||
|
||
return minRect; | ||
} | ||
|
||
(double? left, double top, double? right) calculateToolbarOffset(Rect rect) { | ||
Check warning on line 194 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L194
|
||
final editorOffset = | ||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; | ||
final editorSize = editorState.renderBox?.size ?? Size.zero; | ||
final editorRect = editorOffset & editorSize; | ||
final left = (rect.left - editorOffset.dx).abs(); | ||
final right = (rect.right - editorOffset.dx).abs(); | ||
final width = editorSize.width; | ||
final threshold = width / 3.0; | ||
final top = rect.top < floatingToolbarHeight | ||
? rect.bottom + floatingToolbarHeight | ||
: rect.top; | ||
if (left <= threshold) { | ||
Check warning on line 206 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L196-L206
|
||
// show in left | ||
return (rect.left, top, null); | ||
} else if (left >= threshold && right <= threshold * 2.0) { | ||
Check warning on line 209 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L208-L209
|
||
// show in center | ||
return (threshold, top, null); | ||
} else { | ||
// show in right | ||
return (null, top, editorRect.right - rect.right); | ||
Check warning on line 214 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart Codecov / codecov/patchlib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L214
|
||
} | ||
} | ||
} |