zefyr

toolbar.dart 13KB

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