zefyr

controller.dart 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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:math' as math;
  5. import 'package:flutter/widgets.dart';
  6. import 'package:notus/notus.dart';
  7. import 'package:quill_delta/quill_delta.dart';
  8. import 'package:zefyr/util.dart';
  9. const TextSelection _kZeroSelection = TextSelection.collapsed(
  10. offset: 0,
  11. affinity: TextAffinity.upstream,
  12. );
  13. /// Owner of focus.
  14. enum FocusOwner {
  15. /// Current owner is the editor.
  16. editor,
  17. /// Current owner is the toolbar.
  18. toolbar,
  19. /// No focus owner.
  20. none,
  21. }
  22. /// Controls instance of [ZefyrEditor].
  23. class ZefyrController extends ChangeNotifier {
  24. ZefyrController(NotusDocument document)
  25. : assert(document != null),
  26. _document = document;
  27. /// Zefyr document managed by this controller.
  28. NotusDocument get document => _document;
  29. NotusDocument _document;
  30. /// Currently selected text within the [document].
  31. TextSelection get selection => _selection;
  32. TextSelection _selection = _kZeroSelection;
  33. ChangeSource _lastChangeSource;
  34. /// Source of the last text or selection change.
  35. ChangeSource get lastChangeSource => _lastChangeSource;
  36. /// Store any styles attribute that got toggled by the tap of a button
  37. /// and that has not been applied yet.
  38. /// It gets reseted after each format action within the [document].
  39. NotusStyle get toggledStyles => _toggledStyles;
  40. NotusStyle _toggledStyles = NotusStyle();
  41. /// Updates selection with specified [value].
  42. ///
  43. /// [value] and [source] cannot be `null`.
  44. void updateSelection(TextSelection value,
  45. {ChangeSource source = ChangeSource.remote}) {
  46. _updateSelectionSilent(value, source: source);
  47. notifyListeners();
  48. }
  49. // Updates selection without triggering notifications to listeners.
  50. void _updateSelectionSilent(TextSelection value,
  51. {ChangeSource source = ChangeSource.remote}) {
  52. assert(value != null && source != null);
  53. _selection = value;
  54. _lastChangeSource = source;
  55. _ensureSelectionBeforeLastBreak();
  56. }
  57. @override
  58. void dispose() {
  59. _document.close();
  60. super.dispose();
  61. }
  62. /// Composes [change] into document managed by this controller.
  63. ///
  64. /// This method does not apply any adjustments or heuristic rules to
  65. /// provided [change] and it is caller's responsibility to ensure this change
  66. /// can be composed without errors.
  67. ///
  68. /// If composing this change fails then this method throws [ComposeError].
  69. void compose(Delta change,
  70. {TextSelection selection, ChangeSource source = ChangeSource.remote}) {
  71. if (change.isNotEmpty) {
  72. _document.compose(change, source);
  73. }
  74. if (selection != null) {
  75. _updateSelectionSilent(selection, source: source);
  76. } else {
  77. // Transform selection against the composed change and give priority to
  78. // current position (force: false).
  79. final base =
  80. change.transformPosition(_selection.baseOffset, force: false);
  81. final extent =
  82. change.transformPosition(_selection.extentOffset, force: false);
  83. selection = _selection.copyWith(baseOffset: base, extentOffset: extent);
  84. if (_selection != selection) {
  85. _updateSelectionSilent(selection, source: source);
  86. }
  87. }
  88. _lastChangeSource = source;
  89. notifyListeners();
  90. }
  91. /// Replaces [length] characters in the document starting at [index] with
  92. /// provided [text].
  93. ///
  94. /// Resulting change is registered as produced by user action, e.g.
  95. /// using [ChangeSource.local].
  96. ///
  97. /// It also applies the toggledStyle if needed. And then it resets it
  98. /// in any cases as we don't want to keep it except on inserts.
  99. ///
  100. /// Optionally updates selection if provided.
  101. void replaceText(int index, int length, String text,
  102. {TextSelection selection}) {
  103. Delta delta;
  104. if (length > 0 || text.isNotEmpty) {
  105. delta = document.replace(index, length, text);
  106. // If the delta is a classical insert operation and we have toggled
  107. // some style, then we apply it to our document.
  108. if (delta != null &&
  109. toggledStyles.isNotEmpty &&
  110. delta.length == 2 &&
  111. delta[1].isInsert) {
  112. // Apply it.
  113. Delta retainDelta = Delta()
  114. ..retain(index)
  115. ..retain(1, toggledStyles.toJson());
  116. document.compose(retainDelta, ChangeSource.local);
  117. }
  118. }
  119. // Always reset it after any user action, even if it has not been applied.
  120. _toggledStyles = NotusStyle();
  121. if (selection != null) {
  122. if (delta == null) {
  123. _updateSelectionSilent(selection, source: ChangeSource.local);
  124. } else {
  125. // need to transform selection position in case actual delta
  126. // is different from user's version (in deletes and inserts).
  127. Delta user = Delta()
  128. ..retain(index)
  129. ..insert(text)
  130. ..delete(length);
  131. int positionDelta = getPositionDelta(user, delta);
  132. _updateSelectionSilent(
  133. selection.copyWith(
  134. baseOffset: selection.baseOffset + positionDelta,
  135. extentOffset: selection.extentOffset + positionDelta,
  136. ),
  137. source: ChangeSource.local,
  138. );
  139. }
  140. }
  141. _lastChangeSource = ChangeSource.local;
  142. notifyListeners();
  143. }
  144. void formatText(int index, int length, NotusAttribute attribute) {
  145. final change = document.format(index, length, attribute);
  146. _lastChangeSource = ChangeSource.local;
  147. if (length == 0 &&
  148. (attribute.key == NotusAttribute.bold.key ||
  149. attribute.key == NotusAttribute.italic.key)) {
  150. // Add the attribute to our toggledStyle. It will be used later upon insertion.
  151. _toggledStyles = toggledStyles.put(attribute);
  152. }
  153. // Transform selection against the composed change and give priority to
  154. // the change. This is needed in cases when format operation actually
  155. // inserts data into the document (e.g. embeds).
  156. final base = change.transformPosition(_selection.baseOffset);
  157. final extent = change.transformPosition(_selection.extentOffset);
  158. final adjustedSelection =
  159. _selection.copyWith(baseOffset: base, extentOffset: extent);
  160. if (_selection != adjustedSelection) {
  161. _updateSelectionSilent(adjustedSelection, source: _lastChangeSource);
  162. }
  163. notifyListeners();
  164. }
  165. /// Formats current selection with [attribute].
  166. void formatSelection(NotusAttribute attribute) {
  167. int index = _selection.start;
  168. int length = _selection.end - index;
  169. formatText(index, length, attribute);
  170. }
  171. /// Returns style of specified text range.
  172. ///
  173. /// If nothing is selected but we've toggled an attribute,
  174. /// we also merge those in our style before returning.
  175. NotusStyle getSelectionStyle() {
  176. int start = _selection.start;
  177. int length = _selection.end - start;
  178. var lineStyle = _document.collectStyle(start, length);
  179. lineStyle = lineStyle.mergeAll(toggledStyles);
  180. return lineStyle;
  181. }
  182. TextEditingValue get plainTextEditingValue {
  183. return TextEditingValue(
  184. text: document.toPlainText(),
  185. selection: selection,
  186. composing: TextRange.collapsed(0),
  187. );
  188. }
  189. void _ensureSelectionBeforeLastBreak() {
  190. final end = _document.length - 1;
  191. final base = math.min(_selection.baseOffset, end);
  192. final extent = math.min(_selection.extentOffset, end);
  193. _selection = _selection.copyWith(baseOffset: base, extentOffset: extent);
  194. }
  195. }