zefyr

toolbar.dart 11KB

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