zefyr

toolbar.dart 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file
  2. // for details. All rights reserved. Use of this source code is governed by a
  3. // BSD-style license that can be found in the LICENSE file.
  4. import 'dart:async';
  5. import 'dart:ui' as ui;
  6. import 'package:flutter/material.dart';
  7. import 'package:notus/notus.dart';
  8. import 'iconfont.dart';
  9. import 'buttons.dart';
  10. import 'scope.dart';
  11. import 'theme.dart';
  12. /// List of all button actions supported by [ZefyrToolbar] buttons.
  13. enum ZefyrToolbarAction {
  14. bold,
  15. italic,
  16. underline,
  17. deleteline,
  18. link,
  19. unlink,
  20. emoji,
  21. clipboardCopy,
  22. openInBrowser,
  23. heading,
  24. text,
  25. headingLevel1,
  26. headingLevel2,
  27. headingLevel3,
  28. headingLevel4,
  29. headingLevel5,
  30. headingLevel6,
  31. bulletList,
  32. numberList,
  33. code,
  34. quote,
  35. horizontalRule,
  36. image,
  37. cameraImage,
  38. galleryImage,
  39. hideKeyboard,
  40. close,
  41. confirm,
  42. }
  43. final kZefyrToolbarAttributeActions = <ZefyrToolbarAction, NotusAttributeKey>{
  44. ZefyrToolbarAction.bold: NotusAttribute.bold,
  45. ZefyrToolbarAction.italic: NotusAttribute.italic,
  46. ZefyrToolbarAction.link: NotusAttribute.link,
  47. ZefyrToolbarAction.heading: NotusAttribute.heading,
  48. ZefyrToolbarAction.headingLevel1: NotusAttribute.heading.level1,
  49. ZefyrToolbarAction.headingLevel2: NotusAttribute.heading.level2,
  50. ZefyrToolbarAction.headingLevel3: NotusAttribute.heading.level3,
  51. ZefyrToolbarAction.bulletList: NotusAttribute.block.bulletList,
  52. ZefyrToolbarAction.numberList: NotusAttribute.block.numberList,
  53. ZefyrToolbarAction.code: NotusAttribute.block.code,
  54. ZefyrToolbarAction.quote: NotusAttribute.block.quote,
  55. ZefyrToolbarAction.horizontalRule: NotusAttribute.embed.horizontalRule,
  56. };
  57. const kDefaultButtonIcons = {
  58. ZefyrToolbarAction.bold: IconFont.note_editor_text_bold,
  59. ZefyrToolbarAction.italic: IconFont.note_editor_text_in,
  60. ZefyrToolbarAction.underline: IconFont.note_editor_text_underline,
  61. ZefyrToolbarAction.deleteline: IconFont.note_editor_text_deleteline,
  62. ZefyrToolbarAction.quote: IconFont.note_editor_text_quote,
  63. ZefyrToolbarAction.numberList: IconFont.note_editor_text_ol,
  64. ZefyrToolbarAction.bulletList: IconFont.note_editor_text_ul,
  65. ZefyrToolbarAction.link: IconFont.note_editor_link,
  66. ZefyrToolbarAction.unlink: Icons.link_off,
  67. ZefyrToolbarAction.emoji: IconFont.im_dialogebar_sendemoji,
  68. ZefyrToolbarAction.clipboardCopy: Icons.content_copy,
  69. ZefyrToolbarAction.openInBrowser: Icons.open_in_new,
  70. ZefyrToolbarAction.heading: IconFont.note_editor_text,
  71. ZefyrToolbarAction.headingLevel1: IconFont.note_editor_text_h,
  72. ZefyrToolbarAction.headingLevel2: IconFont.note_editor_text_h1,
  73. ZefyrToolbarAction.headingLevel3: IconFont.note_editor_text_h2,
  74. ZefyrToolbarAction.headingLevel4: IconFont.note_editor_text_h3,
  75. ZefyrToolbarAction.headingLevel5: IconFont.note_editor_text_h4,
  76. ZefyrToolbarAction.headingLevel6: IconFont.note_editor_text_h5,
  77. ZefyrToolbarAction.text: IconFont.note_editor_text,
  78. ZefyrToolbarAction.code: IconFont.note_editor_text_code,
  79. ZefyrToolbarAction.horizontalRule: IconFont.note_editor_text_hr,
  80. ZefyrToolbarAction.image: IconFont.note_editor_image,
  81. ZefyrToolbarAction.cameraImage: IconFont.im_dialoge_more_takephoto,
  82. ZefyrToolbarAction.galleryImage: IconFont.im_dialoge_more_album,
  83. ZefyrToolbarAction.hideKeyboard: IconFont.note_editor_keyboard,
  84. ZefyrToolbarAction.close: Icons.close,
  85. ZefyrToolbarAction.confirm: Icons.check,
  86. };
  87. const kSpecialIconSizes = {
  88. ZefyrToolbarAction.unlink: 20.0,
  89. ZefyrToolbarAction.clipboardCopy: 20.0,
  90. ZefyrToolbarAction.openInBrowser: 20.0,
  91. ZefyrToolbarAction.close: 20.0,
  92. ZefyrToolbarAction.confirm: 20.0,
  93. };
  94. const kDefaultButtonTexts = {};
  95. /// Allows customizing appearance of [ZefyrToolbar].
  96. abstract class ZefyrToolbarDelegate {
  97. /// Builds toolbar button for specified [action].
  98. ///
  99. /// Returned widget is usually an instance of [ZefyrButton].
  100. Widget buildButton(BuildContext context, ZefyrToolbarAction action,
  101. {VoidCallback onPressed});
  102. }
  103. /// Scaffold for [ZefyrToolbar].
  104. class ZefyrToolbarScaffold extends StatelessWidget {
  105. const ZefyrToolbarScaffold({
  106. Key key,
  107. @required this.body,
  108. this.trailing,
  109. this.autoImplyTrailing = true,
  110. }) : super(key: key);
  111. final Widget body;
  112. final Widget trailing;
  113. final bool autoImplyTrailing;
  114. @override
  115. Widget build(BuildContext context) {
  116. final theme = ZefyrTheme.of(context).toolbarTheme;
  117. final toolbar = ZefyrToolbar.of(context);
  118. final constraints =
  119. BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight);
  120. final children = <Widget>[
  121. if (trailing != null)
  122. trailing
  123. else if (autoImplyTrailing)
  124. toolbar.buildButton(context, ZefyrToolbarAction.close)
  125. ];
  126. children.add(Expanded(child: body));
  127. return Container(
  128. constraints: constraints,
  129. decoration: BoxDecoration(
  130. border: Border(
  131. top: BorderSide(color: theme.dividerColor, width: 1)
  132. ),
  133. ),
  134. child: Material(color: theme.color, child: Row(children: children)),
  135. );
  136. }
  137. }
  138. /// Toolbar for [ZefyrEditor].
  139. class ZefyrToolbar extends StatefulWidget implements PreferredSizeWidget {
  140. static const kToolbarHeight = 50.0;
  141. const ZefyrToolbar({
  142. Key key,
  143. @required this.editor,
  144. this.autoHide = true,
  145. this.delegate,
  146. }) : super(key: key);
  147. final ZefyrToolbarDelegate delegate;
  148. final ZefyrScope editor;
  149. /// Whether to automatically hide this toolbar when editor loses focus.
  150. final bool autoHide;
  151. static ZefyrToolbarState of(BuildContext context) {
  152. final _ZefyrToolbarScope scope =
  153. context.dependOnInheritedWidgetOfExactType<_ZefyrToolbarScope>();
  154. return scope?.toolbar;
  155. }
  156. @override
  157. ZefyrToolbarState createState() => ZefyrToolbarState();
  158. @override
  159. ui.Size get preferredSize => Size.fromHeight(ZefyrToolbar.kToolbarHeight);
  160. }
  161. class _ZefyrToolbarScope extends InheritedWidget {
  162. _ZefyrToolbarScope({Key key, @required Widget child, @required this.toolbar})
  163. : super(key: key, child: child);
  164. final ZefyrToolbarState toolbar;
  165. @override
  166. bool updateShouldNotify(_ZefyrToolbarScope oldWidget) {
  167. return toolbar != oldWidget.toolbar;
  168. }
  169. }
  170. class ZefyrToolbarState extends State<ZefyrToolbar>
  171. with SingleTickerProviderStateMixin {
  172. final Key _toolbarKey = UniqueKey();
  173. final Key _overlayKey = UniqueKey();
  174. ZefyrToolbarDelegate _delegate;
  175. AnimationController _overlayAnimation;
  176. WidgetBuilder _overlayBuilder;
  177. Completer<void> _overlayCompleter;
  178. TextSelection _selection;
  179. void markNeedsRebuild() {
  180. setState(() {
  181. if (_selection != editor.selection) {
  182. _selection = editor.selection;
  183. // closeOverlay();
  184. }
  185. });
  186. }
  187. Widget buildButton(BuildContext context, ZefyrToolbarAction action,
  188. {VoidCallback onPressed}) {
  189. return _delegate.buildButton(context, action, onPressed: onPressed);
  190. }
  191. Future<void> showOverlay(WidgetBuilder builder) async {
  192. if (hasOverlay) {
  193. closeOverlay();
  194. }
  195. final completer = Completer<void>();
  196. setState(() {
  197. _overlayBuilder = builder;
  198. _overlayCompleter = completer;
  199. _overlayAnimation.forward();
  200. });
  201. return completer.future;
  202. }
  203. void closeOverlay() {
  204. if (!hasOverlay) return;
  205. _overlayAnimation.reverse().whenComplete(() {
  206. setState(() {
  207. _overlayBuilder = null;
  208. _overlayCompleter?.complete();
  209. _overlayCompleter = null;
  210. });
  211. });
  212. }
  213. bool get hasOverlay => _overlayBuilder != null;
  214. ZefyrScope get editor => widget.editor;
  215. @override
  216. void initState() {
  217. super.initState();
  218. _delegate = widget.delegate ?? _DefaultZefyrToolbarDelegate();
  219. _overlayAnimation =
  220. AnimationController(vsync: this, duration: Duration(milliseconds: 100));
  221. _selection = editor.selection;
  222. }
  223. @override
  224. void didUpdateWidget(ZefyrToolbar oldWidget) {
  225. super.didUpdateWidget(oldWidget);
  226. if (widget.delegate != oldWidget.delegate) {
  227. _delegate = widget.delegate ?? _DefaultZefyrToolbarDelegate();
  228. }
  229. }
  230. @override
  231. void dispose() {
  232. _overlayAnimation.dispose();
  233. super.dispose();
  234. }
  235. @override
  236. Widget build(BuildContext context) {
  237. final layers = <Widget>[];
  238. // Must set unique key for the toolbar to prevent it from reconstructing
  239. // new state each time we toggle overlay.
  240. final toolbar = ZefyrToolbarScaffold(
  241. key: _toolbarKey,
  242. body: ZefyrButtonList(buttons: _buildButtons(context)),
  243. trailing: buildButton(context, ZefyrToolbarAction.hideKeyboard),
  244. );
  245. layers.add(toolbar);
  246. if (hasOverlay) {
  247. Widget widget = Builder(builder: _overlayBuilder);
  248. assert(widget != null);
  249. final overlay = FadeTransition(
  250. key: _overlayKey,
  251. opacity: _overlayAnimation,
  252. child: widget,
  253. );
  254. layers.add(overlay);
  255. }
  256. return _ZefyrToolbarScope(
  257. toolbar: this,
  258. child: Column(
  259. children: layers
  260. ),
  261. );
  262. }
  263. List<Widget> _buildButtons(BuildContext context) {
  264. final buttons = <Widget>[
  265. HeadingButton(),
  266. buildButton(context, ZefyrToolbarAction.emoji),
  267. if (editor.imageDelegate != null) ImageButton(),
  268. LinkButton(),
  269. // buildButton(context, ZefyrToolbarAction.bulletList),
  270. // buildButton(context, ZefyrToolbarAction.numberList),
  271. // buildButton(context, ZefyrToolbarAction.quote),
  272. // buildButton(context, ZefyrToolbarAction.code),
  273. // buildButton(context, ZefyrToolbarAction.horizontalRule),
  274. ];
  275. return buttons;
  276. }
  277. }
  278. /// Scrollable list of toolbar buttons.
  279. class ZefyrButtonList extends StatefulWidget {
  280. const ZefyrButtonList({Key key, @required this.buttons}) : super(key: key);
  281. final List<Widget> buttons;
  282. @override
  283. _ZefyrButtonListState createState() => _ZefyrButtonListState();
  284. }
  285. class _ZefyrButtonListState extends State<ZefyrButtonList> {
  286. final ScrollController _controller = ScrollController();
  287. bool _showLeftArrow = false;
  288. bool _showRightArrow = false;
  289. @override
  290. void initState() {
  291. super.initState();
  292. _controller.addListener(_handleScroll);
  293. // Workaround to allow scroll controller attach to our ListView so that
  294. // we can detect if overflow arrows need to be shown on init.
  295. // TODO: find a better way to detect overflow
  296. Timer.run(_handleScroll);
  297. }
  298. @override
  299. Widget build(BuildContext context) {
  300. final theme = ZefyrTheme.of(context).toolbarTheme;
  301. final color = theme.iconColor;
  302. final list = ListView(
  303. scrollDirection: Axis.horizontal,
  304. controller: _controller,
  305. children: widget.buttons,
  306. physics: ClampingScrollPhysics(),
  307. );
  308. final leftArrow = _showLeftArrow
  309. ? Icon(Icons.arrow_left, size: 18.0, color: color)
  310. : null;
  311. final rightArrow = _showRightArrow
  312. ? Icon(Icons.arrow_right, size: 18.0, color: color)
  313. : null;
  314. return ClipRect(child: list);
  315. // Row(
  316. // children: <Widget>[
  317. // SizedBox(
  318. // width: 12.0,
  319. // height: ZefyrToolbar.kToolbarHeight,
  320. // child: Container(child: leftArrow, color: theme.color),
  321. // ),
  322. // Expanded(child: ClipRect(child: list)),
  323. // SizedBox(
  324. // width: 12.0,
  325. // height: ZefyrToolbar.kToolbarHeight,
  326. // child: Container(child: rightArrow, color: theme.color),
  327. // ),
  328. // ],
  329. // );
  330. }
  331. void _handleScroll() {
  332. setState(() {
  333. _showLeftArrow =
  334. _controller.position.minScrollExtent != _controller.position.pixels;
  335. _showRightArrow =
  336. _controller.position.maxScrollExtent != _controller.position.pixels;
  337. });
  338. }
  339. }
  340. class _DefaultZefyrToolbarDelegate implements ZefyrToolbarDelegate {
  341. @override
  342. Widget buildButton(BuildContext context, ZefyrToolbarAction action,
  343. {VoidCallback onPressed}) {
  344. final theme = Theme.of(context);
  345. if (kDefaultButtonIcons.containsKey(action)) {
  346. final icon = kDefaultButtonIcons[action];
  347. final size = kSpecialIconSizes[action];
  348. return ZefyrButton.icon(
  349. action: action,
  350. icon: icon,
  351. iconSize: size,
  352. onPressed: onPressed,
  353. );
  354. } else {
  355. final text = kDefaultButtonTexts[action];
  356. assert(text != null);
  357. final style = theme.textTheme.caption.copyWith(fontSize: 14.0);
  358. return ZefyrButton.text(
  359. action: action,
  360. text: text,
  361. style: style,
  362. onPressed: onPressed,
  363. );
  364. }
  365. }
  366. }