flowy_toolbar.dart 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. import 'package:flutter/foundation.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_colorpicker/flutter_colorpicker.dart';
  4. import '../model/document/style.dart';
  5. import '../model/document/attribute.dart';
  6. import '../model/document/node/embed.dart';
  7. import '../util/color.dart';
  8. import '../service/controller.dart';
  9. import 'toolbar.dart';
  10. /* -------------------------------- Constant -------------------------------- */
  11. const double kToolbarIconHeight = 18.0;
  12. const double kToolbarButtonHeight = kToolbarIconHeight * 1.77;
  13. class FlowyToolbar extends EditorToolbar {
  14. const FlowyToolbar({
  15. required List<Widget> children,
  16. Key? key,
  17. }) : super(
  18. children: children,
  19. customButtonHeight: kToolbarButtonHeight,
  20. key: key,
  21. );
  22. factory FlowyToolbar.basic({
  23. required EditorController controller,
  24. double? toolbarIconSize,
  25. bool showHistory = true,
  26. bool showBold = true,
  27. bool showItalic = true,
  28. bool showUnderline = true,
  29. bool showStrikethrough = true,
  30. bool showColor = true,
  31. bool showBackgroundColor = true,
  32. bool showClearFormat = true,
  33. bool showHeader = true,
  34. bool showBulletList = true,
  35. bool showOrderedList = true,
  36. bool showCheckList = true,
  37. bool showCodeblock = true,
  38. bool showQuoteblock = true,
  39. bool showIndent = true,
  40. bool showLink = true,
  41. bool showHorizontalLine = true,
  42. OnImageSelectCallback? onImageSelectCallback,
  43. Key? key,
  44. }) {
  45. return FlowyToolbar(children: [
  46. Visibility(
  47. visible: showHistory,
  48. child: HistoryButton(
  49. icon: Icons.undo_outlined,
  50. controller: controller,
  51. isUndo: true,
  52. ),
  53. ),
  54. Visibility(
  55. visible: showHistory,
  56. child: HistoryButton(
  57. icon: Icons.redo_outlined,
  58. controller: controller,
  59. isUndo: false,
  60. ),
  61. ),
  62. const SizedBox(width: 0.6),
  63. Visibility(
  64. visible: showBold,
  65. child: ToggleStyleButton(
  66. attribute: Attribute.bold,
  67. icon: Icons.format_bold,
  68. controller: controller,
  69. ),
  70. ),
  71. const SizedBox(width: 0.6),
  72. Visibility(
  73. visible: showItalic,
  74. child: ToggleStyleButton(
  75. attribute: Attribute.italic,
  76. icon: Icons.format_italic,
  77. controller: controller,
  78. ),
  79. ),
  80. const SizedBox(width: 0.6),
  81. Visibility(
  82. visible: showUnderline,
  83. child: ToggleStyleButton(
  84. attribute: Attribute.underline,
  85. icon: Icons.format_underline,
  86. controller: controller,
  87. ),
  88. ),
  89. const SizedBox(width: 0.6),
  90. Visibility(
  91. visible: showStrikethrough,
  92. child: ToggleStyleButton(
  93. attribute: Attribute.strikeThrough,
  94. icon: Icons.format_strikethrough,
  95. controller: controller,
  96. ),
  97. ),
  98. const SizedBox(width: 0.6),
  99. Visibility(
  100. visible: showColor,
  101. child: ColorButton(
  102. icon: Icons.color_lens,
  103. controller: controller,
  104. isBackground: false,
  105. ),
  106. ),
  107. const SizedBox(width: 0.6),
  108. Visibility(
  109. visible: showColor,
  110. child: ColorButton(
  111. icon: Icons.format_color_fill,
  112. controller: controller,
  113. isBackground: true,
  114. ),
  115. ),
  116. const SizedBox(width: 0.6),
  117. Visibility(
  118. visible: showColor,
  119. child: ClearFormatButton(
  120. icon: Icons.format_clear,
  121. controller: controller,
  122. ),
  123. ),
  124. const SizedBox(width: 0.6),
  125. Visibility(
  126. visible: showOrderedList,
  127. child: ToggleStyleButton(
  128. attribute: Attribute.ordered,
  129. icon: Icons.format_list_numbered,
  130. controller: controller,
  131. ),
  132. ),
  133. Visibility(
  134. visible: showBulletList,
  135. child: ToggleStyleButton(
  136. attribute: Attribute.bullet,
  137. icon: Icons.format_list_bulleted,
  138. controller: controller,
  139. ),
  140. ),
  141. Visibility(
  142. visible: showCheckList,
  143. child: ToggleStyleButton(
  144. attribute: Attribute.unchecked,
  145. icon: Icons.check_box,
  146. controller: controller,
  147. ),
  148. ),
  149. Visibility(
  150. visible: showCodeblock,
  151. child: ToggleStyleButton(
  152. attribute: Attribute.codeBlock,
  153. icon: Icons.code,
  154. controller: controller,
  155. ),
  156. ),
  157. Visibility(
  158. visible: showHeader,
  159. child: VerticalDivider(
  160. indent: 12, endIndent: 12, color: Colors.grey.shade400),
  161. ),
  162. Visibility(
  163. visible: showHeader,
  164. child: HeaderStyleButton(controller: controller),
  165. ),
  166. VerticalDivider(indent: 12, endIndent: 12, color: Colors.grey.shade400),
  167. Visibility(
  168. visible: !showOrderedList &&
  169. !showBulletList &&
  170. !showCheckList &&
  171. !showCodeblock,
  172. child: VerticalDivider(
  173. indent: 12, endIndent: 12, color: Colors.grey.shade400),
  174. ),
  175. Visibility(
  176. visible: showQuoteblock,
  177. child: ToggleStyleButton(
  178. attribute: Attribute.quoteBlock,
  179. controller: controller,
  180. icon: Icons.format_quote,
  181. ),
  182. ),
  183. Visibility(
  184. visible: showIndent,
  185. child: IndentButton(
  186. icon: Icons.format_indent_increase,
  187. controller: controller,
  188. isIncrease: true,
  189. ),
  190. ),
  191. Visibility(
  192. visible: showIndent,
  193. child: IndentButton(
  194. icon: Icons.format_indent_decrease,
  195. controller: controller,
  196. isIncrease: false,
  197. ),
  198. ),
  199. Visibility(
  200. visible: showQuoteblock,
  201. child: VerticalDivider(
  202. indent: 12, endIndent: 12, color: Colors.grey.shade400),
  203. ),
  204. Visibility(
  205. visible: showLink,
  206. child: LinkStyleButton(
  207. controller: controller,
  208. icon: Icons.link,
  209. ),
  210. ),
  211. ]);
  212. }
  213. }
  214. /* ---------------------------------- Util ---------------------------------- */
  215. class ToolbarIconButton extends StatelessWidget {
  216. const ToolbarIconButton({
  217. this.onPressed,
  218. this.icon,
  219. this.size = 40,
  220. this.fillColor,
  221. this.hoverElevation = 1,
  222. this.highlightElevation = 1,
  223. Key? key,
  224. }) : super(key: key);
  225. final VoidCallback? onPressed;
  226. final Widget? icon;
  227. final double size;
  228. final Color? fillColor;
  229. final double hoverElevation;
  230. final double highlightElevation;
  231. @override
  232. Widget build(BuildContext context) {
  233. return ConstrainedBox(
  234. constraints: BoxConstraints.tightFor(width: size, height: size),
  235. child: RawMaterialButton(
  236. visualDensity: VisualDensity.compact,
  237. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
  238. fillColor: fillColor,
  239. elevation: 0,
  240. hoverElevation: hoverElevation,
  241. highlightElevation: hoverElevation,
  242. onPressed: onPressed,
  243. child: icon,
  244. ),
  245. );
  246. }
  247. }
  248. /* ------------------------------- Button Impl ------------------------------ */
  249. // History (Redo, Undo)
  250. class HistoryButton extends StatefulWidget {
  251. const HistoryButton({
  252. required this.icon,
  253. required this.isUndo,
  254. required this.controller,
  255. Key? key,
  256. }) : super(key: key);
  257. final IconData icon;
  258. final bool isUndo;
  259. final EditorController controller;
  260. @override
  261. _HistoryButtonState createState() => _HistoryButtonState();
  262. }
  263. class _HistoryButtonState extends State<HistoryButton> {
  264. Color? _iconColor;
  265. late ThemeData theme;
  266. @override
  267. Widget build(BuildContext context) {
  268. theme = Theme.of(context);
  269. _setIconColor();
  270. final fillColor = theme.canvasColor;
  271. widget.controller.changes.listen((event) async {
  272. _setIconColor();
  273. });
  274. return ToolbarIconButton(
  275. size: kToolbarButtonHeight,
  276. fillColor: fillColor,
  277. hoverElevation: 0,
  278. highlightElevation: 0,
  279. icon: Icon(widget.icon, size: kToolbarIconHeight, color: _iconColor),
  280. onPressed: _applyHistory,
  281. );
  282. }
  283. void _applyHistory() {
  284. if (widget.isUndo) {
  285. if (widget.controller.hasUndo) {
  286. widget.controller.undo();
  287. }
  288. } else {
  289. if (widget.controller.hasRedo) {
  290. widget.controller.redo();
  291. }
  292. }
  293. _setIconColor();
  294. }
  295. void _setIconColor() {
  296. if (!mounted) {
  297. return;
  298. }
  299. if (widget.isUndo) {
  300. setState(() {
  301. _iconColor = widget.controller.hasUndo
  302. ? theme.iconTheme.color
  303. : theme.disabledColor;
  304. });
  305. } else {
  306. setState(() {
  307. _iconColor = widget.controller.hasRedo
  308. ? theme.iconTheme.color
  309. : theme.disabledColor;
  310. });
  311. }
  312. }
  313. }
  314. // Toggle Style (Bold, Italic, Underline, etc..)
  315. typedef ToggleStyleButtonBuilder = Widget Function(
  316. BuildContext context,
  317. Attribute attribute,
  318. IconData icon,
  319. Color? fillColor,
  320. bool? isToggled,
  321. VoidCallback? onPressed,
  322. );
  323. Widget defaultToggleStyleButtonBuilder(
  324. BuildContext context,
  325. Attribute attribute,
  326. IconData icon,
  327. Color? fillColor,
  328. bool? isToggled,
  329. VoidCallback? onPressed,
  330. ) {
  331. final theme = Theme.of(context);
  332. final isEnabled = onPressed != null;
  333. final iconColor = isEnabled
  334. ? isToggled == true
  335. ? theme.primaryIconTheme.color
  336. : theme.iconTheme.color
  337. : theme.disabledColor;
  338. final theFillColor = isToggled == true
  339. ? theme.toggleableActiveColor
  340. : fillColor ?? theme.canvasColor;
  341. return ToolbarIconButton(
  342. onPressed: onPressed,
  343. icon: Icon(icon, size: kToolbarIconHeight, color: iconColor),
  344. size: kToolbarButtonHeight,
  345. fillColor: theFillColor,
  346. hoverElevation: 0,
  347. highlightElevation: 0,
  348. );
  349. }
  350. class ToggleStyleButton extends StatefulWidget {
  351. const ToggleStyleButton({
  352. required this.attribute,
  353. required this.icon,
  354. required this.controller,
  355. this.fillColor,
  356. this.childBuilder = defaultToggleStyleButtonBuilder,
  357. Key? key,
  358. }) : super(key: key);
  359. final Attribute attribute;
  360. final IconData icon;
  361. final Color? fillColor;
  362. final EditorController controller;
  363. final ToggleStyleButtonBuilder childBuilder;
  364. @override
  365. _ToggleStyleButtonState createState() => _ToggleStyleButtonState();
  366. }
  367. class _ToggleStyleButtonState extends State<ToggleStyleButton> {
  368. bool? _isToggled;
  369. Style get _selectionStyle => widget.controller.getSelectionStyle();
  370. @override
  371. void didUpdateWidget(covariant ToggleStyleButton oldWidget) {
  372. super.didUpdateWidget(oldWidget);
  373. if (oldWidget.controller != widget.controller) {
  374. oldWidget.controller.removeListener(_didChangeEditingValue);
  375. widget.controller.addListener(_didChangeEditingValue);
  376. _isToggled = _checkIfAttrToggled(_selectionStyle.attributes);
  377. }
  378. }
  379. @override
  380. void initState() {
  381. super.initState();
  382. _isToggled = _checkIfAttrToggled(_selectionStyle.attributes);
  383. widget.controller.addListener(_didChangeEditingValue);
  384. }
  385. @override
  386. void dispose() {
  387. widget.controller.removeListener(_didChangeEditingValue);
  388. super.dispose();
  389. }
  390. @override
  391. Widget build(BuildContext context) {
  392. final isInCodeBlock =
  393. _selectionStyle.attributes.containsKey(Attribute.codeBlock.key);
  394. final isEnabled =
  395. !isInCodeBlock || widget.attribute.key == Attribute.codeBlock.key;
  396. return widget.childBuilder(
  397. context,
  398. widget.attribute,
  399. widget.icon,
  400. widget.fillColor,
  401. _isToggled,
  402. isEnabled ? _toggleAttribute : null,
  403. );
  404. }
  405. bool _checkIfAttrToggled(Map<String, Attribute> attrs) {
  406. if (widget.attribute.key == Attribute.list.key) {
  407. final attribute = attrs[widget.attribute.key];
  408. if (attribute == null) {
  409. return false;
  410. }
  411. return attribute.value == widget.attribute.value;
  412. }
  413. return attrs.containsKey(widget.attribute.key);
  414. }
  415. void _toggleAttribute() {
  416. widget.controller.formatSelection(
  417. _isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute,
  418. );
  419. }
  420. void _didChangeEditingValue() {
  421. setState(() {
  422. _isToggled =
  423. _checkIfAttrToggled(widget.controller.getSelectionStyle().attributes);
  424. });
  425. }
  426. }
  427. // Header Style
  428. class HeaderStyleButton extends StatefulWidget {
  429. const HeaderStyleButton({
  430. required this.controller,
  431. Key? key,
  432. }) : super(key: key);
  433. final EditorController controller;
  434. @override
  435. _HeaderStyleButtonState createState() => _HeaderStyleButtonState();
  436. }
  437. class _HeaderStyleButtonState extends State<HeaderStyleButton> {
  438. Attribute? _value;
  439. Style get _selectionStyle => widget.controller.getSelectionStyle();
  440. @override
  441. void initState() {
  442. super.initState();
  443. setState(() {
  444. _value =
  445. _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
  446. });
  447. widget.controller.addListener(_didChangeEditingValue);
  448. }
  449. @override
  450. void didUpdateWidget(covariant HeaderStyleButton oldWidget) {
  451. super.didUpdateWidget(oldWidget);
  452. if (oldWidget.controller != widget.controller) {
  453. oldWidget.controller.removeListener(_didChangeEditingValue);
  454. widget.controller.addListener(_didChangeEditingValue);
  455. _value =
  456. _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
  457. }
  458. }
  459. @override
  460. void dispose() {
  461. widget.controller.removeListener(_didChangeEditingValue);
  462. super.dispose();
  463. }
  464. @override
  465. Widget build(BuildContext context) {
  466. return _selectHeaderStyleButtonBuilder(context, _value);
  467. }
  468. void _didChangeEditingValue() {
  469. setState(() {
  470. _value =
  471. _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
  472. });
  473. }
  474. Widget _selectHeaderStyleButtonBuilder(
  475. BuildContext context, Attribute? value) {
  476. final theme = Theme.of(context);
  477. final style = TextStyle(
  478. fontWeight: FontWeight.w600,
  479. fontSize: kToolbarIconHeight * 0.7,
  480. );
  481. final headerTextMapping = <Attribute, String>{
  482. Attribute.header: 'N',
  483. Attribute.h1: 'H1',
  484. Attribute.h2: 'H2',
  485. Attribute.h3: 'H3',
  486. Attribute.h4: 'H4',
  487. Attribute.h5: 'H5',
  488. Attribute.h6: 'H6',
  489. };
  490. final headerStyles = headerTextMapping.keys.toList(growable: false);
  491. final headerTexts = headerTextMapping.values.toList(growable: false);
  492. return Row(
  493. mainAxisSize: MainAxisSize.min,
  494. children: List.generate(headerStyles.length, (index) {
  495. return Padding(
  496. padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0),
  497. child: ConstrainedBox(
  498. constraints: BoxConstraints.tightFor(
  499. width: kToolbarButtonHeight, height: kToolbarButtonHeight),
  500. child: RawMaterialButton(
  501. fillColor: headerTextMapping[value] == headerTexts[index]
  502. ? theme.toggleableActiveColor
  503. : theme.canvasColor,
  504. hoverElevation: 0,
  505. highlightElevation: 0,
  506. visualDensity: VisualDensity.compact,
  507. shape: RoundedRectangleBorder(
  508. borderRadius: BorderRadius.circular(2)),
  509. onPressed: () {
  510. widget.controller.formatSelection(headerStyles[index]);
  511. },
  512. child: Text(
  513. headerTexts[index],
  514. style: style.copyWith(
  515. color: headerTextMapping[value] == headerTexts[index]
  516. ? theme.primaryIconTheme.color
  517. : theme.iconTheme.color,
  518. ),
  519. ),
  520. ),
  521. ),
  522. );
  523. }),
  524. );
  525. }
  526. }
  527. // Color (TextColor, BackgroundColor)
  528. class ColorButton extends StatefulWidget {
  529. const ColorButton({
  530. required this.icon,
  531. required this.isBackground,
  532. required this.controller,
  533. Key? key,
  534. }) : super(key: key);
  535. final IconData icon;
  536. final bool isBackground;
  537. final EditorController controller;
  538. @override
  539. _ColorButtonState createState() => _ColorButtonState();
  540. }
  541. class _ColorButtonState extends State<ColorButton> {
  542. late bool _isToggledColor;
  543. late bool _isToggledBackground;
  544. late bool _isWhite;
  545. late bool _isWhiteBackground;
  546. Style get _selectionStyle => widget.controller.getSelectionStyle();
  547. @override
  548. void didUpdateWidget(covariant ColorButton oldWidget) {
  549. super.didUpdateWidget(oldWidget);
  550. if (oldWidget.controller != widget.controller) {
  551. oldWidget.controller.removeListener(_didChangeEditingValue);
  552. widget.controller.addListener(_didChangeEditingValue);
  553. _updateToggledState();
  554. }
  555. }
  556. @override
  557. void initState() {
  558. super.initState();
  559. _updateToggledState();
  560. }
  561. @override
  562. void dispose() {
  563. widget.controller.removeListener(_didChangeEditingValue);
  564. super.dispose();
  565. }
  566. @override
  567. Widget build(BuildContext context) {
  568. final theme = Theme.of(context);
  569. final iconColor = _isToggledColor && !widget.isBackground && !_isWhite
  570. ? stringToColor(_selectionStyle.attributes['color']!.value)
  571. : theme.iconTheme.color;
  572. final iconColorBackground =
  573. _isToggledBackground && widget.isBackground && !_isWhiteBackground
  574. ? stringToColor(_selectionStyle.attributes['background']!.value)
  575. : theme.iconTheme.color;
  576. final fillColor = _isToggledColor && !widget.isBackground && _isWhite
  577. ? stringToColor('#ffffff')
  578. : theme.canvasColor;
  579. final fillColorBackground =
  580. _isToggledBackground && widget.isBackground && _isWhiteBackground
  581. ? stringToColor('#ffffff')
  582. : theme.canvasColor;
  583. return ToolbarIconButton(
  584. size: kToolbarButtonHeight,
  585. fillColor: widget.isBackground ? fillColorBackground : fillColor,
  586. hoverElevation: 0,
  587. highlightElevation: 0,
  588. icon: Icon(
  589. widget.icon,
  590. size: kToolbarIconHeight,
  591. color: widget.isBackground ? iconColorBackground : iconColor,
  592. ),
  593. onPressed: _showColorPicker,
  594. );
  595. }
  596. void _didChangeEditingValue() {
  597. setState(() {
  598. _updateToggledState();
  599. widget.controller.addListener(_didChangeEditingValue);
  600. });
  601. }
  602. void _updateToggledState() {
  603. _isToggledColor = _checkIfToggledColor(_selectionStyle.attributes);
  604. _isToggledBackground =
  605. _checkIfToggledBackground(_selectionStyle.attributes);
  606. _isWhite = _isToggledColor &&
  607. _selectionStyle.attributes['color']!.value == '#ffffff';
  608. _isWhiteBackground = _isToggledBackground &&
  609. _selectionStyle.attributes['background']!.value == '#ffffff';
  610. }
  611. bool _checkIfToggledColor(Map<String, Attribute> attrs) {
  612. return attrs.containsKey(Attribute.color.key);
  613. }
  614. bool _checkIfToggledBackground(Map<String, Attribute> attrs) {
  615. return attrs.containsKey(Attribute.background.key);
  616. }
  617. void _applyColor(Color color) {
  618. var hex = color.value.toRadixString(16);
  619. if (hex.startsWith('ff')) {
  620. hex = hex.substring(2);
  621. }
  622. hex = '#$hex';
  623. widget.controller.formatSelection(
  624. widget.isBackground ? BackgroundAttribute(hex) : ColorAttribute(hex));
  625. Navigator.of(context).pop();
  626. }
  627. void _showColorPicker() {
  628. showDialog(
  629. context: context,
  630. builder: (_) {
  631. return AlertDialog(
  632. title: const Text('Select Color'),
  633. backgroundColor: Theme.of(context).canvasColor,
  634. content: SingleChildScrollView(
  635. child: MaterialPicker(
  636. pickerColor: const Color(0x00000000),
  637. onColorChanged: _applyColor,
  638. enableLabel: true,
  639. ),
  640. ),
  641. );
  642. });
  643. }
  644. }
  645. // Clear Format
  646. class ClearFormatButton extends StatefulWidget {
  647. const ClearFormatButton({
  648. required this.icon,
  649. required this.controller,
  650. Key? key,
  651. }) : super(key: key);
  652. final IconData icon;
  653. final EditorController controller;
  654. @override
  655. _ClearFormatButtonState createState() => _ClearFormatButtonState();
  656. }
  657. class _ClearFormatButtonState extends State<ClearFormatButton> {
  658. @override
  659. Widget build(BuildContext context) {
  660. final theme = Theme.of(context);
  661. final iconColor = theme.iconTheme.color;
  662. final fillColor = theme.canvasColor;
  663. return ToolbarIconButton(
  664. size: kToolbarButtonHeight,
  665. fillColor: fillColor,
  666. hoverElevation: 0,
  667. highlightElevation: 0,
  668. icon: Icon(widget.icon, size: kToolbarIconHeight, color: iconColor),
  669. onPressed: () {
  670. widget.controller.getSelectionStyle().values.forEach((style) {
  671. widget.controller.formatSelection(Attribute.clone(style, null));
  672. });
  673. },
  674. );
  675. }
  676. }
  677. // Indent
  678. class IndentButton extends StatefulWidget {
  679. const IndentButton({
  680. required this.icon,
  681. required this.controller,
  682. required this.isIncrease,
  683. Key? key,
  684. }) : super(key: key);
  685. final IconData icon;
  686. final EditorController controller;
  687. final bool isIncrease;
  688. @override
  689. _IndentButtonState createState() => _IndentButtonState();
  690. }
  691. class _IndentButtonState extends State<IndentButton> {
  692. @override
  693. Widget build(BuildContext context) {
  694. final theme = Theme.of(context);
  695. final iconColor = theme.iconTheme.color;
  696. final fillColor = theme.canvasColor;
  697. return ToolbarIconButton(
  698. size: kToolbarButtonHeight,
  699. fillColor: fillColor,
  700. hoverElevation: 0,
  701. highlightElevation: 0,
  702. icon: Icon(widget.icon, size: kToolbarIconHeight, color: iconColor),
  703. onPressed: () {
  704. final indent = widget.controller
  705. .getSelectionStyle()
  706. .attributes[Attribute.indent.key];
  707. if (indent == null) {
  708. if (widget.isIncrease) {
  709. widget.controller.formatSelection(Attribute.indentL1);
  710. }
  711. return;
  712. }
  713. if (indent.value == 1 && !widget.isIncrease) {
  714. widget.controller
  715. .formatSelection(Attribute.clone(Attribute.indentL1, null));
  716. return;
  717. }
  718. if (widget.isIncrease) {
  719. // Next indent value
  720. widget.controller
  721. .formatSelection(Attribute.getIndentLevel(indent.value + 1));
  722. return;
  723. }
  724. // Prev indent value
  725. widget.controller
  726. .formatSelection(Attribute.getIndentLevel(indent.value - 1));
  727. },
  728. );
  729. }
  730. }
  731. // Link
  732. class LinkStyleButton extends StatefulWidget {
  733. const LinkStyleButton({
  734. required this.controller,
  735. required this.icon,
  736. Key? key,
  737. }) : super(key: key);
  738. final IconData? icon;
  739. final EditorController controller;
  740. @override
  741. _LinkStyleButtonState createState() => _LinkStyleButtonState();
  742. }
  743. class _LinkStyleButtonState extends State<LinkStyleButton> {
  744. @override
  745. Widget build(BuildContext context) {
  746. final theme = Theme.of(context);
  747. final canAddLink = !widget.controller.selection.isCollapsed;
  748. return ToolbarIconButton(
  749. highlightElevation: 0,
  750. hoverElevation: 0,
  751. size: kToolbarButtonHeight,
  752. icon: Icon(
  753. widget.icon,
  754. size: kToolbarIconHeight,
  755. color: canAddLink ? theme.iconTheme.color : theme.disabledColor,
  756. ),
  757. fillColor: theme.canvasColor,
  758. onPressed: canAddLink ? () => _openLinkDialog(context) : null,
  759. );
  760. }
  761. void _openLinkDialog(BuildContext context) {
  762. showDialog<String>(
  763. context: context,
  764. builder: (context) {
  765. return const LinkEditDialog();
  766. }).then(_applyLinkAttribute);
  767. }
  768. void _applyLinkAttribute(String? value) {
  769. if (value == null || value.isEmpty) {
  770. return;
  771. }
  772. widget.controller.formatSelection(LinkAttribute(value));
  773. }
  774. }
  775. class LinkEditDialog extends StatefulWidget {
  776. const LinkEditDialog({Key? key}) : super(key: key);
  777. @override
  778. _LinkEditDialogState createState() => _LinkEditDialogState();
  779. }
  780. class _LinkEditDialogState extends State<LinkEditDialog> {
  781. String _value = '';
  782. @override
  783. Widget build(BuildContext context) {
  784. return AlertDialog(
  785. content: TextField(
  786. decoration: const InputDecoration(labelText: 'Input Link'),
  787. autofocus: true,
  788. onChanged: _handleLinkChanged,
  789. ),
  790. actions: [
  791. TextButton(
  792. onPressed: _value.isNotEmpty ? _handleFinishEditing : null,
  793. child: const Text('Finish'),
  794. ),
  795. ],
  796. );
  797. }
  798. void _handleLinkChanged(String value) {
  799. setState(() => _value = value);
  800. }
  801. void _handleFinishEditing() {
  802. Navigator.pop(context, _value);
  803. }
  804. }
  805. // Embed
  806. class AddEmbedButton extends StatelessWidget {
  807. const AddEmbedButton({
  808. required this.controller,
  809. required this.icon,
  810. this.fillColor,
  811. Key? key,
  812. }) : super(key: key);
  813. final IconData icon;
  814. final EditorController controller;
  815. final Color? fillColor;
  816. @override
  817. Widget build(BuildContext context) {
  818. final theme = Theme.of(context);
  819. return ToolbarIconButton(
  820. highlightElevation: 0,
  821. hoverElevation: 0,
  822. size: kToolbarButtonHeight,
  823. icon: Icon(icon, size: kToolbarIconHeight, color: theme.iconTheme.color),
  824. fillColor: fillColor ?? theme.canvasColor,
  825. onPressed: () {
  826. final index = controller.selection.baseOffset;
  827. final length = controller.selection.extentOffset - index;
  828. controller.replaceText(
  829. index,
  830. length,
  831. BlockEmbed.horizontalRule,
  832. null,
  833. );
  834. },
  835. );
  836. }
  837. }