chore: fotmat the code and implement floating_toolbar

This commit is contained in:
Lucas.Xu 2023-04-28 13:33:13 +08:00
commit e64d829012
12 changed files with 126 additions and 37 deletions

View file

@ -22,8 +22,10 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
- always_declare_return_types
- require_trailing_commas
- unnecessary_raw_strings
- use_decorated_box
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -117,7 +117,7 @@ class _HomePageState extends State<HomePage> {
editorState: _editorState,
);
textRobot.insertText(
r'''
'''
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
@ -374,6 +374,6 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
editorStyle,
...darkPluginStyleExtension,
quote,
]);
],);
}
}

View file

@ -34,14 +34,23 @@ class SimpleEditor extends StatelessWidget {
..handler = debugPrint
..level = LogLevel.all;
onEditorStateChange(editorState);
final scrollController = ScrollController();
if (PlatformExtension.isDesktopOrWeb) {
return FloatingToolbar(
editorState: editorState,
child: _buildEditor(context, editorState));
editorState: editorState,
scrollController: scrollController,
child: _buildEditor(
context,
editorState,
scrollController,
),
);
} else {
return Column(
children: [
Expanded(child: _buildEditor(context, editorState)),
Expanded(
child: _buildEditor(context, editorState, scrollController),
),
if (Platform.isIOS || Platform.isAndroid)
_buildMobileToolbar(context, editorState),
],
@ -56,11 +65,16 @@ class SimpleEditor extends StatelessWidget {
);
}
Widget _buildEditor(BuildContext context, EditorState editorState) {
Widget _buildEditor(
BuildContext context,
EditorState editorState,
ScrollController? scrollController,
) {
return AppFlowyEditor(
editorState: editorState,
themeData: themeData,
autoFocus: editorState.document.isEmpty,
scrollController: scrollController,
// customBuilders: {
// 'paragraph': TextBlockComponentBuilder(),
// 'todo_list': TodoListBlockComponentBuilder(),

View file

@ -36,5 +36,5 @@ ThemeData customizeEditorTheme(BuildContext context) {
editorStyle,
...darkPluginStyleExtension,
quote,
]);
],);
}

View file

@ -1,5 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'package:flutter/services.dart';
Future<void> onInsert(

View file

@ -120,7 +120,7 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
@override
void startAutoScroll(
Offset offset, {
double edgeOffset = 200,
double edgeOffset = 100,
AxisDirection? direction,
}) =>
forward.startAutoScroll(

View file

@ -1,7 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/selection/desktop_selection_service.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart';
import 'package:appflowy_editor/src/editor/util/util.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;

View file

@ -1,6 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'package:appflowy_editor/src/editor/util/util.dart';
/// Show the slash menu
///

View file

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
/// A floating toolbar that displays at the top of the editor when the selection
/// and will be hidden when the selection is collapsed.
@ -9,10 +10,12 @@ class FloatingToolbar extends StatefulWidget {
const FloatingToolbar({
super.key,
required this.editorState,
required this.scrollController,
required this.child,
});
final EditorState editorState;
final ScrollController scrollController;
final Widget child;
@override
@ -29,17 +32,14 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
super.initState();
editorState.selectionNotifier.addListener(_onSelectionChanged);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
editorState.service.scrollService?.scrollController
.addListener(_onScrollPositionChanged);
});
widget.scrollController.addListener(_onScrollPositionChanged);
}
@override
void dispose() {
editorState.selectionNotifier.removeListener(_onSelectionChanged);
editorState.service.scrollService?.scrollController
.removeListener(_onScrollPositionChanged);
widget.scrollController.removeListener(_onScrollPositionChanged);
super.dispose();
}
@ -54,16 +54,17 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
if (selection == null || selection.isCollapsed) {
_clear();
} else {
_show();
// uses debounce to avoid the computing the rects too frequently.
_showAfter(const Duration(milliseconds: 200));
}
}
void _onScrollPositionChanged() {
final offset = editorState.service.scrollService?.scrollController.offset;
if (offset != null) {
Log.toolbar.debug('offset = $offset');
_show();
}
final offset = widget.scrollController.offset;
Log.toolbar.debug('offset = $offset');
_clear();
_showAfter(Duration.zero);
}
final String _debounceKey = 'show the toolbar';
@ -74,29 +75,75 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
_toolbarContainer = null;
}
void _show() {
void _showAfter([Duration duration = Duration.zero]) {
_clear(); // clear the previous toolbar
// uses debounce to avoid the computing the rects too frequently.
Debounce.debounce(
_debounceKey,
const Duration(milliseconds: 200),
duration,
() {
final rects = _computeSelectionRects();
if (rects.isNotEmpty) {
Log.toolbar.debug('rects = $rects');
}
_showToolbar();
},
);
}
void _showToolbar() {
final rects = _computeSelectionRects();
if (rects.isEmpty) {
return;
}
final offset = _findSuitableOffset(rects.map((e) => e.topLeft));
_toolbarContainer = OverlayEntry(
builder: (context) {
return Positioned(
left: offset.dx,
top: max(0, offset.dy) - 30,
child: _buildToolbar(context),
);
},
);
Overlay.of(context).insert(_toolbarContainer!);
}
Widget _buildToolbar(BuildContext context) {
return Container(
width: 300,
height: 30,
color: Colors.red,
);
}
/// Compute the rects of the selection.
List<Offset> _computeSelectionRects() {
List<Rect> _computeSelectionRects() {
final selection = editorState.selection;
if (selection == null || selection.isCollapsed) {
return [];
}
final nodes = editorState.getNodesInSelection(selection);
final rects = <Rect>[];
for (final node in nodes) {
final selectable = node.selectable;
if (selectable == null) {
continue;
}
final nodeRects = selectable.getRectsInSelection(selection);
if (nodeRects.isEmpty) {
continue;
}
final renderBox = node.renderBox;
if (renderBox == null) {
continue;
}
for (final rect in nodeRects) {
final globalOffset = renderBox.localToGlobal(rect.topLeft);
rects.add(globalOffset & rect.size);
}
}
/*
final rects = nodes
.map(
(node) => node.selectable
@ -109,7 +156,32 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
.whereNotNull()
.expand((element) => element)
.toList();
*/
return rects;
}
Offset _findSuitableOffset(Iterable<Offset> offsets) {
assert(offsets.isNotEmpty);
// find the min offset with non-negative dy.
final offsetsWithNonNegativeDy =
offsets.where((element) => element.dy >= 30);
if (offsetsWithNonNegativeDy.isEmpty) {
// if all the rects offset is negative, then the selection is not visible.
return offsets.last;
}
final minOffset = offsetsWithNonNegativeDy.reduce((min, current) {
if (min.dy < current.dy) {
return min;
} else if (min.dy == current.dy) {
return min.dx < current.dx ? min : current;
} else {
return current;
}
});
return minOffset;
}
}

View file

@ -40,6 +40,7 @@ class AppFlowyEditor extends StatefulWidget {
this.customActionMenuBuilder,
this.showDefaultToolbar = true,
this.shrinkWrap = false,
this.scrollController,
ThemeData? themeData,
}) : super(key: key) {
this.themeData = themeData ??
@ -87,6 +88,8 @@ class AppFlowyEditor extends StatefulWidget {
late final ThemeData themeData;
final ScrollController? scrollController;
@override
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
}
@ -169,6 +172,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
child: _buildScroll(
child: ScrollServiceWidget(
key: editorState.service.scrollServiceKey,
scrollController: widget.scrollController,
child: Container(
color: editorStyle.backgroundColor,
padding: editorStyle.padding!,

View file

@ -29,6 +29,7 @@ dependencies:
flutter_localizations:
sdk: flutter
tuple: ^2.0.1
collection: ^1.17.0
dev_dependencies:
flutter_test:

View file

@ -111,9 +111,9 @@ class TestableEditor {
_editorState.document.addParagraph(initialText: '');
}
Future<void> updateSelection(Selection? selection) {
Future<void> updateSelection(Selection? selection) async {
_editorState.selection = selection;
return tester.pumpAndSettle();
await tester.pumpAndSettle();
}
Node? nodeAtPath(Path path) {