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;
- }
- }
|