| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 | import 'dart:async';import 'dart:math';import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';import 'package:easy_localization/easy_localization.dart';import 'package:flutter_quill/flutter_quill.dart';import 'package:flutter/material.dart';import 'package:styled_widget/styled_widget.dart';import 'check_button.dart';import 'color_picker.dart';import 'header_button.dart';import 'history_button.dart';import 'link_button.dart';import 'toggle_button.dart';import 'toolbar_icon_button.dart';import 'package:app_flowy/generated/locale_keys.g.dart';class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {  final List<Widget> children;  final double toolBarHeight;  final Color? color;  const EditorToolbar({    required this.children,    this.toolBarHeight = 46,    this.color,    Key? key,  }) : super(key: key);  @override  Widget build(BuildContext context) {    return Container(      color: Theme.of(context).canvasColor,      constraints: BoxConstraints.tightFor(height: preferredSize.height),      child: ToolbarButtonList(buttons: children)          .padding(horizontal: 4, vertical: 4),    );  }  @override  Size get preferredSize => Size.fromHeight(toolBarHeight);  factory EditorToolbar.basic({    required QuillController controller,    double toolbarIconSize = defaultIconSize,    OnImagePickCallback? onImagePickCallback,    OnVideoPickCallback? onVideoPickCallback,    MediaPickSettingSelector? mediaPickSettingSelector,    FilePickImpl? filePickImpl,    WebImagePickImpl? webImagePickImpl,    WebVideoPickImpl? webVideoPickImpl,    Key? key,  }) {    return EditorToolbar(      key: key,      toolBarHeight: toolbarIconSize * 2,      children: [        FlowyHistoryButton(          icon: Icons.undo_outlined,          iconSize: toolbarIconSize,          controller: controller,          undo: true,          tooltipText: LocaleKeys.toolbar_undo.tr(),        ),        FlowyHistoryButton(          icon: Icons.redo_outlined,          iconSize: toolbarIconSize,          controller: controller,          undo: false,          tooltipText: LocaleKeys.toolbar_redo.tr(),        ),        FlowyToggleStyleButton(          attribute: Attribute.bold,          normalIcon: 'editor/bold',          iconSize: toolbarIconSize,          controller: controller,          tooltipText: LocaleKeys.toolbar_bold.tr(),        ),        FlowyToggleStyleButton(          attribute: Attribute.italic,          normalIcon: 'editor/italic',          iconSize: toolbarIconSize,          controller: controller,          tooltipText: LocaleKeys.toolbar_italic.tr(),        ),        FlowyToggleStyleButton(          attribute: Attribute.underline,          normalIcon: 'editor/underline',          iconSize: toolbarIconSize,          controller: controller,          tooltipText: LocaleKeys.toolbar_underline.tr(),        ),        FlowyToggleStyleButton(          attribute: Attribute.strikeThrough,          normalIcon: 'editor/strikethrough',          iconSize: toolbarIconSize,          controller: controller,          tooltipText: LocaleKeys.toolbar_strike.tr(),        ),        FlowyColorButton(          icon: Icons.format_color_fill,          iconSize: toolbarIconSize,          controller: controller,          background: true,        ),        // FlowyImageButton(        //   iconSize: toolbarIconSize,        //   controller: controller,        //   onImagePickCallback: onImagePickCallback,        //   filePickImpl: filePickImpl,        //   webImagePickImpl: webImagePickImpl,        //   mediaPickSettingSelector: mediaPickSettingSelector,        // ),        FlowyHeaderStyleButton(          controller: controller,          iconSize: toolbarIconSize,        ),        FlowyToggleStyleButton(          attribute: Attribute.ol,          controller: controller,          normalIcon: 'editor/numbers',          iconSize: toolbarIconSize,          tooltipText: LocaleKeys.toolbar_numList.tr(),        ),        FlowyToggleStyleButton(          attribute: Attribute.ul,          controller: controller,          normalIcon: 'editor/bullet_list',          iconSize: toolbarIconSize,          tooltipText: LocaleKeys.toolbar_bulletList.tr(),        ),        FlowyCheckListButton(          attribute: Attribute.unchecked,          controller: controller,          iconSize: toolbarIconSize,          tooltipText: LocaleKeys.toolbar_checkList.tr(),        ),        FlowyToggleStyleButton(          attribute: Attribute.inlineCode,          controller: controller,          normalIcon: 'editor/inline_block',          iconSize: toolbarIconSize,          tooltipText: LocaleKeys.toolbar_inlineCode.tr(),        ),        FlowyToggleStyleButton(          attribute: Attribute.blockQuote,          controller: controller,          normalIcon: 'editor/quote',          iconSize: toolbarIconSize,          tooltipText: LocaleKeys.toolbar_quote.tr(),        ),        FlowyLinkStyleButton(          controller: controller,          iconSize: toolbarIconSize,        ),        FlowyEmojiStyleButton(          normalIcon: 'editor/insert_emoticon',          controller: controller,          tooltipText: "Emoji Picker",        ),      ],    );  }}class ToolbarButtonList extends StatefulWidget {  const ToolbarButtonList({required this.buttons, Key? key}) : super(key: key);  final List<Widget> buttons;  @override  ToolbarButtonListState createState() => ToolbarButtonListState();}class ToolbarButtonListState extends State<ToolbarButtonList>    with WidgetsBindingObserver {  final ScrollController _controller = ScrollController();  bool _showLeftArrow = false;  bool _showRightArrow = false;  @override  void initState() {    super.initState();    _controller.addListener(_handleScroll);    // Listening to the WidgetsBinding instance is necessary so that we can    // hide the arrows when the window gets a new size and thus the toolbar    // becomes scrollable/unscrollable.    WidgetsBinding.instance.addObserver(this);    // Workaround to allow the scroll controller attach to our ListView so that    // we can detect if overflow arrows need to be shown on init.    Timer.run(_handleScroll);  }  @override  Widget build(BuildContext context) {    return LayoutBuilder(      builder: (BuildContext context, BoxConstraints constraints) {        List<Widget> children = [];        double width =            (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;        final isFit = constraints.maxWidth > width;        if (!isFit) {          children.add(_buildLeftArrow());          width = width + 18;        }        children.add(_buildScrollableList(constraints, isFit));        if (!isFit) {          children.add(_buildRightArrow());          width = width + 18;        }        return SizedBox(          width: min(constraints.maxWidth, width),          child: Row(            children: children,          ),        );      },    );  }  @override  void didChangeMetrics() => _handleScroll();  @override  void dispose() {    _controller.dispose();    WidgetsBinding.instance.removeObserver(this);    super.dispose();  }  void _handleScroll() {    if (!mounted) return;    setState(() {      _showLeftArrow =          _controller.position.minScrollExtent != _controller.position.pixels;      _showRightArrow =          _controller.position.maxScrollExtent != _controller.position.pixels;    });  }  Widget _buildLeftArrow() {    return SizedBox(      width: 8,      child: Transform.translate(        // Move the icon a few pixels to center it        offset: const Offset(-5, 0),        child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null,      ),    );  }  // [[sliver: https://medium.com/flutter/slivers-demystified-6ff68ab0296f]]  Widget _buildScrollableList(BoxConstraints constraints, bool isFit) {    Widget child = Expanded(      child: CustomScrollView(        scrollDirection: Axis.horizontal,        controller: _controller,        physics: const ClampingScrollPhysics(),        slivers: [          SliverList(            delegate: SliverChildBuilderDelegate(              (BuildContext context, int index) {                return widget.buttons[index];              },              childCount: widget.buttons.length,              addAutomaticKeepAlives: false,            ),          )        ],      ),    );    if (!isFit) {      child = ScrollConfiguration(        // Remove the glowing effect, as we already have the arrow indicators        behavior: _NoGlowBehavior(),        // The CustomScrollView is necessary so that the children are not        // stretched to the height of the toolbar, https://bit.ly/3uC3bjI        child: child,      );    }    return child;  }  Widget _buildRightArrow() {    return SizedBox(      width: 8,      child: Transform.translate(        // Move the icon a few pixels to center it        offset: const Offset(-5, 0),        child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null,      ),    );  }}class _NoGlowBehavior extends ScrollBehavior {  @override  Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) {    return child;  }}
 |