From 70c465daef8c06ecde1748b7fbaadf644b9a026c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 15 Nov 2023 10:48:43 +0800 Subject: [PATCH 1/4] feat: optimize mobile editing --- example/lib/home_page.dart | 3 +- .../editor/toolbar/mobile/mobile_toolbar.dart | 52 +++++++++++++++---- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 836d77cc8..ce4e6cc50 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -87,13 +87,14 @@ class _HomePageState extends State { key: _scaffoldKey, extendBodyBehindAppBar: PlatformExtension.isDesktopOrWeb, drawer: _buildDrawer(context), + resizeToAvoidBottomInset: false, appBar: AppBar( backgroundColor: const Color.fromARGB(255, 134, 46, 247), foregroundColor: Colors.white, surfaceTintColor: Colors.transparent, title: const Text('AppFlowy Editor'), ), - body: SafeArea(child: _buildBody(context)), + body: _buildBody(context), ); } diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar.dart index e8eafc37a..fcc5c4c51 100644 --- a/lib/src/editor/toolbar/mobile/mobile_toolbar.dart +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar.dart @@ -90,7 +90,7 @@ abstract class MobileToolbarWidgetService { void closeItemMenu(); } -class MobileToolbarWidget extends StatefulWidget { +class MobileToolbarWidget extends StatefulWidget with WidgetsBindingObserver { const MobileToolbarWidget({ super.key, required this.editorState, @@ -111,6 +111,9 @@ class MobileToolbarWidgetState extends State bool _showItemMenu = false; int? _selectedToolbarItemIndex; + double previousKeyboardHeight = 0.0; + bool updateKeyboardHeight = true; + @override void closeItemMenu() { if (_showItemMenu) { @@ -124,6 +127,13 @@ class MobileToolbarWidgetState extends State Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; final style = MobileToolbarStyle.of(context); + + final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; + if (updateKeyboardHeight) { + previousKeyboardHeight = keyboardHeight; + } + // print('object $keyboardHeight'); + return Column( children: [ Container( @@ -147,15 +157,27 @@ class MobileToolbarWidgetState extends State toolbarItems: widget.toolbarItems, toolbarWidgetService: this, itemWithMenuOnPressed: (selectedItemIndex) { + updateKeyboardHeight = false; setState(() { // If last selected item is selected again, toggle item menu if (_selectedToolbarItemIndex == selectedItemIndex) { _showItemMenu = !_showItemMenu; + + if (!_showItemMenu) { + // updateKeyboardHeight = true; + widget.editorState.service.keyboardService + ?.enableKeyBoard(widget.selection); + } else { + updateKeyboardHeight = false; + widget.editorState.service.keyboardService + ?.closeKeyboard(); + } } else { _selectedToolbarItemIndex = selectedItemIndex; // If not, show item menu _showItemMenu = true; // close keyboard when menu pop up + widget.editorState.service.keyboardService ?.closeKeyboard(); } @@ -174,8 +196,8 @@ class MobileToolbarWidgetState extends State onPressed: () { setState(() { _showItemMenu = false; - widget.editorState.service.keyboardService! - .enableKeyBoard(widget.selection); + widget.editorState.service.keyboardService + ?.enableKeyBoard(widget.selection); }); }, icon: const AFMobileIcon( @@ -187,14 +209,22 @@ class MobileToolbarWidgetState extends State ), ), // only for MobileToolbarItem.withMenu - if (_showItemMenu && _selectedToolbarItemIndex != null) - MobileToolbarItemMenu( - editorState: widget.editorState, - itemMenuBuilder: () => widget - .toolbarItems[_selectedToolbarItemIndex!] - // pass current [MobileToolbarWidgetState] to be used to closeItemMenu - .itemMenuBuilder!(widget.editorState, widget.selection, this), - ), + (_showItemMenu && _selectedToolbarItemIndex != null) + ? Container( + constraints: BoxConstraints( + minHeight: previousKeyboardHeight, + ), + child: MobileToolbarItemMenu( + editorState: widget.editorState, + itemMenuBuilder: () => widget + .toolbarItems[_selectedToolbarItemIndex!] + // pass current [MobileToolbarWidgetState] to be used to closeItemMenu + .itemMenuBuilder!(widget.editorState, widget.selection, this), + ), + ) + : SizedBox( + height: previousKeyboardHeight, + ), ], ); } From 0b7f8859965710d29f666257d10fa232e25fea1b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 15 Nov 2023 13:57:42 +0800 Subject: [PATCH 2/4] feat: refactot the mobile toolbar --- example/lib/pages/mobile_editor.dart | 2 +- lib/src/editor/toolbar/mobile/mobile.dart | 1 + .../editor/toolbar/mobile/mobile_toolbar.dart | 15 +- .../toolbar/mobile/mobile_toolbar_item.dart | 23 +- .../toolbar/mobile/mobile_toolbar_style.dart | 66 ++-- .../toolbar/mobile/mobile_toolbar_v2.dart | 305 ++++++++++++++++++ .../code_mobile_toolbar_item.dart | 10 +- .../background_color_options_widgets.dart | 2 +- ...xt_and_background_color_tool_bar_item.dart | 17 +- .../color/text_color_options_widgets.dart | 2 +- .../color/utils/clear_color_button.dart | 2 +- .../color/utils/color_button.dart | 2 +- .../divider_mobile_toolbar_item.dart | 9 +- .../heading_mobile_toolbar_item.dart | 13 +- .../link_mobile_toolbar_item.dart | 10 +- .../list_mobile_toolbar_item.dart | 11 +- .../quote_mobile_toolbar_item.dart | 18 +- .../text_decoration_mobile_toolbar_item.dart | 13 +- .../todo_list_mobile_toolbar_item.dart | 16 +- .../utils/mobile_toolbar_item_menu.dart | 2 +- .../utils/mobile_toolbar_item_menu_btn.dart | 2 +- .../mobile_toolbar_menu_grid_delegate.dart | 2 +- .../link_mobile_toolbar_item_test.dart | 6 +- .../mobile/mobile_toolbar_item_test.dart | 6 +- .../mobile/mobile_toolbar_style_test.dart | 10 +- .../mobile_toolbar_style_test_widget.dart | 8 +- 26 files changed, 463 insertions(+), 110 deletions(-) create mode 100644 lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart diff --git a/example/lib/pages/mobile_editor.dart b/example/lib/pages/mobile_editor.dart index cd434e8ad..49ad2c164 100644 --- a/example/lib/pages/mobile_editor.dart +++ b/example/lib/pages/mobile_editor.dart @@ -90,7 +90,7 @@ class _MobileEditorState extends State { ), ), // build mobile toolbar - MobileToolbar( + MobileToolbarV2( editorState: editorState, toolbarItems: [ textDecorationMobileToolbarItem, diff --git a/lib/src/editor/toolbar/mobile/mobile.dart b/lib/src/editor/toolbar/mobile/mobile.dart index a494e335f..21330ddc3 100644 --- a/lib/src/editor/toolbar/mobile/mobile.dart +++ b/lib/src/editor/toolbar/mobile/mobile.dart @@ -2,5 +2,6 @@ export 'mobile_floating_toolbar/mobile_floating_toolbar.dart'; export 'mobile_toolbar.dart'; export 'mobile_toolbar_item.dart'; export 'mobile_toolbar_style.dart'; +export 'mobile_toolbar_v2.dart'; export 'toolbar_items/toolbar_items.dart'; export 'utils/utils.dart'; diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar.dart index fcc5c4c51..c06a1397c 100644 --- a/lib/src/editor/toolbar/mobile/mobile_toolbar.dart +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar.dart @@ -53,14 +53,14 @@ class MobileToolbar extends StatelessWidget { return const SizedBox.shrink(); } return RepaintBoundary( - child: MobileToolbarStyle( + child: MobileToolbarTheme( backgroundColor: backgroundColor, foregroundColor: foregroundColor, clearDiagonalLineColor: clearDiagonalLineColor, itemHighlightColor: itemHighlightColor, itemOutlineColor: itemOutlineColor, - tabbarSelectedBackgroundColor: tabbarSelectedBackgroundColor, - tabbarSelectedForegroundColor: tabbarSelectedForegroundColor, + tabBarSelectedBackgroundColor: tabbarSelectedBackgroundColor, + tabBarSelectedForegroundColor: tabbarSelectedForegroundColor, primaryColor: primaryColor, onPrimaryColor: onPrimaryColor, outlineColor: outlineColor, @@ -126,7 +126,7 @@ class MobileToolbarWidgetState extends State @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; if (updateKeyboardHeight) { @@ -219,7 +219,7 @@ class MobileToolbarWidgetState extends State itemMenuBuilder: () => widget .toolbarItems[_selectedToolbarItemIndex!] // pass current [MobileToolbarWidgetState] to be used to closeItemMenu - .itemMenuBuilder!(widget.editorState, widget.selection, this), + .itemMenuBuilder!(context, widget.editorState, this), ), ) : SizedBox( @@ -269,9 +269,10 @@ class _ToolbarItemListView extends StatelessWidget { return ListView.builder( itemBuilder: (context, index) { final toolbarItem = toolbarItems[index]; - final icon = toolbarItem.itemIconBuilder( + final icon = toolbarItem.itemIconBuilder?.call( context, editorState, + toolbarWidgetService, ); if (icon == null) { return const SizedBox.shrink(); @@ -286,8 +287,8 @@ class _ToolbarItemListView extends StatelessWidget { // close menu if other item's menu is still on the screen toolbarWidgetService.closeItemMenu(); toolbarItems[index].actionHandler?.call( + context, editorState, - selection, ); } }, diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar_item.dart index 5ffa3d8a1..a124ddac3 100644 --- a/lib/src/editor/toolbar/mobile/mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar_item.dart @@ -1,16 +1,16 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -typedef MobileToolbarItemMenuBuilder = Widget Function( +typedef MobileToolbarItemIconBuilder = Widget Function( + BuildContext context, EditorState editorState, - Selection selection, // To access to the state of MobileToolbarWidget MobileToolbarWidgetService service, ); typedef MobileToolbarItemActionHandler = void Function( + BuildContext context, EditorState editorState, - Selection selection, ); class MobileToolbarItem { @@ -19,20 +19,21 @@ class MobileToolbarItem { required this.itemIconBuilder, required this.actionHandler, }) : hasMenu = false, - itemMenuBuilder = null; + itemMenuBuilder = null, + assert(itemIconBuilder != null && actionHandler != null); /// Tool bar item that opens a menu to show options const MobileToolbarItem.withMenu({ required this.itemIconBuilder, required this.itemMenuBuilder, }) : hasMenu = true, - actionHandler = null; + actionHandler = null, + assert(itemMenuBuilder != null && itemIconBuilder != null); - final bool hasMenu; - final Widget? Function( - BuildContext context, - EditorState editorState, - ) itemIconBuilder; - final MobileToolbarItemMenuBuilder? itemMenuBuilder; + // if the result is null, the item will be hidden + final MobileToolbarItemIconBuilder? itemIconBuilder; final MobileToolbarItemActionHandler? actionHandler; + + final bool hasMenu; + final MobileToolbarItemIconBuilder? itemMenuBuilder; } diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar_style.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar_style.dart index 891a89a66..a7112565e 100644 --- a/lib/src/editor/toolbar/mobile/mobile_toolbar_style.dart +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar_style.dart @@ -7,14 +7,35 @@ import 'package:flutter/material.dart'; /// itemHighlightColor -> selected item border color /// /// itemOutlineColor -> item border color -class MobileToolbarStyle extends InheritedWidget { +class MobileToolbarTheme extends InheritedWidget { + const MobileToolbarTheme({ + super.key, + this.backgroundColor = Colors.white, + this.foregroundColor = const Color(0xff676666), + this.clearDiagonalLineColor = const Color(0xffB3261E), + this.itemHighlightColor = const Color(0xff1F71AC), + this.itemOutlineColor = const Color(0xFFE3E3E3), + this.tabBarSelectedBackgroundColor = const Color(0x23808080), + this.tabBarSelectedForegroundColor = Colors.black, + this.primaryColor = const Color(0xff1F71AC), + this.onPrimaryColor = Colors.white, + this.outlineColor = const Color(0xFFE3E3E3), + this.toolbarHeight = 50.0, + this.borderRadius = 6.0, + this.buttonHeight = 40.0, + this.buttonSpacing = 8.0, + this.buttonBorderWidth = 1.0, + this.buttonSelectedBorderWidth = 2.0, + required super.child, + }); + final Color backgroundColor; final Color foregroundColor; final Color clearDiagonalLineColor; final Color itemHighlightColor; final Color itemOutlineColor; - final Color tabbarSelectedBackgroundColor; - final Color tabbarSelectedForegroundColor; + final Color tabBarSelectedBackgroundColor; + final Color tabBarSelectedForegroundColor; final Color primaryColor; final Color onPrimaryColor; final Color outlineColor; @@ -25,44 +46,25 @@ class MobileToolbarStyle extends InheritedWidget { final double buttonBorderWidth; final double buttonSelectedBorderWidth; - const MobileToolbarStyle({ - super.key, - required this.backgroundColor, - required this.foregroundColor, - required this.clearDiagonalLineColor, - required this.itemHighlightColor, - required this.itemOutlineColor, - required this.tabbarSelectedBackgroundColor, - required this.tabbarSelectedForegroundColor, - required this.primaryColor, - required this.onPrimaryColor, - required this.outlineColor, - required this.toolbarHeight, - required this.borderRadius, - required this.buttonHeight, - required this.buttonSpacing, - required this.buttonBorderWidth, - required this.buttonSelectedBorderWidth, - required super.child, - }); + static MobileToolbarTheme of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()!; + } - static MobileToolbarStyle of(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType()!; + static MobileToolbarTheme? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); } @override - bool updateShouldNotify(covariant MobileToolbarStyle oldWidget) { - // This function is called whenever the inherited widget is rebuilt. - // It should return true if the new widget's values are different from the old widget's values. + bool updateShouldNotify(covariant MobileToolbarTheme oldWidget) { return backgroundColor != oldWidget.backgroundColor || foregroundColor != oldWidget.foregroundColor || clearDiagonalLineColor != oldWidget.clearDiagonalLineColor || itemHighlightColor != oldWidget.itemHighlightColor || itemOutlineColor != oldWidget.itemOutlineColor || - tabbarSelectedBackgroundColor != - oldWidget.tabbarSelectedBackgroundColor || - tabbarSelectedForegroundColor != - oldWidget.tabbarSelectedForegroundColor || + tabBarSelectedBackgroundColor != + oldWidget.tabBarSelectedBackgroundColor || + tabBarSelectedForegroundColor != + oldWidget.tabBarSelectedForegroundColor || primaryColor != oldWidget.primaryColor || onPrimaryColor != oldWidget.onPrimaryColor || outlineColor != oldWidget.outlineColor || diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart new file mode 100644 index 000000000..74c29876d --- /dev/null +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart @@ -0,0 +1,305 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +class MobileToolbarV2 extends StatelessWidget { + const MobileToolbarV2({ + super.key, + required this.editorState, + required this.toolbarItems, + }); + + final EditorState editorState; + final List toolbarItems; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: editorState.selectionNotifier, + builder: (_, Selection? selection, __) { + // if the selection is null, hide the toolbar + if (selection == null) { + return const SizedBox.shrink(); + } + + Widget child = _MobileToolbar( + editorState: editorState, + toolbarItems: toolbarItems, + ); + + // if the MobileToolbarTheme is not provided, provide it + if (MobileToolbarTheme.maybeOf(context) == null) { + child = MobileToolbarTheme( + child: child, + ); + } + + return RepaintBoundary( + child: child, + ); + }, + ); + } +} + +class _MobileToolbar extends StatefulWidget { + const _MobileToolbar({ + required this.editorState, + required this.toolbarItems, + }); + + final EditorState editorState; + final List toolbarItems; + + @override + State<_MobileToolbar> createState() => _MobileToolbarState(); +} + +class _MobileToolbarState extends State<_MobileToolbar> + implements MobileToolbarWidgetService { + // used to control the toolbar menu items + ValueNotifier showMenuNotifier = ValueNotifier(false); + + // when the users click the menu item, the keyboard will be hidden, + // but in this case, we don't want to update the cached keyboard height. + // This is because we want to keep the same height when the menu is shown. + bool canUpdateCachedKeyboardHeight = true; + ValueNotifier cachedKeyboardHeight = ValueNotifier(0.0); + double get keyboardHeight => MediaQuery.of(context).viewInsets.bottom; + + // used to check if click the same item again + int? selectedMenuIndex; + + @override + void dispose() { + showMenuNotifier.dispose(); + cachedKeyboardHeight.dispose(); + + super.dispose(); + } + + @override + void reassemble() { + super.reassemble(); + + canUpdateCachedKeyboardHeight = true; + closeItemMenu(); + _closeKeyboard(); + } + + @override + Widget build(BuildContext context) { + // update the keyboard height here. + // try to get the height in `didChangeMetrics`, but it's not accurate. + if (canUpdateCachedKeyboardHeight) { + cachedKeyboardHeight.value = keyboardHeight; + } + // toolbar + // - if the menu is shown, the toolbar will be pushed up by the height of the menu + // - otherwise, add a spacer to push the toolbar up when the keyboard is shown + return Column( + children: [ + _buildToolbar(context), + _buildMenuOrSpacer(context), + ], + ); + } + + @override + void closeItemMenu() { + showMenuNotifier.value = false; + } + + void showItemMenu() { + showMenuNotifier.value = true; + } + + // toolbar list view and close keyboard/menu button + Widget _buildToolbar(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final style = MobileToolbarTheme.of(context); + + return Container( + width: width, + height: style.toolbarHeight, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: style.itemOutlineColor, + ), + bottom: BorderSide(color: style.itemOutlineColor), + ), + color: style.backgroundColor, + ), + child: Row( + children: [ + // toolbar list view + Expanded( + child: _ToolbarItemListView( + toolbarItems: widget.toolbarItems, + editorState: widget.editorState, + toolbarWidgetService: this, + itemWithMenuOnPressed: (index) { + // click the same one + if (selectedMenuIndex == index && showMenuNotifier.value) { + // if the menu is shown, close it and show the keyboard + closeItemMenu(); + _showKeyboard(); + // update the cached keyboard height after the keyboard is shown + Future.delayed(const Duration(milliseconds: 500), () { + canUpdateCachedKeyboardHeight = true; + }); + } else { + canUpdateCachedKeyboardHeight = false; + selectedMenuIndex = index; + showItemMenu(); + _closeKeyboard(); + } + }, + ), + ), + // divider + const Padding( + padding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 4.0, + ), + child: VerticalDivider(), + ), + // close menu or close keyboard button + ValueListenableBuilder( + valueListenable: showMenuNotifier, + builder: (_, showingMenu, __) { + return _CloseKeyboardOrMenuButton( + showingMenu: showingMenu, + onPressed: () { + if (showingMenu) { + // close the menu and show the keyboard + closeItemMenu(); + _showKeyboard(); + } else { + // close the keyboard and clear the selection + // if the selection is null, the keyboard and the toolbar will be hidden automatically + widget.editorState.selection = null; + } + }, + ); + }, + ), + ], + ), + ); + } + + // if there's no menu, we need to add a spacer to push the toolbar up when the keyboard is shown + Widget _buildMenuOrSpacer(BuildContext context) { + return ValueListenableBuilder( + valueListenable: cachedKeyboardHeight, + builder: (_, height, ___) { + return ValueListenableBuilder( + valueListenable: showMenuNotifier, + builder: (_, showingMenu, __) { + return ConstrainedBox( + constraints: BoxConstraints(minHeight: height), + child: !(showMenuNotifier.value && selectedMenuIndex != null) + ? const SizedBox.shrink() + : MobileToolbarItemMenu( + editorState: widget.editorState, + itemMenuBuilder: () => widget + .toolbarItems[selectedMenuIndex!].itemMenuBuilder! + .call( + context, + widget.editorState, + this, + ), + ), + ); + }, + ); + }, + ); + } + + void _showKeyboard() { + final selection = widget.editorState.selection; + if (selection != null) { + widget.editorState.service.keyboardService?.enableKeyBoard(selection); + } + } + + void _closeKeyboard() { + widget.editorState.service.keyboardService?.closeKeyboard(); + } +} + +class _ToolbarItemListView extends StatelessWidget { + const _ToolbarItemListView({ + required this.toolbarItems, + required this.editorState, + required this.toolbarWidgetService, + required this.itemWithMenuOnPressed, + }); + + final Function(int index) itemWithMenuOnPressed; + final List toolbarItems; + final EditorState editorState; + final MobileToolbarWidgetService toolbarWidgetService; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (context, index) { + final toolbarItem = toolbarItems[index]; + final icon = toolbarItem.itemIconBuilder?.call( + context, + editorState, + toolbarWidgetService, + ); + if (icon == null) { + return const SizedBox.shrink(); + } + return IconButton( + icon: icon, + onPressed: () { + if (toolbarItem.hasMenu) { + // open /close current item menu through its parent widget(MobileToolbarWidget) + itemWithMenuOnPressed.call(index); + } else { + // close menu if other item's menu is still on the screen + toolbarWidgetService.closeItemMenu(); + toolbarItems[index].actionHandler?.call( + context, + editorState, + ); + } + }, + ); + }, + itemCount: toolbarItems.length, + scrollDirection: Axis.horizontal, + ); + } +} + +class _CloseKeyboardOrMenuButton extends StatelessWidget { + const _CloseKeyboardOrMenuButton({ + required this.showingMenu, + required this.onPressed, + }); + + final bool showingMenu; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return IconButton( + padding: EdgeInsets.zero, + alignment: Alignment.centerLeft, + onPressed: onPressed, + icon: showingMenu + ? const AFMobileIcon( + afMobileIcons: AFMobileIcons.close, + ) + : const Icon(Icons.keyboard_hide), + ); + } +} diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart index 82555212c..9a391475a 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/code_mobile_toolbar_item.dart @@ -1,8 +1,10 @@ import 'package:appflowy_editor/appflowy_editor.dart'; final codeMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.code), - actionHandler: (editorState, selection) => - editorState.toggleAttribute(AppFlowyRichTextKeys.code), + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.code, + ), + actionHandler: (_, editorState) => editorState.toggleAttribute( + AppFlowyRichTextKeys.code, + ), ); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart index fc90b2a2f..9a4f30668 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart @@ -22,7 +22,7 @@ class _BackgroundColorOptionsWidgetsState extends State { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); final colorOptions = widget.backgroundColorOptions ?? generateHighlightColorOptions(); final selection = widget.selection; diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item.dart index b2bace6e4..2faaa42fb 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_and_background_color_tool_bar_item.dart @@ -6,9 +6,14 @@ MobileToolbarItem buildTextAndBackgroundColorMobileToolbarItem({ List? backgroundColorOptions, }) { return MobileToolbarItem.withMenu( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.color), - itemMenuBuilder: (editorState, selection, _) { + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.color, + ), + itemMenuBuilder: (_, editorState, ___) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } return _TextAndBackgroundColorMenu( editorState, selection, @@ -41,7 +46,7 @@ class _TextAndBackgroundColorMenuState extends State<_TextAndBackgroundColorMenu> { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); List myTabs = [ Tab( text: AppFlowyEditorL10n.current.textColor, @@ -58,10 +63,10 @@ class _TextAndBackgroundColorMenuState child: TabBar( indicatorSize: TabBarIndicatorSize.tab, tabs: myTabs, - labelColor: style.tabbarSelectedForegroundColor, + labelColor: style.tabBarSelectedForegroundColor, indicator: BoxDecoration( borderRadius: BorderRadius.circular(style.borderRadius), - color: style.tabbarSelectedBackgroundColor, + color: style.tabBarSelectedForegroundColor, ), // remove the bottom line of TabBar dividerColor: Colors.transparent, diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart index 2d7c0ece3..69987a2fa 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart @@ -21,7 +21,7 @@ class TextColorOptionsWidgets extends StatefulWidget { class _TextColorOptionsWidgetsState extends State { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); final selection = widget.selection; final nodes = widget.editorState.getNodesInSelection(selection); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/clear_color_button.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/clear_color_button.dart index a953a67e1..80e76da87 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/clear_color_button.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/clear_color_button.dart @@ -12,7 +12,7 @@ class ClearColorButton extends StatelessWidget { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); return InkWell( onTap: onPressed, diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/color_button.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/color_button.dart index 349dac4da..dab24a997 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/color_button.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/utils/color_button.dart @@ -17,7 +17,7 @@ class ColorButton extends StatelessWidget { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); return InkWell( onTap: onPressed, child: Container( diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item.dart index f64a066e1..f518a5852 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/divider_mobile_toolbar_item.dart @@ -1,9 +1,10 @@ import 'package:appflowy_editor/appflowy_editor.dart'; final dividerMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.divider), - actionHandler: ((editorState, selection) { + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.divider, + ), + actionHandler: (_, editorState) { // same as the [handler] of [dividerMenuItem] in Desktop final selection = editorState.selection; if (selection == null || !selection.isCollapsed) { @@ -29,5 +30,5 @@ final dividerMobileToolbarItem = MobileToolbarItem.action( transaction.afterSelection = Selection.collapsed(Position(path: insertedPath.next)); editorState.apply(transaction); - }), + }, ); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/heading_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/heading_mobile_toolbar_item.dart index 8952cd329..2068d045a 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/heading_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/heading_mobile_toolbar_item.dart @@ -2,9 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final headingMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.heading), - itemMenuBuilder: (editorState, selection, _) { + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.heading, + ), + itemMenuBuilder: (_, editorState, __) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } return _HeadingMenu( selection, editorState, @@ -46,7 +51,7 @@ class _HeadingMenuState extends State<_HeadingMenu> { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); final size = MediaQuery.sizeOf(context); final btnList = headings.map((currentHeading) { // Check if current node is heading and its level diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart index a2ab8b828..4d2382e3a 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/link_mobile_toolbar_item.dart @@ -2,10 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final linkMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __) => const AFMobileIcon( + itemIconBuilder: (_, __, ___) => const AFMobileIcon( afMobileIcons: AFMobileIcons.link, ), - itemMenuBuilder: (editorState, selection, itemMenuService) { + itemMenuBuilder: (_, editorState, itemMenuService) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } final String? linkText = editorState.getDeltaAttributeValueInSelection( AppFlowyRichTextKeys.href, selection, @@ -65,7 +69,7 @@ class _MobileLinkMenuState extends State { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); const double spacing = 8; return Material( // TextField widget needs to be wrapped in a Material widget to provide a visual appearance diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/list_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/list_mobile_toolbar_item.dart index 49f5f7338..d075e4a76 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/list_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/list_mobile_toolbar_item.dart @@ -2,9 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final listMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.list), - itemMenuBuilder: (editorState, selection, _) { + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.list, + ), + itemMenuBuilder: (_, editorState, __) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } return _ListMenu(editorState, selection); }, ); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/quote_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/quote_mobile_toolbar_item.dart index 14ea0045a..cec1eeb21 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/quote_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/quote_mobile_toolbar_item.dart @@ -1,10 +1,18 @@ import 'package:appflowy_editor/appflowy_editor.dart'; final quoteMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.quote), - actionHandler: ((editorState, selection) { - final node = editorState.getNodeAtPath(selection.start.path)!; + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.quote, + ), + actionHandler: (context, editorState) async { + final selection = editorState.selection; + if (selection == null) { + return; + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null) { + return; + } final isQuote = node.type == QuoteBlockKeys.type; editorState.formatNode( selection, @@ -15,5 +23,5 @@ final quoteMobileToolbarItem = MobileToolbarItem.action( }, ), ); - }), + }, ); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart index 83010e46b..d4530e634 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item.dart @@ -2,9 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final textDecorationMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.textDecoration), - itemMenuBuilder: (editorState, selection, _) { + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.textDecoration, + ), + itemMenuBuilder: (_, editorState, __) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } return _TextDecorationMenu(editorState, selection); }, ); @@ -47,7 +52,7 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> { ]; @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); final btnList = textDecorations.map((currentDecoration) { // Check current decoration is active or not final selection = widget.selection; diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/todo_list_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/todo_list_mobile_toolbar_item.dart index 20495355c..7d9812fca 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/todo_list_mobile_toolbar_item.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/todo_list_mobile_toolbar_item.dart @@ -1,10 +1,18 @@ import 'package:appflowy_editor/appflowy_editor.dart'; final todoListMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.checkbox), - actionHandler: (editorState, selection) async { - final node = editorState.getNodeAtPath(selection.start.path)!; + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.checkbox, + ), + actionHandler: (context, editorState) async { + final selection = editorState.selection; + if (selection == null) { + return; + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null) { + return; + } final isTodoList = node.type == TodoListBlockKeys.type; editorState.formatNode( diff --git a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu.dart b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu.dart index 887e735ed..38ba9f480 100644 --- a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu.dart +++ b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu.dart @@ -14,7 +14,7 @@ class MobileToolbarItemMenu extends StatelessWidget { @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); return Container( width: size.width, diff --git a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart index 22660bf9c..3b3a69ee3 100644 --- a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart +++ b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart @@ -17,7 +17,7 @@ class MobileToolbarItemMenuBtn extends StatelessWidget { @override Widget build(BuildContext context) { - final style = MobileToolbarStyle.of(context); + final style = MobileToolbarTheme.of(context); return OutlinedButton.icon( onPressed: onPressed, icon: icon ?? const SizedBox.shrink(), diff --git a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_menu_grid_delegate.dart b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_menu_grid_delegate.dart index 4b1cd7984..f3e70e237 100644 --- a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_menu_grid_delegate.dart +++ b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_menu_grid_delegate.dart @@ -2,7 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/rendering.dart'; SliverGridDelegate buildMobileToolbarMenuGridDelegate({ - required MobileToolbarStyle mobileToolbarStyle, + required MobileToolbarTheme mobileToolbarStyle, required int crossAxisCount, }) { return SliverGridDelegateWithFixedCrossAxisCountAndFixedHeight( diff --git a/test/editor/toolbar/link_mobile_toolbar_item_test.dart b/test/editor/toolbar/link_mobile_toolbar_item_test.dart index 167c8b555..37ed2e6ef 100644 --- a/test/editor/toolbar/link_mobile_toolbar_item_test.dart +++ b/test/editor/toolbar/link_mobile_toolbar_item_test.dart @@ -80,14 +80,14 @@ void main() { }); } -Widget _wrapWithStyle({required Widget child}) => MobileToolbarStyle( +Widget _wrapWithStyle({required Widget child}) => MobileToolbarTheme( backgroundColor: Colors.blue, foregroundColor: Colors.blue, clearDiagonalLineColor: Colors.blue, itemHighlightColor: Colors.blue, itemOutlineColor: Colors.blue, - tabbarSelectedBackgroundColor: Colors.blue, - tabbarSelectedForegroundColor: Colors.blue, + tabBarSelectedBackgroundColor: Colors.blue, + tabBarSelectedForegroundColor: Colors.blue, primaryColor: Colors.blue, onPrimaryColor: Colors.blue, outlineColor: Colors.blue, diff --git a/test/mobile/toolbar/mobile/mobile_toolbar_item_test.dart b/test/mobile/toolbar/mobile/mobile_toolbar_item_test.dart index c71660f37..ae9771073 100644 --- a/test/mobile/toolbar/mobile/mobile_toolbar_item_test.dart +++ b/test/mobile/toolbar/mobile/mobile_toolbar_item_test.dart @@ -6,7 +6,7 @@ void main() { group('MobileToolbarItem', () { test('action item should not have a menu', () { final item = MobileToolbarItem.action( - itemIconBuilder: (_, __) => const Icon(Icons.format_bold), + itemIconBuilder: (_, __, ___) => const Icon(Icons.format_bold), actionHandler: (editorState, selection) {}, ); @@ -15,8 +15,8 @@ void main() { test('menu item should have a menu', () { final item = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __) => const Icon(Icons.format_color_text), - itemMenuBuilder: (editorState, selection, _) { + itemIconBuilder: (_, __, ___) => const Icon(Icons.format_color_text), + itemMenuBuilder: (_, editorState, __) { return Container(); }, ); diff --git a/test/mobile/toolbar/mobile/mobile_toolbar_style_test.dart b/test/mobile/toolbar/mobile/mobile_toolbar_style_test.dart index 6f387e424..001c21ba7 100644 --- a/test/mobile/toolbar/mobile/mobile_toolbar_style_test.dart +++ b/test/mobile/toolbar/mobile/mobile_toolbar_style_test.dart @@ -1,6 +1,6 @@ +import 'package:appflowy_editor/src/editor/toolbar/mobile/mobile_toolbar_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy_editor/src/editor/toolbar/mobile/mobile_toolbar_style.dart'; void main() { testWidgets('MobileToolbarStyle should have correct values', @@ -23,14 +23,14 @@ void main() { const buttonSelectedBorderWidth = 2.0; await tester.pumpWidget( - const MobileToolbarStyle( + const MobileToolbarTheme( backgroundColor: backgroundColor, foregroundColor: foregroundColor, clearDiagonalLineColor: clearDiagonalLineColor, itemHighlightColor: itemHighlightColor, itemOutlineColor: itemOutlineColor, - tabbarSelectedBackgroundColor: tabbarSelectedBackgroundColor, - tabbarSelectedForegroundColor: tabbarSelectedForegroundColor, + tabBarSelectedBackgroundColor: tabbarSelectedBackgroundColor, + tabBarSelectedForegroundColor: tabbarSelectedForegroundColor, primaryColor: primaryColor, onPrimaryColor: onPrimaryColor, outlineColor: outlineColor, @@ -45,7 +45,7 @@ void main() { ); final mobileToolbarStyle = - MobileToolbarStyle.of(tester.element(find.byType(MobileToolbarStyle))); + MobileToolbarTheme.of(tester.element(find.byType(MobileToolbarTheme))); expect(mobileToolbarStyle.backgroundColor, equals(backgroundColor)); expect(mobileToolbarStyle.foregroundColor, equals(foregroundColor)); diff --git a/test/mobile/toolbar/mobile/test_helpers/mobile_toolbar_style_test_widget.dart b/test/mobile/toolbar/mobile/test_helpers/mobile_toolbar_style_test_widget.dart index 6896558fa..616b52b34 100644 --- a/test/mobile/toolbar/mobile/test_helpers/mobile_toolbar_style_test_widget.dart +++ b/test/mobile/toolbar/mobile/test_helpers/mobile_toolbar_style_test_widget.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; /// Used in testing mobile app with toolbar class MobileToolbarStyleTestWidget extends StatelessWidget { @@ -45,14 +45,14 @@ class MobileToolbarStyleTestWidget extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - home: MobileToolbarStyle( + home: MobileToolbarTheme( backgroundColor: backgroundColor, foregroundColor: foregroundColor, clearDiagonalLineColor: clearDiagonalLineColor, itemHighlightColor: itemHighlightColor, itemOutlineColor: itemOutlineColor, - tabbarSelectedBackgroundColor: tabbarSelectedBackgroundColor, - tabbarSelectedForegroundColor: tabbarSelectedForegroundColor, + tabBarSelectedBackgroundColor: tabbarSelectedBackgroundColor, + tabBarSelectedForegroundColor: tabbarSelectedForegroundColor, primaryColor: primaryColor, onPrimaryColor: onPrimaryColor, outlineColor: outlineColor, From 2dee8c2fa61503a0f64bc96510712e0f911d5d0f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 15 Nov 2023 15:16:54 +0800 Subject: [PATCH 3/4] feat: don't reattch text input if the selection doesn't change --- example/lib/pages/mobile_editor.dart | 1 + .../service/keyboard_service_widget.dart | 15 ++- .../toolbar/mobile/mobile_toolbar_v2.dart | 61 +++++++--- ...ext_decoration_mobile_toolbar_item_v2.dart | 106 ++++++++++++++++++ .../mobile/toolbar_items/toolbar_items.dart | 9 +- .../utils/mobile_toolbar_item_menu_btn.dart | 27 +++-- 6 files changed, 190 insertions(+), 29 deletions(-) create mode 100644 lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart diff --git a/example/lib/pages/mobile_editor.dart b/example/lib/pages/mobile_editor.dart index 49ad2c164..caa9f3a14 100644 --- a/example/lib/pages/mobile_editor.dart +++ b/example/lib/pages/mobile_editor.dart @@ -94,6 +94,7 @@ class _MobileEditorState extends State { editorState: editorState, toolbarItems: [ textDecorationMobileToolbarItem, + textDecorationMobileToolbarItemV2, buildTextAndBackgroundColorMobileToolbarItem(), headingMobileToolbarItem, todoListMobileToolbarItem, diff --git a/lib/src/editor/editor_component/service/keyboard_service_widget.dart b/lib/src/editor/editor_component/service/keyboard_service_widget.dart index e0ec0b9b0..8ebb393a2 100644 --- a/lib/src/editor/editor_component/service/keyboard_service_widget.dart +++ b/lib/src/editor/editor_component/service/keyboard_service_widget.dart @@ -33,6 +33,9 @@ class KeyboardServiceWidgetState extends State late final TextInputService textInputService; late final FocusNode focusNode; + // previous selection + Selection? previousSelection; + // use for IME only bool enableShortcuts = true; @@ -187,9 +190,17 @@ class KeyboardServiceWidgetState extends State if (doNotAttach == true) { return; } - enableShortcuts = true; + // attach the delta text input service if needed final selection = editorState.selection; + + if (PlatformExtension.isMobile && previousSelection == selection) { + // no need to attach the text input service if the selection is not changed. + return; + } + + enableShortcuts = true; + if (selection == null) { textInputService.close(); } else { @@ -202,6 +213,8 @@ class KeyboardServiceWidgetState extends State Log.editor.debug('keyboard service - request focus'); } } + + previousSelection = selection; } void _attachTextInputService(Selection selection) { diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart index 74c29876d..d52b9a6b6 100644 --- a/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart @@ -15,21 +15,16 @@ class MobileToolbarV2 extends StatelessWidget { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: editorState.selectionNotifier, - builder: (_, Selection? selection, __) { + builder: (_, Selection? selection, child) { // if the selection is null, hide the toolbar if (selection == null) { return const SizedBox.shrink(); } - Widget child = _MobileToolbar( - editorState: editorState, - toolbarItems: toolbarItems, - ); - // if the MobileToolbarTheme is not provided, provide it if (MobileToolbarTheme.maybeOf(context) == null) { child = MobileToolbarTheme( - child: child, + child: child!, ); } @@ -37,6 +32,10 @@ class MobileToolbarV2 extends StatelessWidget { child: child, ); }, + child: _MobileToolbar( + editorState: editorState, + toolbarItems: toolbarItems, + ), ); } } @@ -57,7 +56,7 @@ class _MobileToolbar extends StatefulWidget { class _MobileToolbarState extends State<_MobileToolbar> implements MobileToolbarWidgetService { // used to control the toolbar menu items - ValueNotifier showMenuNotifier = ValueNotifier(false); + PropertyValueNotifier showMenuNotifier = PropertyValueNotifier(false); // when the users click the menu item, the keyboard will be hidden, // but in this case, we don't want to update the cached keyboard height. @@ -69,6 +68,25 @@ class _MobileToolbarState extends State<_MobileToolbar> // used to check if click the same item again int? selectedMenuIndex; + Selection? currentSelection; + + @override + void initState() { + super.initState(); + + currentSelection = widget.editorState.selection; + } + + @override + void didUpdateWidget(covariant _MobileToolbar oldWidget) { + super.didUpdateWidget(oldWidget); + + if (currentSelection != widget.editorState.selection) { + currentSelection = widget.editorState.selection; + closeItemMenu(); + } + } + @override void dispose() { showMenuNotifier.dispose(); @@ -138,6 +156,17 @@ class _MobileToolbarState extends State<_MobileToolbar> toolbarItems: widget.toolbarItems, editorState: widget.editorState, toolbarWidgetService: this, + itemWithActionOnPressed: (_) { + if (showMenuNotifier.value) { + closeItemMenu(); + _showKeyboard(); + // update the cached keyboard height after the keyboard is shown + Debounce.debounce('canUpdateCachedKeyboardHeight', + const Duration(milliseconds: 500), () { + canUpdateCachedKeyboardHeight = true; + }); + } + }, itemWithMenuOnPressed: (index) { // click the same one if (selectedMenuIndex == index && showMenuNotifier.value) { @@ -145,7 +174,8 @@ class _MobileToolbarState extends State<_MobileToolbar> closeItemMenu(); _showKeyboard(); // update the cached keyboard height after the keyboard is shown - Future.delayed(const Duration(milliseconds: 500), () { + Debounce.debounce('canUpdateCachedKeyboardHeight', + const Duration(milliseconds: 500), () { canUpdateCachedKeyboardHeight = true; }); } else { @@ -200,9 +230,8 @@ class _MobileToolbarState extends State<_MobileToolbar> builder: (_, showingMenu, __) { return ConstrainedBox( constraints: BoxConstraints(minHeight: height), - child: !(showMenuNotifier.value && selectedMenuIndex != null) - ? const SizedBox.shrink() - : MobileToolbarItemMenu( + child: (showingMenu && selectedMenuIndex != null) + ? MobileToolbarItemMenu( editorState: widget.editorState, itemMenuBuilder: () => widget .toolbarItems[selectedMenuIndex!].itemMenuBuilder! @@ -211,7 +240,8 @@ class _MobileToolbarState extends State<_MobileToolbar> widget.editorState, this, ), - ), + ) + : const SizedBox.shrink(), ); }, ); @@ -237,9 +267,11 @@ class _ToolbarItemListView extends StatelessWidget { required this.editorState, required this.toolbarWidgetService, required this.itemWithMenuOnPressed, + required this.itemWithActionOnPressed, }); final Function(int index) itemWithMenuOnPressed; + final Function(int index) itemWithActionOnPressed; final List toolbarItems; final EditorState editorState; final MobileToolbarWidgetService toolbarWidgetService; @@ -262,8 +294,9 @@ class _ToolbarItemListView extends StatelessWidget { onPressed: () { if (toolbarItem.hasMenu) { // open /close current item menu through its parent widget(MobileToolbarWidget) - itemWithMenuOnPressed.call(index); + itemWithMenuOnPressed(index); } else { + itemWithActionOnPressed(index); // close menu if other item's menu is still on the screen toolbarWidgetService.closeItemMenu(); toolbarItems[index].actionHandler?.call( diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart new file mode 100644 index 000000000..b0c0906cb --- /dev/null +++ b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart @@ -0,0 +1,106 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final textDecorationMobileToolbarItemV2 = MobileToolbarItem.withMenu( + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.textDecoration, + ), + itemMenuBuilder: (_, editorState, __) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } + return _TextDecorationMenu(editorState, selection); + }, +); + +class _TextDecorationMenu extends StatefulWidget { + const _TextDecorationMenu( + this.editorState, + this.selection, + ); + + final EditorState editorState; + final Selection selection; + + @override + State<_TextDecorationMenu> createState() => _TextDecorationMenuState(); +} + +class _TextDecorationMenuState extends State<_TextDecorationMenu> { + final textDecorations = [ + // BIUS + TextDecorationUnit( + icon: AFMobileIcons.bold, + label: AppFlowyEditorL10n.current.bold, + name: AppFlowyRichTextKeys.bold, + ), + TextDecorationUnit( + icon: AFMobileIcons.italic, + label: AppFlowyEditorL10n.current.italic, + name: AppFlowyRichTextKeys.italic, + ), + TextDecorationUnit( + icon: AFMobileIcons.underline, + label: AppFlowyEditorL10n.current.underline, + name: AppFlowyRichTextKeys.underline, + ), + TextDecorationUnit( + icon: AFMobileIcons.strikethrough, + label: AppFlowyEditorL10n.current.strikethrough, + name: AppFlowyRichTextKeys.strikethrough, + ), + + // Code + TextDecorationUnit( + icon: AFMobileIcons.code, + label: AppFlowyEditorL10n.current.embedCode, + name: AppFlowyRichTextKeys.code, + ), + ]; + + @override + Widget build(BuildContext context) { + final style = MobileToolbarTheme.of(context); + + final bius = textDecorations.map((currentDecoration) { + // Check current decoration is active or not + final selection = widget.selection; + final nodes = widget.editorState.getNodesInSelection(selection); + final bool isSelected; + if (selection.isCollapsed) { + isSelected = widget.editorState.toggledStyle.containsKey( + currentDecoration.name, + ); + } else { + isSelected = nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[currentDecoration.name] == true, + ); + }); + } + + return MobileToolbarItemMenuBtn( + icon: AFMobileIcon( + afMobileIcons: currentDecoration.icon, + ), + label: Text(currentDecoration.label), + isSelected: isSelected, + onPressed: () { + setState(() { + widget.editorState.toggleAttribute(currentDecoration.name); + }); + }, + ); + }).toList(); + + return GridView( + shrinkWrap: true, + gridDelegate: buildMobileToolbarMenuGridDelegate( + mobileToolbarStyle: style, + crossAxisCount: 2, + ), + children: bius, + ); + } +} diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart b/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart index 27c2f2ef9..8bc5b9e90 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart @@ -1,9 +1,10 @@ -export 'text_decoration_mobile_toolbar_item.dart'; export 'code_mobile_toolbar_item.dart'; +export 'color/color.dart'; +export 'divider_mobile_toolbar_item.dart'; +export 'heading_mobile_toolbar_item.dart'; export 'link_mobile_toolbar_item.dart'; export 'list_mobile_toolbar_item.dart'; export 'quote_mobile_toolbar_item.dart'; -export 'heading_mobile_toolbar_item.dart'; -export 'divider_mobile_toolbar_item.dart'; +export 'text_decoration_mobile_toolbar_item.dart'; +export 'text_decoration_mobile_toolbar_item_v2.dart'; export 'todo_list_mobile_toolbar_item.dart'; -export 'color/color.dart'; diff --git a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart index 3b3a69ee3..78cb50e91 100644 --- a/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart +++ b/lib/src/editor/toolbar/mobile/utils/mobile_toolbar_item_menu_btn.dart @@ -6,24 +6,22 @@ class MobileToolbarItemMenuBtn extends StatelessWidget { super.key, required this.onPressed, this.icon, - required this.label, + this.label, required this.isSelected, }); final Function() onPressed; final Widget? icon; - final Widget label; + final Widget? label; final bool isSelected; @override Widget build(BuildContext context) { final style = MobileToolbarTheme.of(context); - return OutlinedButton.icon( + return OutlinedButton( onPressed: onPressed, - icon: icon ?? const SizedBox.shrink(), - label: label, style: ButtonStyle( - alignment: Alignment.centerLeft, + alignment: label == null ? Alignment.center : Alignment.centerLeft, foregroundColor: MaterialStateProperty.all(style.foregroundColor), splashFactory: NoSplash.splashFactory, side: MaterialStateProperty.resolveWith( @@ -43,12 +41,21 @@ class MobileToolbarItemMenuBtn extends StatelessWidget { ), ), padding: MaterialStateProperty.all( - const EdgeInsets.symmetric( - vertical: 0, - horizontal: 8, - ), + EdgeInsets.zero, ), ), + child: Row( + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6.0, + ), + child: icon!, + ), + label ?? const SizedBox.shrink(), + ], + ), ); } } From 75ea5e3241870f12f275969744560d8a9e056ad9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 15 Nov 2023 15:42:35 +0800 Subject: [PATCH 4/4] chore: update mobile toolbar layout --- example/lib/pages/mobile_editor.dart | 7 +- .../toolbar/mobile/mobile_toolbar_v2.dart | 10 +- .../blocks_mobile_toolbar_item.dart | 137 ++++++++++++++++++ .../mobile/toolbar_items/toolbar_items.dart | 1 + 4 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 lib/src/editor/toolbar/mobile/toolbar_items/blocks_mobile_toolbar_item.dart diff --git a/example/lib/pages/mobile_editor.dart b/example/lib/pages/mobile_editor.dart index caa9f3a14..35f2bd84e 100644 --- a/example/lib/pages/mobile_editor.dart +++ b/example/lib/pages/mobile_editor.dart @@ -93,16 +93,11 @@ class _MobileEditorState extends State { MobileToolbarV2( editorState: editorState, toolbarItems: [ - textDecorationMobileToolbarItem, textDecorationMobileToolbarItemV2, buildTextAndBackgroundColorMobileToolbarItem(), - headingMobileToolbarItem, - todoListMobileToolbarItem, - listMobileToolbarItem, + blocksMobileToolbarItem, linkMobileToolbarItem, - quoteMobileToolbarItem, dividerMobileToolbarItem, - codeMobileToolbarItem, ], ), ], diff --git a/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart b/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart index d52b9a6b6..6d3a2fc9d 100644 --- a/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart +++ b/lib/src/editor/toolbar/mobile/mobile_toolbar_v2.dart @@ -149,6 +149,7 @@ class _MobileToolbarState extends State<_MobileToolbar> color: style.backgroundColor, ), child: Row( + mainAxisSize: MainAxisSize.min, children: [ // toolbar list view Expanded( @@ -191,9 +192,10 @@ class _MobileToolbarState extends State<_MobileToolbar> const Padding( padding: EdgeInsets.symmetric( vertical: 8, - horizontal: 4.0, ), - child: VerticalDivider(), + child: VerticalDivider( + width: 1, + ), ), // close menu or close keyboard button ValueListenableBuilder( @@ -215,6 +217,9 @@ class _MobileToolbarState extends State<_MobileToolbar> ); }, ), + const SizedBox( + width: 4.0, + ), ], ), ); @@ -326,7 +331,6 @@ class _CloseKeyboardOrMenuButton extends StatelessWidget { Widget build(BuildContext context) { return IconButton( padding: EdgeInsets.zero, - alignment: Alignment.centerLeft, onPressed: onPressed, icon: showingMenu ? const AFMobileIcon( diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/blocks_mobile_toolbar_item.dart b/lib/src/editor/toolbar/mobile/toolbar_items/blocks_mobile_toolbar_item.dart new file mode 100644 index 000000000..0b08b7fa9 --- /dev/null +++ b/lib/src/editor/toolbar/mobile/toolbar_items/blocks_mobile_toolbar_item.dart @@ -0,0 +1,137 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final blocksMobileToolbarItem = MobileToolbarItem.withMenu( + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.list, + ), + itemMenuBuilder: (_, editorState, __) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } + return _BlocksMenu(editorState, selection); + }, +); + +class _BlocksMenu extends StatefulWidget { + const _BlocksMenu( + this.editorState, + this.selection, + ); + + final Selection selection; + final EditorState editorState; + + @override + State<_BlocksMenu> createState() => _BlocksMenuState(); +} + +class _BlocksMenuState extends State<_BlocksMenu> { + final lists = [ + // heading + _ListUnit( + icon: AFMobileIcons.h1, + label: AppFlowyEditorL10n.current.mobileHeading1, + name: HeadingBlockKeys.type, + level: 1, + ), + _ListUnit( + icon: AFMobileIcons.h2, + label: AppFlowyEditorL10n.current.mobileHeading2, + name: HeadingBlockKeys.type, + level: 2, + ), + _ListUnit( + icon: AFMobileIcons.h3, + label: AppFlowyEditorL10n.current.mobileHeading3, + name: HeadingBlockKeys.type, + level: 3, + ), + // list + _ListUnit( + icon: AFMobileIcons.bulletedList, + label: AppFlowyEditorL10n.current.bulletedList, + name: BulletedListBlockKeys.type, + ), + _ListUnit( + icon: AFMobileIcons.numberedList, + label: AppFlowyEditorL10n.current.numberedList, + name: NumberedListBlockKeys.type, + ), + _ListUnit( + icon: AFMobileIcons.checkbox, + label: AppFlowyEditorL10n.current.checkbox, + name: TodoListBlockKeys.type, + ), + _ListUnit( + icon: AFMobileIcons.quote, + label: AppFlowyEditorL10n.current.quote, + name: QuoteBlockKeys.type, + ), + ]; + + @override + Widget build(BuildContext context) { + final children = lists.map((list) { + // Check if current node is list and its type + final node = widget.editorState.getNodeAtPath( + widget.selection.start.path, + )!; + + final isSelected = node.type == list.name && + (list.level == null || + node.attributes[HeadingBlockKeys.level] == list.level); + + return MobileToolbarItemMenuBtn( + icon: AFMobileIcon(afMobileIcons: list.icon), + label: Text(list.label), + isSelected: isSelected, + onPressed: () { + setState(() { + widget.editorState.formatNode( + widget.selection, + (node) => node.copyWith( + type: isSelected ? ParagraphBlockKeys.type : list.name, + attributes: { + ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(), + blockComponentBackgroundColor: + node.attributes[blockComponentBackgroundColor], + if (!isSelected && list.name == TodoListBlockKeys.type) + TodoListBlockKeys.checked: false, + if (!isSelected && list.name == HeadingBlockKeys.type) + HeadingBlockKeys.level: list.level, + }, + ), + ); + }); + }, + ); + }).toList(); + + return GridView( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 5, + ), + children: children, + ); + } +} + +class _ListUnit { + final AFMobileIcons icon; + final String label; + final String name; + final int? level; + + _ListUnit({ + required this.icon, + required this.label, + required this.name, + this.level, + }); +} diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart b/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart index 8bc5b9e90..fac598323 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/toolbar_items.dart @@ -1,3 +1,4 @@ +export 'blocks_mobile_toolbar_item.dart'; export 'code_mobile_toolbar_item.dart'; export 'color/color.dart'; export 'divider_mobile_toolbar_item.dart';