tool_bar.dart 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import 'dart:async';
  2. import 'dart:math';
  3. import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
  4. import 'package:easy_localization/easy_localization.dart';
  5. import 'package:flutter_quill/flutter_quill.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:styled_widget/styled_widget.dart';
  8. import 'check_button.dart';
  9. import 'color_picker.dart';
  10. import 'header_button.dart';
  11. import 'history_button.dart';
  12. import 'link_button.dart';
  13. import 'toggle_button.dart';
  14. import 'toolbar_icon_button.dart';
  15. import 'package:app_flowy/generated/locale_keys.g.dart';
  16. class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
  17. final List<Widget> children;
  18. final double toolBarHeight;
  19. final Color? color;
  20. const EditorToolbar({
  21. required this.children,
  22. this.toolBarHeight = 46,
  23. this.color,
  24. Key? key,
  25. }) : super(key: key);
  26. @override
  27. Widget build(BuildContext context) {
  28. return Container(
  29. color: Theme.of(context).canvasColor,
  30. constraints: BoxConstraints.tightFor(height: preferredSize.height),
  31. child: ToolbarButtonList(buttons: children).padding(horizontal: 4, vertical: 4),
  32. );
  33. }
  34. @override
  35. Size get preferredSize => Size.fromHeight(toolBarHeight);
  36. factory EditorToolbar.basic({
  37. required QuillController controller,
  38. double toolbarIconSize = defaultIconSize,
  39. OnImagePickCallback? onImagePickCallback,
  40. OnVideoPickCallback? onVideoPickCallback,
  41. MediaPickSettingSelector? mediaPickSettingSelector,
  42. FilePickImpl? filePickImpl,
  43. WebImagePickImpl? webImagePickImpl,
  44. WebVideoPickImpl? webVideoPickImpl,
  45. Key? key,
  46. }) {
  47. return EditorToolbar(
  48. key: key,
  49. toolBarHeight: toolbarIconSize * 2,
  50. children: [
  51. FlowyHistoryButton(
  52. icon: Icons.undo_outlined,
  53. iconSize: toolbarIconSize,
  54. controller: controller,
  55. undo: true,
  56. tooltipText: LocaleKeys.toolbar_undo.tr(),
  57. ),
  58. FlowyHistoryButton(
  59. icon: Icons.redo_outlined,
  60. iconSize: toolbarIconSize,
  61. controller: controller,
  62. undo: false,
  63. tooltipText: LocaleKeys.toolbar_redo.tr(),
  64. ),
  65. FlowyToggleStyleButton(
  66. attribute: Attribute.bold,
  67. normalIcon: 'editor/bold',
  68. iconSize: toolbarIconSize,
  69. controller: controller,
  70. tooltipText: LocaleKeys.toolbar_bold.tr(),
  71. ),
  72. FlowyToggleStyleButton(
  73. attribute: Attribute.italic,
  74. normalIcon: 'editor/italic',
  75. iconSize: toolbarIconSize,
  76. controller: controller,
  77. tooltipText: LocaleKeys.toolbar_italic.tr(),
  78. ),
  79. FlowyToggleStyleButton(
  80. attribute: Attribute.underline,
  81. normalIcon: 'editor/underline',
  82. iconSize: toolbarIconSize,
  83. controller: controller,
  84. tooltipText: LocaleKeys.toolbar_underline.tr(),
  85. ),
  86. FlowyToggleStyleButton(
  87. attribute: Attribute.strikeThrough,
  88. normalIcon: 'editor/strikethrough',
  89. iconSize: toolbarIconSize,
  90. controller: controller,
  91. tooltipText: LocaleKeys.toolbar_strike.tr(),
  92. ),
  93. FlowyColorButton(
  94. icon: Icons.format_color_fill,
  95. iconSize: toolbarIconSize,
  96. controller: controller,
  97. background: true,
  98. ),
  99. // FlowyImageButton(
  100. // iconSize: toolbarIconSize,
  101. // controller: controller,
  102. // onImagePickCallback: onImagePickCallback,
  103. // filePickImpl: filePickImpl,
  104. // webImagePickImpl: webImagePickImpl,
  105. // mediaPickSettingSelector: mediaPickSettingSelector,
  106. // ),
  107. FlowyHeaderStyleButton(
  108. controller: controller,
  109. iconSize: toolbarIconSize,
  110. ),
  111. FlowyToggleStyleButton(
  112. attribute: Attribute.ol,
  113. controller: controller,
  114. normalIcon: 'editor/numbers',
  115. iconSize: toolbarIconSize,
  116. tooltipText: LocaleKeys.toolbar_numList.tr(),
  117. ),
  118. FlowyToggleStyleButton(
  119. attribute: Attribute.ul,
  120. controller: controller,
  121. normalIcon: 'editor/bullet_list',
  122. iconSize: toolbarIconSize,
  123. tooltipText: LocaleKeys.toolbar_bulletList.tr(),
  124. ),
  125. FlowyCheckListButton(
  126. attribute: Attribute.unchecked,
  127. controller: controller,
  128. iconSize: toolbarIconSize,
  129. tooltipText: LocaleKeys.toolbar_checkList.tr(),
  130. ),
  131. FlowyToggleStyleButton(
  132. attribute: Attribute.inlineCode,
  133. controller: controller,
  134. normalIcon: 'editor/inline_block',
  135. iconSize: toolbarIconSize,
  136. tooltipText: LocaleKeys.toolbar_inlineCode.tr(),
  137. ),
  138. FlowyToggleStyleButton(
  139. attribute: Attribute.blockQuote,
  140. controller: controller,
  141. normalIcon: 'editor/quote',
  142. iconSize: toolbarIconSize,
  143. tooltipText: LocaleKeys.toolbar_quote.tr(),
  144. ),
  145. FlowyLinkStyleButton(
  146. controller: controller,
  147. iconSize: toolbarIconSize,
  148. ),
  149. FlowyEmojiStyleButton(
  150. normalIcon: 'editor/insert_emoticon',
  151. controller: controller,
  152. tooltipText: "Emoji Picker",
  153. ),
  154. ],
  155. );
  156. }
  157. }
  158. class ToolbarButtonList extends StatefulWidget {
  159. const ToolbarButtonList({required this.buttons, Key? key}) : super(key: key);
  160. final List<Widget> buttons;
  161. @override
  162. _ToolbarButtonListState createState() => _ToolbarButtonListState();
  163. }
  164. class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindingObserver {
  165. final ScrollController _controller = ScrollController();
  166. bool _showLeftArrow = false;
  167. bool _showRightArrow = false;
  168. @override
  169. void initState() {
  170. super.initState();
  171. _controller.addListener(_handleScroll);
  172. // Listening to the WidgetsBinding instance is necessary so that we can
  173. // hide the arrows when the window gets a new size and thus the toolbar
  174. // becomes scrollable/unscrollable.
  175. WidgetsBinding.instance.addObserver(this);
  176. // Workaround to allow the scroll controller attach to our ListView so that
  177. // we can detect if overflow arrows need to be shown on init.
  178. Timer.run(_handleScroll);
  179. }
  180. @override
  181. Widget build(BuildContext context) {
  182. return LayoutBuilder(
  183. builder: (BuildContext context, BoxConstraints constraints) {
  184. List<Widget> children = [];
  185. double width = (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
  186. final isFit = constraints.maxWidth > width;
  187. if (!isFit) {
  188. children.add(_buildLeftArrow());
  189. width = width + 18;
  190. }
  191. children.add(_buildScrollableList(constraints, isFit));
  192. if (!isFit) {
  193. children.add(_buildRightArrow());
  194. width = width + 18;
  195. }
  196. return SizedBox(
  197. width: min(constraints.maxWidth, width),
  198. child: Row(
  199. children: children,
  200. ),
  201. );
  202. },
  203. );
  204. }
  205. @override
  206. void didChangeMetrics() => _handleScroll();
  207. @override
  208. void dispose() {
  209. _controller.dispose();
  210. WidgetsBinding.instance.removeObserver(this);
  211. super.dispose();
  212. }
  213. void _handleScroll() {
  214. if (!mounted) return;
  215. setState(() {
  216. _showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels;
  217. _showRightArrow = _controller.position.maxScrollExtent != _controller.position.pixels;
  218. });
  219. }
  220. Widget _buildLeftArrow() {
  221. return SizedBox(
  222. width: 8,
  223. child: Transform.translate(
  224. // Move the icon a few pixels to center it
  225. offset: const Offset(-5, 0),
  226. child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null,
  227. ),
  228. );
  229. }
  230. // [[sliver: https://medium.com/flutter/slivers-demystified-6ff68ab0296f]]
  231. Widget _buildScrollableList(BoxConstraints constraints, bool isFit) {
  232. Widget child = Expanded(
  233. child: CustomScrollView(
  234. scrollDirection: Axis.horizontal,
  235. controller: _controller,
  236. physics: const ClampingScrollPhysics(),
  237. slivers: [
  238. SliverList(
  239. delegate: SliverChildBuilderDelegate(
  240. (BuildContext context, int index) {
  241. return widget.buttons[index];
  242. },
  243. childCount: widget.buttons.length,
  244. addAutomaticKeepAlives: false,
  245. ),
  246. )
  247. ],
  248. ),
  249. );
  250. if (!isFit) {
  251. child = ScrollConfiguration(
  252. // Remove the glowing effect, as we already have the arrow indicators
  253. behavior: _NoGlowBehavior(),
  254. // The CustomScrollView is necessary so that the children are not
  255. // stretched to the height of the toolbar, https://bit.ly/3uC3bjI
  256. child: child,
  257. );
  258. }
  259. return child;
  260. }
  261. Widget _buildRightArrow() {
  262. return SizedBox(
  263. width: 8,
  264. child: Transform.translate(
  265. // Move the icon a few pixels to center it
  266. offset: const Offset(-5, 0),
  267. child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null,
  268. ),
  269. );
  270. }
  271. }
  272. class _NoGlowBehavior extends ScrollBehavior {
  273. @override
  274. Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) {
  275. return child;
  276. }
  277. }