controller.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import 'dart:math' as math;
  2. import 'package:flutter/material.dart';
  3. import 'package:tuple/tuple.dart';
  4. import '../model/quill_delta.dart';
  5. import '../util/delta_diff.dart';
  6. import '../model/document/attribute.dart';
  7. import '../model/document/document.dart';
  8. import '../model/document/style.dart';
  9. import '../model/document/node/embed.dart';
  10. class EditorController extends ChangeNotifier {
  11. EditorController({
  12. required this.document,
  13. required this.selection,
  14. });
  15. factory EditorController.basic() {
  16. return EditorController(
  17. document: Document(),
  18. selection: const TextSelection.collapsed(offset: 0),
  19. );
  20. }
  21. final Document document;
  22. TextSelection selection;
  23. Style toggledStyle = Style();
  24. // item1: Document state before [change].
  25. // item2: Change delta applied to the document.
  26. // item3: The source of this change.
  27. Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => document.changes;
  28. TextEditingValue get plainTextEditingValue => TextEditingValue(
  29. text: document.toPlainText(),
  30. selection: selection,
  31. );
  32. Style getSelectionStyle() =>
  33. document.collectStyle(selection.start, selection.end - selection.start)
  34. ..mergeAll(toggledStyle);
  35. bool get hasUndo => document.hasUndo;
  36. bool get hasRedo => document.hasRedo;
  37. void undo() {
  38. final action = document.undo();
  39. if (action.item1) {
  40. _handleHistoryChange(action.item2);
  41. }
  42. }
  43. void redo() {
  44. final action = document.redo();
  45. if (action.item1) {
  46. _handleHistoryChange(action.item2);
  47. }
  48. }
  49. @override
  50. void dispose() {
  51. document.close();
  52. super.dispose();
  53. }
  54. void updateSelection(TextSelection textSelection, ChangeSource source) {
  55. _updateSelection(textSelection, source);
  56. notifyListeners();
  57. }
  58. void formatSelection(Attribute? attribute) {
  59. formatText(selection.start, selection.end - selection.start, attribute);
  60. }
  61. void formatText(int index, int length, Attribute? attribute) {
  62. if (length == 0 &&
  63. attribute!.isInline &&
  64. attribute.key != Attribute.link.key) {
  65. toggledStyle = toggledStyle.put(attribute);
  66. }
  67. final change = document.format(index, length, attribute);
  68. final adjustedSelection = selection.copyWith(
  69. baseOffset: change.transformPosition(selection.baseOffset),
  70. extentOffset: change.transformPosition(selection.extentOffset),
  71. );
  72. if (selection != adjustedSelection) {
  73. _updateSelection(adjustedSelection, ChangeSource.LOCAL);
  74. }
  75. notifyListeners();
  76. }
  77. void replaceText(
  78. int index, int length, Object? data, TextSelection? textSelection) {
  79. assert(data is String || data is Embeddable);
  80. Delta? delta;
  81. if (length > 0 || data is! String || data.isNotEmpty) {
  82. delta = document.replace(index, length, data);
  83. var shouldRetainDelta = toggledStyle.isNotEmpty &&
  84. delta.isNotEmpty &&
  85. delta.length <= 2 &&
  86. delta.last.isInsert;
  87. if (shouldRetainDelta &&
  88. toggledStyle.isNotEmpty &&
  89. delta.length == 2 &&
  90. delta.last.data == '\n') {
  91. // if all attributes are inline, shouldRetainDelta should be false
  92. final anyAttributeNotInline =
  93. toggledStyle.values.any((attr) => !attr.isInline);
  94. shouldRetainDelta &= anyAttributeNotInline;
  95. }
  96. if (shouldRetainDelta) {
  97. final retainDelta = Delta()
  98. ..retain(index)
  99. ..retain(
  100. data is String ? data.length : 1,
  101. toggledStyle.toJson(),
  102. );
  103. document.compose(retainDelta, ChangeSource.LOCAL);
  104. }
  105. }
  106. toggledStyle = Style();
  107. if (textSelection != null) {
  108. if (delta == null || delta.isEmpty) {
  109. _updateSelection(textSelection, ChangeSource.LOCAL);
  110. } else {
  111. final user = Delta()
  112. ..retain(index)
  113. ..insert(data)
  114. ..delete(length);
  115. final positionDelta = getPositionDelta(user, delta);
  116. _updateSelection(
  117. textSelection.copyWith(
  118. baseOffset: textSelection.baseOffset + positionDelta,
  119. extentOffset: textSelection.extentOffset + positionDelta,
  120. ),
  121. ChangeSource.LOCAL);
  122. }
  123. }
  124. notifyListeners();
  125. }
  126. void compose(Delta delta, TextSelection textSelection, ChangeSource source) {
  127. if (delta.isNotEmpty) {
  128. document.compose(delta, source);
  129. }
  130. textSelection = selection.copyWith(
  131. baseOffset: delta.transformPosition(selection.baseOffset, force: false),
  132. extentOffset:
  133. delta.transformPosition(selection.extentOffset, force: false),
  134. );
  135. if (selection != textSelection) {
  136. _updateSelection(textSelection, source);
  137. }
  138. notifyListeners();
  139. }
  140. /* --------------------------------- Helper --------------------------------- */
  141. void _handleHistoryChange(int? length) {
  142. if (length != 0) {
  143. updateSelection(
  144. TextSelection.collapsed(offset: selection.baseOffset + length!),
  145. ChangeSource.LOCAL,
  146. );
  147. } else {
  148. // no need to move cursor
  149. notifyListeners();
  150. }
  151. }
  152. void _updateSelection(TextSelection textSelection, ChangeSource source) {
  153. selection = textSelection;
  154. final end = document.length - 1;
  155. selection = selection.copyWith(
  156. baseOffset: math.min(selection.baseOffset, end),
  157. extentOffset: math.min(selection.extentOffset, end),
  158. );
  159. }
  160. }