Browse Source

Merge branch 'master' into flutter-master

Anatoly Pulyaevskiy 6 years ago
parent
commit
3be80cf9c2
56 changed files with 1022 additions and 619 deletions
  1. 1
    1
      doc/quick_start.md
  2. 4
    3
      packages/notus/analysis_options.yaml
  3. 4
    4
      packages/notus/lib/src/convert/markdown.dart
  4. 1
    1
      packages/notus/lib/src/document.dart
  5. 9
    9
      packages/notus/lib/src/document/attributes.dart
  6. 1
    1
      packages/notus/lib/src/document/block.dart
  7. 2
    2
      packages/notus/lib/src/document/leaf.dart
  8. 5
    3
      packages/notus/lib/src/document/line.dart
  9. 1
    1
      packages/notus/lib/src/document/node.dart
  10. 2
    1
      packages/notus/lib/src/heuristics/delete_rules.dart
  11. 3
    3
      packages/notus/lib/src/heuristics/insert_rules.dart
  12. 1
    1
      packages/notus/test/convert/markdown_test.dart
  13. 1
    1
      packages/notus/test/document/attributes_test.dart
  14. 1
    1
      packages/notus/test/document_test.dart
  15. 18
    0
      packages/zefyr/CHANGELOG.md
  16. 35
    4
      packages/zefyr/README.md
  17. 4
    3
      packages/zefyr/analysis_options.yaml
  18. 12
    0
      packages/zefyr/example/lib/main.dart
  19. 1
    1
      packages/zefyr/example/lib/src/form.dart
  20. 18
    1
      packages/zefyr/example/lib/src/full_page.dart
  21. 87
    0
      packages/zefyr/example/lib/src/view.dart
  22. 9
    9
      packages/zefyr/lib/src/widgets/buttons.dart
  23. 14
    2
      packages/zefyr/lib/src/widgets/caret.dart
  24. 25
    41
      packages/zefyr/lib/src/widgets/common.dart
  25. 42
    0
      packages/zefyr/lib/src/widgets/cursor_timer.dart
  26. 42
    13
      packages/zefyr/lib/src/widgets/editable_box.dart
  27. 40
    104
      packages/zefyr/lib/src/widgets/editable_text.dart
  28. 13
    167
      packages/zefyr/lib/src/widgets/editor.dart
  29. 1
    1
      packages/zefyr/lib/src/widgets/image.dart
  30. 2
    2
      packages/zefyr/lib/src/widgets/quote.dart
  31. 4
    2
      packages/zefyr/lib/src/widgets/render_context.dart
  32. 9
    8
      packages/zefyr/lib/src/widgets/rich_text.dart
  33. 232
    0
      packages/zefyr/lib/src/widgets/scope.dart
  34. 41
    28
      packages/zefyr/lib/src/widgets/selection.dart
  35. 6
    0
      packages/zefyr/lib/src/widgets/theme.dart
  36. 3
    3
      packages/zefyr/lib/src/widgets/toolbar.dart
  37. 105
    0
      packages/zefyr/lib/src/widgets/view.dart
  38. 2
    0
      packages/zefyr/lib/zefyr.dart
  39. 1
    1
      packages/zefyr/pubspec.yaml
  40. 4
    3
      packages/zefyr/test/painting/caret_painter_test.dart
  41. 0
    41
      packages/zefyr/test/rendering/render_editable_paragraph_test.dart
  42. 64
    0
      packages/zefyr/test/rendering/render_editable_proxy_box_test.dart
  43. 20
    3
      packages/zefyr/test/testing.dart
  44. 14
    14
      packages/zefyr/test/widgets/buttons_test.dart
  45. 1
    1
      packages/zefyr/test/widgets/code_test.dart
  46. 0
    107
      packages/zefyr/test/widgets/editable_text_scope_test.dart
  47. 13
    1
      packages/zefyr/test/widgets/editable_text_test.dart
  48. 4
    4
      packages/zefyr/test/widgets/editor_test.dart
  49. 4
    4
      packages/zefyr/test/widgets/horizontal_rule_test.dart
  50. 5
    4
      packages/zefyr/test/widgets/image_test.dart
  51. 1
    1
      packages/zefyr/test/widgets/list_test.dart
  52. 1
    1
      packages/zefyr/test/widgets/quote_test.dart
  53. 1
    0
      packages/zefyr/test/widgets/render_context_test.dart
  54. 5
    5
      packages/zefyr/test/widgets/rich_text_test.dart
  55. 75
    0
      packages/zefyr/test/widgets/scope_test.dart
  56. 8
    8
      packages/zefyr/test/widgets/selection_test.dart

+ 1
- 1
doc/quick_start.md View File

12
 
12
 
13
 ```yaml
13
 ```yaml
14
 dependencies:
14
 dependencies:
15
-  zefyr: ^0.3.0
15
+  zefyr: [latest_version]
16
 ```
16
 ```
17
 
17
 
18
 And run `flutter packages get` to install. This installs both `zefyr`
18
 And run `flutter packages get` to install. This installs both `zefyr`

+ 4
- 3
packages/notus/analysis_options.yaml View File

1
 analyzer:
1
 analyzer:
2
-  language:
3
-    enableSuperMixins: true
2
+  strong-mode:
3
+    implicit-casts: false
4
+    implicit-dynamic: false
4
 
5
 
5
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
6
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
6
 linter:
7
 linter:
7
   rules:
8
   rules:
8
-    # - avoid_init_to_null
9
+    - avoid_init_to_null
9
     - cancel_subscriptions
10
     - cancel_subscriptions
10
     - close_sinks
11
     - close_sinks
11
     - directives_ordering
12
     - directives_ordering

+ 4
- 4
packages/notus/lib/src/convert/markdown.dart View File

114
   String _writeLine(String text, NotusStyle style) {
114
   String _writeLine(String text, NotusStyle style) {
115
     StringBuffer buffer = new StringBuffer();
115
     StringBuffer buffer = new StringBuffer();
116
     if (style.contains(NotusAttribute.heading)) {
116
     if (style.contains(NotusAttribute.heading)) {
117
-      _writeAttribute(buffer, style.get(NotusAttribute.heading));
117
+      _writeAttribute(buffer, style.get<int>(NotusAttribute.heading));
118
     }
118
     }
119
 
119
 
120
     // Write the text itself
120
     // Write the text itself
163
     } else if (attribute == NotusAttribute.italic) {
163
     } else if (attribute == NotusAttribute.italic) {
164
       _writeItalicTag(buffer);
164
       _writeItalicTag(buffer);
165
     } else if (attribute.key == NotusAttribute.link.key) {
165
     } else if (attribute.key == NotusAttribute.link.key) {
166
-      _writeLinkTag(buffer, attribute, close: close);
166
+      _writeLinkTag(buffer, attribute as NotusAttribute<String>, close: close);
167
     } else if (attribute.key == NotusAttribute.heading.key) {
167
     } else if (attribute.key == NotusAttribute.heading.key) {
168
-      _writeHeadingTag(buffer, attribute);
168
+      _writeHeadingTag(buffer, attribute as NotusAttribute<int>);
169
     } else if (attribute.key == NotusAttribute.block.key) {
169
     } else if (attribute.key == NotusAttribute.block.key) {
170
-      _writeBlockTag(buffer, attribute, close: close);
170
+      _writeBlockTag(buffer, attribute as NotusAttribute<String>, close: close);
171
     } else {
171
     } else {
172
       throw new ArgumentError('Cannot handle $attribute');
172
       throw new ArgumentError('Cannot handle $attribute');
173
     }
173
     }

+ 1
- 1
packages/notus/lib/src/document.dart View File

44
     _loadDocument(_delta);
44
     _loadDocument(_delta);
45
   }
45
   }
46
 
46
 
47
-  NotusDocument.fromJson(dynamic data)
47
+  NotusDocument.fromJson(List data)
48
       : _heuristics = NotusHeuristics.fallback,
48
       : _heuristics = NotusHeuristics.fallback,
49
         _delta = Delta.fromJson(data) {
49
         _delta = Delta.fromJson(data) {
50
     _loadDocument(_delta);
50
     _loadDocument(_delta);

+ 9
- 9
packages/notus/lib/src/document/attributes.dart View File

122
   /// Embed style attribute.
122
   /// Embed style attribute.
123
   static const embed = const EmbedAttributeBuilder._();
123
   static const embed = const EmbedAttributeBuilder._();
124
 
124
 
125
-  factory NotusAttribute._fromKeyValue(String key, T value) {
125
+  static NotusAttribute _fromKeyValue(String key, dynamic value) {
126
     if (!_registry.containsKey(key))
126
     if (!_registry.containsKey(key))
127
       throw new ArgumentError.value(
127
       throw new ArgumentError.value(
128
           key, 'No attribute with key "$key" registered.');
128
           key, 'No attribute with key "$key" registered.');
166
       new NotusAttribute<T>._(key, scope, value);
166
       new NotusAttribute<T>._(key, scope, value);
167
 
167
 
168
   @override
168
   @override
169
-  bool operator ==(other) {
169
+  bool operator ==(Object other) {
170
     if (identical(this, other)) return true;
170
     if (identical(this, other)) return true;
171
     if (other is! NotusAttribute<T>) return false;
171
     if (other is! NotusAttribute<T>) return false;
172
     NotusAttribute<T> typedOther = other;
172
     NotusAttribute<T> typedOther = other;
190
 
190
 
191
   final Map<String, NotusAttribute> _data;
191
   final Map<String, NotusAttribute> _data;
192
 
192
 
193
-  static NotusStyle fromJson(Map data) {
193
+  static NotusStyle fromJson(Map<String, dynamic> data) {
194
     if (data == null) return new NotusStyle();
194
     if (data == null) return new NotusStyle();
195
 
195
 
196
-    final result = data.map((key, value) {
197
-      var attr = new NotusAttribute._fromKeyValue(key, value);
196
+    final result = data.map((String key, dynamic value) {
197
+      var attr = NotusAttribute._fromKeyValue(key, value);
198
       return new MapEntry<String, NotusAttribute>(key, attr);
198
       return new MapEntry<String, NotusAttribute>(key, attr);
199
     });
199
     });
200
     return new NotusStyle._(result);
200
     return new NotusStyle._(result);
228
   /// [attribute].
228
   /// [attribute].
229
   bool containsSame(NotusAttribute attribute) {
229
   bool containsSame(NotusAttribute attribute) {
230
     assert(attribute != null);
230
     assert(attribute != null);
231
-    return get(attribute) == attribute;
231
+    return get<dynamic>(attribute) == attribute;
232
   }
232
   }
233
 
233
 
234
   /// Returns value of specified attribute [key] in this set.
234
   /// Returns value of specified attribute [key] in this set.
290
   /// Returns JSON-serializable representation of this style.
290
   /// Returns JSON-serializable representation of this style.
291
   Map<String, dynamic> toJson() => _data.isEmpty
291
   Map<String, dynamic> toJson() => _data.isEmpty
292
       ? null
292
       ? null
293
-      : _data.map(
294
-          (_, value) => new MapEntry<String, dynamic>(value.key, value.value));
293
+      : _data.map<String, dynamic>((String _, NotusAttribute value) =>
294
+          new MapEntry<String, dynamic>(value.key, value.value));
295
 
295
 
296
   @override
296
   @override
297
-  bool operator ==(other) {
297
+  bool operator ==(Object other) {
298
     if (identical(this, other)) return true;
298
     if (identical(this, other)) return true;
299
     if (other is! NotusStyle) return false;
299
     if (other is! NotusStyle) return false;
300
     NotusStyle typedOther = other;
300
     NotusStyle typedOther = other;

+ 1
- 1
packages/notus/lib/src/document/block.dart View File

40
       while (child != line) {
40
       while (child != line) {
41
         child.unlink();
41
         child.unlink();
42
         before.add(child);
42
         before.add(child);
43
-        child = this.first;
43
+        child = this.first as LineNode;
44
       }
44
       }
45
       line.unlink();
45
       line.unlink();
46
       insertBefore(line);
46
       insertBefore(line);

+ 2
- 2
packages/notus/lib/src/document/leaf.dart View File

52
     assert(index >= 0 && index <= length);
52
     assert(index >= 0 && index <= length);
53
     if (index == 0) return this;
53
     if (index == 0) return this;
54
     if (index == length && isLast) return null;
54
     if (index == length && isLast) return null;
55
-    if (index == length && !isLast) return next;
55
+    if (index == length && !isLast) return next as LeafNode;
56
 
56
 
57
     String text = _value;
57
     String text = _value;
58
     _value = text.substring(0, index);
58
     _value = text.substring(0, index);
124
   }
124
   }
125
 
125
 
126
   @override
126
   @override
127
-  LineNode get parent => super.parent;
127
+  LineNode get parent => super.parent as LineNode;
128
 
128
 
129
   @override
129
   @override
130
   int get length => _value.length;
130
   int get length => _value.length;

+ 5
- 3
packages/notus/lib/src/document/line.dart View File

34
     if (isLast) {
34
     if (isLast) {
35
       if (parent is BlockNode) {
35
       if (parent is BlockNode) {
36
         if (parent.isLast) return null;
36
         if (parent.isLast) return null;
37
-        return (parent.next is BlockNode)
37
+        LineNode line = (parent.next is BlockNode)
38
             ? (parent.next as BlockNode).first
38
             ? (parent.next as BlockNode).first
39
             : parent.next;
39
             : parent.next;
40
+        return line;
40
       } else
41
       } else
41
         return null;
42
         return null;
42
     } else {
43
     } else {
43
-      return (next is BlockNode) ? (next as BlockNode).first : next;
44
+      LineNode line = (next is BlockNode) ? (next as BlockNode).first : next;
45
+      return line;
44
     }
46
     }
45
   }
47
   }
46
 
48
 
129
       result = result.mergeAll(node.style);
131
       result = result.mergeAll(node.style);
130
       int pos = node.length - data.offset;
132
       int pos = node.length - data.offset;
131
       while (!node.isLast && pos < local) {
133
       while (!node.isLast && pos < local) {
132
-        node = node.next;
134
+        node = node.next as LeafNode;
133
         _handle(node.style);
135
         _handle(node.style);
134
         pos += node.length;
136
         pos += node.length;
135
       }
137
       }

+ 1
- 1
packages/notus/lib/src/document/node.dart View File

238
 
238
 
239
     if (isEmpty) {
239
     if (isEmpty) {
240
       assert(index == 0);
240
       assert(index == 0);
241
-      Node node = defaultChild;
241
+      T node = defaultChild;
242
       add(node);
242
       add(node);
243
       node.insert(index, value, style);
243
       node.insert(index, value, style);
244
     } else {
244
     } else {

+ 2
- 1
packages/notus/lib/src/heuristics/delete_rules.dart View File

69
 
69
 
70
   Map<String, dynamic> _unsetAttributes(Map<String, dynamic> attributes) {
70
   Map<String, dynamic> _unsetAttributes(Map<String, dynamic> attributes) {
71
     if (attributes == null) return null;
71
     if (attributes == null) return null;
72
-    return attributes.map((key, value) => new MapEntry(key, null));
72
+    return attributes.map<String, dynamic>((String key, dynamic value) =>
73
+        new MapEntry<String, dynamic>(key, null));
73
   }
74
   }
74
 }
75
 }
75
 
76
 

+ 3
- 3
packages/notus/lib/src/heuristics/insert_rules.dart View File

87
     final target = iter.next();
87
     final target = iter.next();
88
 
88
 
89
     if (target.data.startsWith('\n')) {
89
     if (target.data.startsWith('\n')) {
90
-      Map<String, dynamic> resetStyle = null;
90
+      Map<String, dynamic> resetStyle;
91
       if (target.attributes != null &&
91
       if (target.attributes != null &&
92
           target.attributes.containsKey(NotusAttribute.heading.key)) {
92
           target.attributes.containsKey(NotusAttribute.heading.key)) {
93
         resetStyle = NotusAttribute.heading.unset.toJson();
93
         resetStyle = NotusAttribute.heading.unset.toJson();
290
       }
290
       }
291
     }
291
     }
292
 
292
 
293
-    Map<String, dynamic> resetStyle = null;
294
-    Map<String, dynamic> blockStyle = null;
293
+    Map<String, dynamic> resetStyle;
294
+    Map<String, dynamic> blockStyle;
295
     if (lineStyle != null) {
295
     if (lineStyle != null) {
296
       if (lineStyle.containsKey(NotusAttribute.heading.key)) {
296
       if (lineStyle.containsKey(NotusAttribute.heading.key)) {
297
         resetStyle = NotusAttribute.heading.unset.toJson();
297
         resetStyle = NotusAttribute.heading.unset.toJson();

+ 1
- 1
packages/notus/test/convert/markdown_test.dart View File

141
 
141
 
142
 final doc =
142
 final doc =
143
     r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\nZefyr is an "},{"insert":"early preview","attributes":{"b":true}},{"insert":" open source library.\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data format and Document Model"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style attributes"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic rules"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and flexibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:notus/notus.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" print(“Hello world!”);"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}}]';
143
     r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\nZefyr is an "},{"insert":"early preview","attributes":{"b":true}},{"insert":" open source library.\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data format and Document Model"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style attributes"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic rules"},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and flexibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:notus/notus.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" print(“Hello world!”);"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}}]';
144
-final delta = Delta.fromJson(json.decode(doc));
144
+final delta = Delta.fromJson(json.decode(doc) as List);
145
 
145
 
146
 final expectedMarkdown = '''
146
 final expectedMarkdown = '''
147
 # Zefyr
147
 # Zefyr

+ 1
- 1
packages/notus/test/document/attributes_test.dart View File

7
 void main() {
7
 void main() {
8
   group('$NotusStyle', () {
8
   group('$NotusStyle', () {
9
     test('get', () {
9
     test('get', () {
10
-      var attrs = NotusStyle.fromJson({'block': 'ul'});
10
+      var attrs = NotusStyle.fromJson(<String, dynamic>{'block': 'ul'});
11
       var attr = attrs.get(NotusAttribute.block);
11
       var attr = attrs.get(NotusAttribute.block);
12
       expect(attr, NotusAttribute.ul);
12
       expect(attr, NotusAttribute.ul);
13
     });
13
     });

+ 1
- 1
packages/notus/test/document_test.dart View File

46
     test('json serialization', () {
46
     test('json serialization', () {
47
       final original = dartconfDoc();
47
       final original = dartconfDoc();
48
       final jsonData = json.encode(original);
48
       final jsonData = json.encode(original);
49
-      final doc = NotusDocument.fromJson(json.decode(jsonData));
49
+      final doc = NotusDocument.fromJson(json.decode(jsonData) as List);
50
       expect(doc.toDelta(), original.toDelta());
50
       expect(doc.toDelta(), original.toDelta());
51
       expect(json.encode(doc), jsonData);
51
       expect(json.encode(doc), jsonData);
52
     });
52
     });

+ 18
- 0
packages/zefyr/CHANGELOG.md View File

1
+## Unreleased
2
+
3
+* Added non-scrollable `ZefyrView` widget which allows previewing Notus documents inside
4
+  layouts using their own scrollables like ListView.
5
+* Breaking change: renamed `EditableRichText` to `ZefyrRichText`. User code is unlikely to be
6
+  affected unless you've extended Zefyr with custom implementations of block widgets.
7
+* Breaking change: renamed `RenderEditableParagraph` to `RenderZefyrParagraph`. User code is
8
+  unlikely to be affected unless you've extended Zefyr with custom implementations of block widgets.
9
+* Added `ZefyrScope` class - replaces previously used scope objects `ZefyrEditableTextScope` and
10
+  `ZefyrEditorScope`. Unified all shared resources under one class.
11
+* Breaking change: removed `ZefyrEditor.of` and `ZefyrEditableText.of` static methods.
12
+  Use `ZefyrScope.of` instead.
13
+
14
+## 0.3.1
15
+
16
+- Fixed autofocus not being triggered when set to `true` for the first time.
17
+- Allow customizing cursor color via ZefyrTheme.
18
+
1
 ## 0.3.0
19
 ## 0.3.0
2
 
20
 
3
 This version introduces new widget `ZefyrScaffold` which allows embedding Zefyr in custom
21
 This version introduces new widget `ZefyrScaffold` which allows embedding Zefyr in custom

+ 35
- 4
packages/zefyr/README.md View File

7
 
7
 
8
 [issue tracker]: https://github.com/memspace/zefyr/issues
8
 [issue tracker]: https://github.com/memspace/zefyr/issues
9
 
9
 
10
-> **Important:** Zefyr currently tracks latest development version of
11
-> Flutter (`master` branch) and may not work with Flutter beta channel
12
-> if there are breaking changes.
13
-
14
 For documentation see [https://github.com/memspace/zefyr](https://github.com/memspace/zefyr).
10
 For documentation see [https://github.com/memspace/zefyr](https://github.com/memspace/zefyr).
15
 
11
 
16
 ![zefyr screenshot](https://github.com/memspace/zefyr/raw/master/packages/zefyr/zefyr.png)
12
 ![zefyr screenshot](https://github.com/memspace/zefyr/raw/master/packages/zefyr/zefyr.png)
13
+
14
+## Installation
15
+
16
+Official releases of Zefyr can be installed from Dart's Pub package repository.
17
+
18
+> Note that versions from Pub track stable channel of Flutter. If you are on master channel
19
+> check out instructions below in this document.
20
+
21
+
22
+To install Zefyr from Pub add `zefyr` package as a dependency to your `pubspec.yaml`:
23
+
24
+```yaml
25
+dependencies:
26
+  zefyr: [latest_version]
27
+```
28
+
29
+And run `flutter packages get`.
30
+
31
+#### Installing version of Zefyr compatible with master channel of Flutter.
32
+
33
+You need to add git dependency to your pubspec.yaml that points to `flutter-master` branch:
34
+
35
+```yaml
36
+dependencies:
37
+  zefyr:
38
+    git:
39
+      url: https://github.com/memspace/zefyr.git
40
+      ref: flutter-master
41
+      path: packages/zefyr
42
+```
43
+
44
+And run `flutter packages get`.
45
+
46
+Continue to [https://github.com/memspace/zefyr/blob/master/doc/quick_start.md](documentation) to
47
+learn more about Zefyr and how to use it in your projects.

+ 4
- 3
packages/zefyr/analysis_options.yaml View File

1
 analyzer:
1
 analyzer:
2
-  language:
3
-#    enableSuperMixins: true
2
+  strong-mode:
3
+    implicit-casts: false
4
+    implicit-dynamic: false
4
 
5
 
5
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
6
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
6
 linter:
7
 linter:
7
   rules:
8
   rules:
8
-    # - avoid_init_to_null
9
+    - avoid_init_to_null
9
     - cancel_subscriptions
10
     - cancel_subscriptions
10
     - close_sinks
11
     - close_sinks
11
     - directives_ordering
12
     - directives_ordering

+ 12
- 0
packages/zefyr/example/lib/main.dart View File

4
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
5
 import 'src/form.dart';
5
 import 'src/form.dart';
6
 import 'src/full_page.dart';
6
 import 'src/full_page.dart';
7
+import 'src/view.dart';
7
 
8
 
8
 void main() {
9
 void main() {
9
   runApp(new ZefyrApp());
10
   runApp(new ZefyrApp());
20
       routes: {
21
       routes: {
21
         "/fullPage": buildFullPage,
22
         "/fullPage": buildFullPage,
22
         "/form": buildFormPage,
23
         "/form": buildFormPage,
24
+        "/view": buildViewPage,
23
       },
25
       },
24
     );
26
     );
25
   }
27
   }
31
   Widget buildFormPage(BuildContext context) {
33
   Widget buildFormPage(BuildContext context) {
32
     return FormEmbeddedScreen();
34
     return FormEmbeddedScreen();
33
   }
35
   }
36
+
37
+  Widget buildViewPage(BuildContext context) {
38
+    return ViewScreen();
39
+  }
34
 }
40
 }
35
 
41
 
36
 class HomePage extends StatelessWidget {
42
 class HomePage extends StatelessWidget {
59
             color: Colors.lightBlue,
65
             color: Colors.lightBlue,
60
             textColor: Colors.white,
66
             textColor: Colors.white,
61
           ),
67
           ),
68
+          FlatButton(
69
+            onPressed: () => nav.pushNamed('/view'),
70
+            child: Text('Read-only embeddable view'),
71
+            color: Colors.lightBlue,
72
+            textColor: Colors.white,
73
+          ),
62
           Expanded(child: Container()),
74
           Expanded(child: Container()),
63
         ],
75
         ],
64
       ),
76
       ),

+ 1
- 1
packages/zefyr/example/lib/src/form.dart View File

56
         decoration: InputDecoration(labelText: 'Description'),
56
         decoration: InputDecoration(labelText: 'Description'),
57
         controller: _controller,
57
         controller: _controller,
58
         focusNode: _focusNode,
58
         focusNode: _focusNode,
59
-        autofocus: false,
59
+        autofocus: true,
60
         imageDelegate: new CustomImageDelegate(),
60
         imageDelegate: new CustomImageDelegate(),
61
         physics: ClampingScrollPhysics(),
61
         physics: ClampingScrollPhysics(),
62
       ),
62
       ),

+ 18
- 1
packages/zefyr/example/lib/src/full_page.dart View File

1
+import 'dart:async';
1
 import 'dart:convert';
2
 import 'dart:convert';
2
 
3
 
3
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
31
     r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
32
     r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
32
 
33
 
33
 Delta getDelta() {
34
 Delta getDelta() {
34
-  return Delta.fromJson(json.decode(doc));
35
+  return Delta.fromJson(json.decode(doc) as List);
35
 }
36
 }
36
 
37
 
37
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
38
 class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
39
       ZefyrController(NotusDocument.fromDelta(getDelta()));
40
       ZefyrController(NotusDocument.fromDelta(getDelta()));
40
   final FocusNode _focusNode = new FocusNode();
41
   final FocusNode _focusNode = new FocusNode();
41
   bool _editing = false;
42
   bool _editing = false;
43
+  StreamSubscription<NotusChange> _sub;
44
+
45
+  @override
46
+  void initState() {
47
+    super.initState();
48
+    _sub = _controller.document.changes.listen((change) {
49
+      print('${change.source}: ${change.change}');
50
+    });
51
+  }
52
+
53
+  @override
54
+  void dispose() {
55
+    _sub.cancel();
56
+    super.dispose();
57
+  }
42
 
58
 
43
   @override
59
   @override
44
   Widget build(BuildContext context) {
60
   Widget build(BuildContext context) {
45
     final theme = new ZefyrThemeData(
61
     final theme = new ZefyrThemeData(
62
+      cursorColor: Colors.blue,
46
       toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
63
       toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
47
         color: Colors.grey.shade800,
64
         color: Colors.grey.shade800,
48
         toggleColor: Colors.grey.shade900,
65
         toggleColor: Colors.grey.shade900,

+ 87
- 0
packages/zefyr/example/lib/src/view.dart View File

1
+import 'dart:convert';
2
+
3
+import 'package:flutter/material.dart';
4
+import 'package:quill_delta/quill_delta.dart';
5
+import 'package:zefyr/zefyr.dart';
6
+
7
+import 'full_page.dart';
8
+
9
+class ViewScreen extends StatefulWidget {
10
+  @override
11
+  _ViewScreen createState() => new _ViewScreen();
12
+}
13
+
14
+final doc =
15
+    r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\n"},{"insert":"​","attributes":{"embed":{"type":"image","source":"asset://images/breeze.jpg"}}},{"insert":"\n"},{"insert":"Photo by Hiroyuki Takeda.","attributes":{"i":true}},{"insert":"\nZefyr is currently in "},{"insert":"early preview","attributes":{"b":true}},{"insert":". If you have a feature request or found a bug, please file it at the "},{"insert":"issue tracker","attributes":{"a":"https://github.com/memspace/zefyr/issues"}},{"insert":'
16
+    r'".\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/quick_start.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data Format and Document Model","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/data_and_document.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style Attributes","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/attr'
17
+    r'ibutes.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic Rules","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/heuristics.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"FAQ","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/faq.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and fle'
18
+    r'xibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nMarkdown inspired semantics"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Ever needed to have a heading line inside of a quote block, like this:\nI’m a Markdown heading"},{"insert":"\n","attributes":{"block":"quote","heading":3}},{"insert":"And I’m a regular paragraph"},{"insert":"\n","attributes":{"block":"quote"}},{"insert":"Code blocks"},{"insert":"\n","attributes":{"headin'
19
+    r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
20
+
21
+Delta getDelta() {
22
+  return Delta.fromJson(json.decode(doc) as List);
23
+}
24
+
25
+class _ViewScreen extends State<ViewScreen> {
26
+  final doc = NotusDocument.fromDelta(getDelta());
27
+
28
+  @override
29
+  Widget build(BuildContext context) {
30
+    final theme = new ZefyrThemeData(
31
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
32
+        color: Colors.grey.shade800,
33
+        toggleColor: Colors.grey.shade900,
34
+        iconColor: Colors.white,
35
+        disabledIconColor: Colors.grey.shade500,
36
+      ),
37
+    );
38
+    return Scaffold(
39
+      resizeToAvoidBottomPadding: true,
40
+      appBar: AppBar(
41
+        elevation: 1.0,
42
+        backgroundColor: Colors.grey.shade200,
43
+        brightness: Brightness.light,
44
+        title: ZefyrLogo(),
45
+      ),
46
+      body: ZefyrTheme(
47
+        data: theme,
48
+        child: ListView(
49
+          children: <Widget>[
50
+            SizedBox(height: 16.0),
51
+            ListTile(
52
+              leading: Icon(Icons.info),
53
+              title: Text('ZefyrView inside ListView'),
54
+              subtitle: Text(
55
+                  'Allows embedding Notus documents in custom scrollables'),
56
+              trailing: Icon(Icons.keyboard_arrow_down),
57
+            ),
58
+            Padding(
59
+              padding: const EdgeInsets.all(16.0),
60
+              child: ZefyrView(
61
+                document: doc,
62
+                imageDelegate: new CustomImageDelegate(),
63
+              ),
64
+            )
65
+          ],
66
+        ),
67
+      ),
68
+    );
69
+  }
70
+}
71
+
72
+/// Custom image delegate used by this example to load image from application
73
+/// assets.
74
+///
75
+/// Default image delegate only supports [FileImage]s.
76
+class CustomImageDelegate extends ZefyrDefaultImageDelegate {
77
+  @override
78
+  Widget buildImage(BuildContext context, String imageSource) {
79
+    // We use custom "asset" scheme to distinguish asset images from other files.
80
+    if (imageSource.startsWith('asset://')) {
81
+      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
82
+      return new Image(image: asset);
83
+    } else {
84
+      return super.buildImage(context, imageSource);
85
+    }
86
+  }
87
+}

+ 9
- 9
packages/zefyr/lib/src/widgets/buttons.dart View File

7
 import 'package:notus/notus.dart';
7
 import 'package:notus/notus.dart';
8
 import 'package:url_launcher/url_launcher.dart';
8
 import 'package:url_launcher/url_launcher.dart';
9
 
9
 
10
-import 'editor.dart';
10
+import 'scope.dart';
11
 import 'theme.dart';
11
 import 'theme.dart';
12
 import 'toolbar.dart';
12
 import 'toolbar.dart';
13
 
13
 
94
     }
94
     }
95
   }
95
   }
96
 
96
 
97
-  Color _getColor(ZefyrEditorScope editor, ZefyrToolbarTheme theme) {
97
+  Color _getColor(ZefyrScope editor, ZefyrToolbarTheme theme) {
98
     if (isAttributeAction) {
98
     if (isAttributeAction) {
99
       final attribute = kZefyrToolbarAttributeActions[action];
99
       final attribute = kZefyrToolbarAttributeActions[action];
100
       final isToggled = (attribute is NotusAttribute)
100
       final isToggled = (attribute is NotusAttribute)
106
   }
106
   }
107
 
107
 
108
   VoidCallback _getPressedHandler(
108
   VoidCallback _getPressedHandler(
109
-      ZefyrEditorScope editor, ZefyrToolbarState toolbar) {
109
+      ZefyrScope editor, ZefyrToolbarState toolbar) {
110
     if (onPressed != null) {
110
     if (onPressed != null) {
111
       return onPressed;
111
       return onPressed;
112
     } else if (isAttributeAction) {
112
     } else if (isAttributeAction) {
123
     return null;
123
     return null;
124
   }
124
   }
125
 
125
 
126
-  void _toggleAttribute(NotusAttribute attribute, ZefyrEditorScope editor) {
126
+  void _toggleAttribute(NotusAttribute attribute, ZefyrScope editor) {
127
     final isToggled = editor.selectionStyle.containsSame(attribute);
127
     final isToggled = editor.selectionStyle.containsSame(attribute);
128
     if (isToggled) {
128
     if (isToggled) {
129
       editor.formatSelection(attribute.unset);
129
       editor.formatSelection(attribute.unset);
303
   final TextEditingController _inputController = TextEditingController();
303
   final TextEditingController _inputController = TextEditingController();
304
   Key _inputKey;
304
   Key _inputKey;
305
   bool _formatError = false;
305
   bool _formatError = false;
306
-  ZefyrEditorScope _editor;
306
+  ZefyrScope _editor;
307
 
307
 
308
   bool get isEditing => _inputKey != null;
308
   bool get isEditing => _inputKey != null;
309
 
309
 
491
 class _LinkInputState extends State<_LinkInput> {
491
 class _LinkInputState extends State<_LinkInput> {
492
   final FocusNode _focusNode = FocusNode();
492
   final FocusNode _focusNode = FocusNode();
493
 
493
 
494
-  ZefyrEditorScope _editor;
494
+  ZefyrScope _editor;
495
   bool _didAutoFocus = false;
495
   bool _didAutoFocus = false;
496
 
496
 
497
   @override
497
   @override
505
     final toolbar = ZefyrToolbar.of(context);
505
     final toolbar = ZefyrToolbar.of(context);
506
 
506
 
507
     if (_editor != toolbar.editor) {
507
     if (_editor != toolbar.editor) {
508
-      _editor?.setToolbarFocusNode(null);
508
+      _editor?.toolbarFocusNode = null;
509
       _editor = toolbar.editor;
509
       _editor = toolbar.editor;
510
-      _editor.setToolbarFocusNode(_focusNode);
510
+      _editor.toolbarFocusNode = _focusNode;
511
     }
511
     }
512
   }
512
   }
513
 
513
 
514
   @override
514
   @override
515
   void dispose() {
515
   void dispose() {
516
-    _editor?.setToolbarFocusNode(null);
516
+    _editor?.toolbarFocusNode = null;
517
     _focusNode.dispose();
517
     _focusNode.dispose();
518
     _editor = null;
518
     _editor = null;
519
     super.dispose();
519
     super.dispose();

+ 14
- 2
packages/zefyr/lib/src/widgets/caret.dart View File

5
 
5
 
6
 import 'package:flutter/material.dart';
6
 import 'package:flutter/material.dart';
7
 
7
 
8
-class CaretPainter {
8
+/// Helper class responsible for cursor layout and painting.
9
+class CursorPainter {
9
   static const double _kCaretHeightOffset = 2.0; // pixels
10
   static const double _kCaretHeightOffset = 2.0; // pixels
10
   static const double _kCaretWidth = 1.0; // pixels
11
   static const double _kCaretWidth = 1.0; // pixels
11
 
12
 
14
         0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset);
15
         0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset);
15
   }
16
   }
16
 
17
 
18
+  CursorPainter(Color color)
19
+      : assert(color != null),
20
+        _color = color;
21
+
17
   Rect _prototype;
22
   Rect _prototype;
18
 
23
 
19
   Rect get prototype => _prototype;
24
   Rect get prototype => _prototype;
20
 
25
 
26
+  Color _color;
27
+  Color get color => _color;
28
+  set color(Color value) {
29
+    assert(value != null);
30
+    _color = value;
31
+  }
32
+
21
   void layout(double lineHeight) {
33
   void layout(double lineHeight) {
22
     _prototype = buildPrototype(lineHeight);
34
     _prototype = buildPrototype(lineHeight);
23
   }
35
   }
24
 
36
 
25
   void paint(Canvas canvas, Offset offset) {
37
   void paint(Canvas canvas, Offset offset) {
26
-    final Paint paint = new Paint()..color = Colors.black;
38
+    final Paint paint = new Paint()..color = _color;
27
     final Rect caretRect = _prototype.shift(offset);
39
     final Rect caretRect = _prototype.shift(offset);
28
     canvas.drawRect(caretRect, paint);
40
     canvas.drawRect(caretRect, paint);
29
   }
41
   }

+ 25
- 41
packages/zefyr/lib/src/widgets/common.dart View File

6
 import 'package:notus/notus.dart';
6
 import 'package:notus/notus.dart';
7
 
7
 
8
 import 'editable_box.dart';
8
 import 'editable_box.dart';
9
-import 'editable_text.dart';
10
 import 'horizontal_rule.dart';
9
 import 'horizontal_rule.dart';
11
 import 'image.dart';
10
 import 'image.dart';
12
 import 'rich_text.dart';
11
 import 'rich_text.dart';
12
+import 'scope.dart';
13
 import 'theme.dart';
13
 import 'theme.dart';
14
 
14
 
15
 /// Raw widget representing a single line of rich text document in Zefyr editor.
15
 /// Raw widget representing a single line of rich text document in Zefyr editor.
43
 
43
 
44
   @override
44
   @override
45
   Widget build(BuildContext context) {
45
   Widget build(BuildContext context) {
46
-    ensureVisible(context);
46
+    final scope = ZefyrScope.of(context);
47
+    if (scope.isEditable) {
48
+      ensureVisible(context, scope);
49
+    }
47
     final theme = ZefyrTheme.of(context);
50
     final theme = ZefyrTheme.of(context);
48
-    final editable = ZefyrEditableText.of(context);
49
 
51
 
50
     Widget content;
52
     Widget content;
51
     if (widget.node.hasEmbed) {
53
     if (widget.node.hasEmbed) {
52
-      content = buildEmbed(context);
54
+      content = buildEmbed(context, scope);
53
     } else {
55
     } else {
54
       assert(widget.style != null);
56
       assert(widget.style != null);
55
-
56
-      final text = new EditableRichText(
57
+      content = ZefyrRichText(
57
         node: widget.node,
58
         node: widget.node,
58
         text: buildText(context),
59
         text: buildText(context),
59
       );
60
       );
60
-      content = new EditableBox(
61
-        child: text,
61
+    }
62
+
63
+    if (scope.isEditable) {
64
+      content = EditableBox(
65
+        child: content,
62
         node: widget.node,
66
         node: widget.node,
63
         layerLink: _link,
67
         layerLink: _link,
64
-        renderContext: editable.renderContext,
65
-        showCursor: editable.showCursor,
66
-        selection: editable.selection,
68
+        renderContext: scope.renderContext,
69
+        showCursor: scope.showCursor,
70
+        selection: scope.selection,
67
         selectionColor: theme.selectionColor,
71
         selectionColor: theme.selectionColor,
72
+        cursorColor: theme.cursorColor,
68
       );
73
       );
74
+      content = CompositedTransformTarget(link: _link, child: content);
69
     }
75
     }
70
 
76
 
71
-    final result = new CompositedTransformTarget(link: _link, child: content);
72
     if (widget.padding != null) {
77
     if (widget.padding != null) {
73
-      return new Padding(padding: widget.padding, child: result);
78
+      return Padding(padding: widget.padding, child: content);
74
     }
79
     }
75
-    return result;
80
+    return content;
76
   }
81
   }
77
 
82
 
78
-  void ensureVisible(BuildContext context) {
79
-    final editable = ZefyrEditableText.of(context);
80
-    if (editable.selection.isCollapsed &&
81
-        widget.node.containsOffset(editable.selection.extentOffset)) {
83
+  void ensureVisible(BuildContext context, ZefyrScope scope) {
84
+    if (scope.selection.isCollapsed &&
85
+        widget.node.containsOffset(scope.selection.extentOffset)) {
82
       WidgetsBinding.instance.addPostFrameCallback((_) {
86
       WidgetsBinding.instance.addPostFrameCallback((_) {
83
         bringIntoView(context);
87
         bringIntoView(context);
84
       });
88
       });
136
     return result;
140
     return result;
137
   }
141
   }
138
 
142
 
139
-  Widget buildEmbed(BuildContext context) {
140
-    final theme = ZefyrTheme.of(context);
141
-    final editable = ZefyrEditableText.of(context);
142
-
143
+  Widget buildEmbed(BuildContext context, ZefyrScope scope) {
143
     EmbedNode node = widget.node.children.single;
144
     EmbedNode node = widget.node.children.single;
144
     EmbedAttribute embed = node.style.get(NotusAttribute.embed);
145
     EmbedAttribute embed = node.style.get(NotusAttribute.embed);
145
 
146
 
146
     if (embed.type == EmbedType.horizontalRule) {
147
     if (embed.type == EmbedType.horizontalRule) {
147
-      final hr = new ZefyrHorizontalRule(node: node);
148
-      return new EditableBox(
149
-        child: hr,
150
-        node: widget.node,
151
-        layerLink: _link,
152
-        renderContext: editable.renderContext,
153
-        showCursor: editable.showCursor,
154
-        selection: editable.selection,
155
-        selectionColor: theme.selectionColor,
156
-      );
148
+      return ZefyrHorizontalRule(node: node);
157
     } else if (embed.type == EmbedType.image) {
149
     } else if (embed.type == EmbedType.image) {
158
-      return new EditableBox(
159
-        child: ZefyrImage(node: node, delegate: editable.imageDelegate),
160
-        node: widget.node,
161
-        layerLink: _link,
162
-        renderContext: editable.renderContext,
163
-        showCursor: editable.showCursor,
164
-        selection: editable.selection,
165
-        selectionColor: theme.selectionColor,
166
-      );
150
+      return ZefyrImage(node: node, delegate: scope.imageDelegate);
167
     } else {
151
     } else {
168
       throw new UnimplementedError('Unimplemented embed type ${embed.type}');
152
       throw new UnimplementedError('Unimplemented embed type ${embed.type}');
169
     }
153
     }

+ 42
- 0
packages/zefyr/lib/src/widgets/cursor_timer.dart View File

1
+import 'dart:async';
2
+
3
+import 'package:flutter/material.dart';
4
+
5
+/// Helper class that keeps state relevant to the editing cursor.
6
+class CursorTimer {
7
+  static const _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
8
+
9
+  Timer _timer;
10
+  final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
11
+
12
+  ValueNotifier<bool> get value => _showCursor;
13
+
14
+  void _cursorTick(Timer timer) {
15
+    _showCursor.value = !_showCursor.value;
16
+  }
17
+
18
+  /// Starts cursor timer.
19
+  void start() {
20
+    _showCursor.value = true;
21
+    _timer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
22
+  }
23
+
24
+  /// Stops cursor timer.
25
+  void stop() {
26
+    _timer?.cancel();
27
+    _timer = null;
28
+    _showCursor.value = false;
29
+  }
30
+
31
+  /// Starts or stops cursor timer based on current state of [focusNode]
32
+  /// and [selection].
33
+  void startOrStop(FocusNode focusNode, TextSelection selection) {
34
+    final hasFocus = focusNode.hasFocus;
35
+    final selectionCollapsed = selection.isCollapsed;
36
+    if (_timer == null && hasFocus && selectionCollapsed) {
37
+      start();
38
+    } else if (_timer != null && (!hasFocus || !selectionCollapsed)) {
39
+      stop();
40
+    }
41
+  }
42
+}

+ 42
- 13
packages/zefyr/lib/src/widgets/editable_box.dart View File

20
     @required this.showCursor,
20
     @required this.showCursor,
21
     @required this.selection,
21
     @required this.selection,
22
     @required this.selectionColor,
22
     @required this.selectionColor,
23
+    @required this.cursorColor,
23
   }) : super(child: child);
24
   }) : super(child: child);
24
 
25
 
25
   final ContainerNode node;
26
   final ContainerNode node;
28
   final ValueNotifier<bool> showCursor;
29
   final ValueNotifier<bool> showCursor;
29
   final TextSelection selection;
30
   final TextSelection selection;
30
   final Color selectionColor;
31
   final Color selectionColor;
32
+  final Color cursorColor;
31
 
33
 
32
   @override
34
   @override
33
   RenderEditableProxyBox createRenderObject(BuildContext context) {
35
   RenderEditableProxyBox createRenderObject(BuildContext context) {
38
       showCursor: showCursor,
40
       showCursor: showCursor,
39
       selection: selection,
41
       selection: selection,
40
       selectionColor: selectionColor,
42
       selectionColor: selectionColor,
43
+      cursorColor: cursorColor,
41
     );
44
     );
42
   }
45
   }
43
 
46
 
50
       ..renderContext = renderContext
53
       ..renderContext = renderContext
51
       ..showCursor = showCursor
54
       ..showCursor = showCursor
52
       ..selection = selection
55
       ..selection = selection
53
-      ..selectionColor = selectionColor;
56
+      ..selectionColor = selectionColor
57
+      ..cursorColor = cursorColor;
54
   }
58
   }
55
 }
59
 }
56
 
60
 
67
     @required ValueNotifier<bool> showCursor,
71
     @required ValueNotifier<bool> showCursor,
68
     @required TextSelection selection,
72
     @required TextSelection selection,
69
     @required Color selectionColor,
73
     @required Color selectionColor,
74
+    @required Color cursorColor,
70
   })  : _node = node,
75
   })  : _node = node,
71
         _layerLink = layerLink,
76
         _layerLink = layerLink,
72
         _renderContext = renderContext,
77
         _renderContext = renderContext,
75
         _selectionColor = selectionColor,
80
         _selectionColor = selectionColor,
76
         super() {
81
         super() {
77
     this.child = child;
82
     this.child = child;
83
+    _cursorPainter = CursorPainter(cursorColor);
78
   }
84
   }
79
 
85
 
86
+  CursorPainter _cursorPainter;
87
+
88
+  set cursorColor(Color value) {
89
+    if (_cursorPainter.color != value) {
90
+      _cursorPainter.color = value;
91
+      markNeedsPaint();
92
+    }
93
+  }
94
+
95
+  bool _isDirty = true;
96
+
80
   ContainerNode get node => _node;
97
   ContainerNode get node => _node;
81
   ContainerNode _node;
98
   ContainerNode _node;
82
   void set node(ContainerNode value) {
99
   void set node(ContainerNode value) {
102
   set showCursor(ValueNotifier<bool> value) {
119
   set showCursor(ValueNotifier<bool> value) {
103
     assert(value != null);
120
     assert(value != null);
104
     if (_showCursor == value) return;
121
     if (_showCursor == value) return;
105
-    if (attached) _showCursor.removeListener(markNeedsPaint);
122
+    if (attached) _showCursor.removeListener(markNeedsCursorPaint);
106
     _showCursor = value;
123
     _showCursor = value;
107
-    if (attached) _showCursor.addListener(markNeedsPaint);
124
+    if (attached) _showCursor.addListener(markNeedsCursorPaint);
108
     markNeedsPaint();
125
     markNeedsPaint();
109
   }
126
   }
110
 
127
 
130
   /// Returns `true` if current selection is collapsed, located within
147
   /// Returns `true` if current selection is collapsed, located within
131
   /// this paragraph and is visible according to tick timer.
148
   /// this paragraph and is visible according to tick timer.
132
   bool get isCaretVisible {
149
   bool get isCaretVisible {
150
+    return _showCursor.value && containsCaret;
151
+  }
152
+
153
+  /// Returns `true` if current selection is collapsed and located
154
+  /// within this paragraph.
155
+  bool get containsCaret {
133
     if (!_selection.isCollapsed) return false;
156
     if (!_selection.isCollapsed) return false;
134
-    if (!_showCursor.value) return false;
135
 
157
 
136
     final int start = node.documentOffset;
158
     final int start = node.documentOffset;
137
     final int end = start + node.length;
159
     final int end = start + node.length;
146
     return intersectsWithSelection(_selection);
168
     return intersectsWithSelection(_selection);
147
   }
169
   }
148
 
170
 
171
+  void markNeedsCursorPaint() {
172
+    if (containsCaret) {
173
+      markNeedsPaint();
174
+    }
175
+  }
176
+
149
   //
177
   //
150
   // Overridden members of RenderBox
178
   // Overridden members of RenderBox
151
   //
179
   //
153
   @override
181
   @override
154
   void attach(PipelineOwner owner) {
182
   void attach(PipelineOwner owner) {
155
     super.attach(owner);
183
     super.attach(owner);
156
-    _showCursor.addListener(markNeedsPaint);
184
+    _showCursor.addListener(markNeedsCursorPaint);
157
     _renderContext.addBox(this);
185
     _renderContext.addBox(this);
186
+    _renderContext.markDirty(this, _isDirty);
158
   }
187
   }
159
 
188
 
160
   @override
189
   @override
161
   void detach() {
190
   void detach() {
162
-    _showCursor.removeListener(markNeedsPaint);
191
+    _showCursor.removeListener(markNeedsCursorPaint);
163
     _renderContext.removeBox(this);
192
     _renderContext.removeBox(this);
164
     super.detach();
193
     super.detach();
165
   }
194
   }
168
   @mustCallSuper
197
   @mustCallSuper
169
   void performLayout() {
198
   void performLayout() {
170
     super.performLayout();
199
     super.performLayout();
171
-    _caretPainter.layout(preferredLineHeight);
200
+    _cursorPainter.layout(preferredLineHeight);
172
     // Indicate to render context that this object can be used by other
201
     // Indicate to render context that this object can be used by other
173
     // layers (selection overlay, for instance).
202
     // layers (selection overlay, for instance).
203
+    _isDirty = false;
174
     _renderContext.markDirty(this, false);
204
     _renderContext.markDirty(this, false);
175
   }
205
   }
176
 
206
 
177
   @override
207
   @override
178
   void markNeedsLayout() {
208
   void markNeedsLayout() {
179
     // Temporarily remove this object from the render context.
209
     // Temporarily remove this object from the render context.
210
+    _isDirty = true;
180
     _renderContext.markDirty(this, true);
211
     _renderContext.markDirty(this, true);
181
     super.markNeedsLayout();
212
     super.markNeedsLayout();
182
   }
213
   }
191
       paintSelection(context, offset, selection, selectionColor);
222
       paintSelection(context, offset, selection, selectionColor);
192
     }
223
     }
193
     if (isCaretVisible) {
224
     if (isCaretVisible) {
194
-      _paintCaret(context, offset);
225
+      _paintCursor(context, offset);
195
     }
226
     }
196
   }
227
   }
197
 
228
 
198
-  final CaretPainter _caretPainter = new CaretPainter();
199
-
200
-  void _paintCaret(PaintingContext context, Offset offset) {
229
+  void _paintCursor(PaintingContext context, Offset offset) {
201
     Offset caretOffset =
230
     Offset caretOffset =
202
-        getOffsetForCaret(_selection.extent, _caretPainter.prototype);
203
-    _caretPainter.paint(context.canvas, caretOffset + offset);
231
+        getOffsetForCaret(_selection.extent, _cursorPainter.prototype);
232
+    _cursorPainter.paint(context.canvas, caretOffset + offset);
204
   }
233
   }
205
 
234
 
206
   @override
235
   @override

+ 40
- 104
packages/zefyr/lib/src/widgets/editable_text.dart View File

1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
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
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.
3
 // BSD-style license that can be found in the LICENSE file.
4
-import 'dart:async';
5
-
6
-import 'package:collection/collection.dart';
7
 import 'package:flutter/cupertino.dart';
4
 import 'package:flutter/cupertino.dart';
8
 import 'package:flutter/widgets.dart';
5
 import 'package:flutter/widgets.dart';
9
 import 'package:notus/notus.dart';
6
 import 'package:notus/notus.dart';
11
 import 'code.dart';
8
 import 'code.dart';
12
 import 'common.dart';
9
 import 'common.dart';
13
 import 'controller.dart';
10
 import 'controller.dart';
14
-import 'editable_box.dart';
11
+import 'cursor_timer.dart';
15
 import 'editor.dart';
12
 import 'editor.dart';
16
 import 'image.dart';
13
 import 'image.dart';
17
 import 'input.dart';
14
 import 'input.dart';
19
 import 'paragraph.dart';
16
 import 'paragraph.dart';
20
 import 'quote.dart';
17
 import 'quote.dart';
21
 import 'render_context.dart';
18
 import 'render_context.dart';
19
+import 'scope.dart';
22
 import 'selection.dart';
20
 import 'selection.dart';
21
+import 'theme.dart';
23
 
22
 
24
 /// Core widget responsible for editing Zefyr documents.
23
 /// Core widget responsible for editing Zefyr documents.
25
 ///
24
 ///
26
-/// Depends on presence of [ZefyrTheme] somewhere up the widget tree.
25
+/// Depends on presence of [ZefyrTheme] and [ZefyrScope] somewhere up the
26
+/// widget tree.
27
 ///
27
 ///
28
 /// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to
28
 /// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to
29
 /// edit style attributes.
29
 /// edit style attributes.
49
   /// Padding around editable area.
49
   /// Padding around editable area.
50
   final EdgeInsets padding;
50
   final EdgeInsets padding;
51
 
51
 
52
-  static ZefyrEditableTextScope of(BuildContext context) {
53
-    final ZefyrEditableTextScope result =
54
-        context.inheritFromWidgetOfExactType(ZefyrEditableTextScope);
55
-    return result;
56
-  }
57
-
58
   @override
52
   @override
59
   _ZefyrEditableTextState createState() => new _ZefyrEditableTextState();
53
   _ZefyrEditableTextState createState() => new _ZefyrEditableTextState();
60
 }
54
 }
61
 
55
 
62
-/// Provides access to shared state of [ZefyrEditableText].
63
-class ZefyrEditableTextScope extends InheritedWidget {
64
-  static const _kEquality = const SetEquality<RenderEditableBox>();
65
-
66
-  ZefyrEditableTextScope({
67
-    Key key,
68
-    @required Widget child,
69
-    @required this.selection,
70
-    @required this.showCursor,
71
-    @required this.renderContext,
72
-    @required this.imageDelegate,
73
-  })  : _activeBoxes = new Set.from(renderContext.active),
74
-        super(key: key, child: child);
75
-
76
-  final TextSelection selection;
77
-  final ValueNotifier<bool> showCursor;
78
-  final ZefyrRenderContext renderContext;
79
-  final ZefyrImageDelegate imageDelegate;
80
-  final Set<RenderEditableBox> _activeBoxes;
81
-
82
-  @override
83
-  bool updateShouldNotify(ZefyrEditableTextScope oldWidget) {
84
-    return selection != oldWidget.selection ||
85
-        showCursor != oldWidget.showCursor ||
86
-        imageDelegate != oldWidget.imageDelegate ||
87
-        !_kEquality.equals(_activeBoxes, oldWidget._activeBoxes);
88
-  }
89
-}
90
-
91
 class _ZefyrEditableTextState extends State<ZefyrEditableText>
56
 class _ZefyrEditableTextState extends State<ZefyrEditableText>
92
     with AutomaticKeepAliveClientMixin {
57
     with AutomaticKeepAliveClientMixin {
93
   //
58
   //
102
 
67
 
103
   /// Current text selection.
68
   /// Current text selection.
104
   TextSelection get selection => widget.controller.selection;
69
   TextSelection get selection => widget.controller.selection;
105
-  ZefyrRenderContext get renderContext => _renderContext;
106
-  ValueNotifier<bool> get showCursor => _cursorTimer.value;
107
 
70
 
108
   /// Express interest in interacting with the keyboard.
71
   /// Express interest in interacting with the keyboard.
109
   ///
72
   ///
119
       FocusScope.of(context).requestFocus(focusNode);
82
       FocusScope.of(context).requestFocus(focusNode);
120
   }
83
   }
121
 
84
 
85
+  void focusOrUnfocusIfNeeded() {
86
+    if (!_didAutoFocus && widget.autofocus && widget.enabled) {
87
+      FocusScope.of(context).autofocus(focusNode);
88
+      _didAutoFocus = true;
89
+    }
90
+    if (!widget.enabled && focusNode.hasFocus) {
91
+      _didAutoFocus = false;
92
+      focusNode.unfocus();
93
+    }
94
+  }
95
+
122
   //
96
   //
123
   // Overridden members of State
97
   // Overridden members of State
124
   //
98
   //
127
   Widget build(BuildContext context) {
101
   Widget build(BuildContext context) {
128
     FocusScope.of(context).reparentIfNeeded(focusNode);
102
     FocusScope.of(context).reparentIfNeeded(focusNode);
129
     super.build(context); // See AutomaticKeepAliveState.
103
     super.build(context); // See AutomaticKeepAliveState.
130
-    ZefyrEditor.of(context);
131
 
104
 
132
     Widget body = ListBody(children: _buildChildren(context));
105
     Widget body = ListBody(children: _buildChildren(context));
133
     if (widget.padding != null) {
106
     if (widget.padding != null) {
149
       ));
122
       ));
150
     }
123
     }
151
 
124
 
152
-    return new ZefyrEditableTextScope(
153
-      selection: selection,
154
-      showCursor: showCursor,
155
-      renderContext: renderContext,
156
-      imageDelegate: widget.imageDelegate,
157
-      child: Stack(fit: StackFit.expand, children: layers),
158
-    );
125
+    return Stack(fit: StackFit.expand, children: layers);
159
   }
126
   }
160
 
127
 
161
   @override
128
   @override
169
   void didUpdateWidget(ZefyrEditableText oldWidget) {
136
   void didUpdateWidget(ZefyrEditableText oldWidget) {
170
     super.didUpdateWidget(oldWidget);
137
     super.didUpdateWidget(oldWidget);
171
     _updateSubscriptions(oldWidget);
138
     _updateSubscriptions(oldWidget);
172
-    if (!_didAutoFocus && widget.autofocus && widget.enabled) {
173
-      FocusScope.of(context).autofocus(focusNode);
174
-      _didAutoFocus = true;
139
+    focusOrUnfocusIfNeeded();
140
+  }
141
+
142
+  @override
143
+  void didChangeDependencies() {
144
+    super.didChangeDependencies();
145
+    final scope = ZefyrScope.of(context);
146
+    if (_renderContext != scope.renderContext) {
147
+      _renderContext?.removeListener(_handleRenderContextChange);
148
+      _renderContext = scope.renderContext;
149
+      _renderContext.addListener(_handleRenderContextChange);
175
     }
150
     }
176
-    if (!widget.enabled && focusNode.hasFocus) {
177
-      _didAutoFocus = false;
178
-      focusNode.unfocus();
151
+    if (_cursorTimer != scope.cursorTimer) {
152
+      _cursorTimer?.stop();
153
+      _cursorTimer = scope.cursorTimer;
154
+      _cursorTimer.startOrStop(focusNode, selection);
179
     }
155
     }
156
+    focusOrUnfocusIfNeeded();
180
   }
157
   }
181
 
158
 
182
   @override
159
   @override
196
   // Private members
173
   // Private members
197
   //
174
   //
198
 
175
 
199
-  final ScrollController _scrollController = new ScrollController();
200
-  final ZefyrRenderContext _renderContext = new ZefyrRenderContext();
201
-  final _CursorTimer _cursorTimer = new _CursorTimer();
176
+  final ScrollController _scrollController = ScrollController();
177
+  ZefyrRenderContext _renderContext;
178
+  CursorTimer _cursorTimer;
202
   InputConnectionController _input;
179
   InputConnectionController _input;
203
   bool _didAutoFocus = false;
180
   bool _didAutoFocus = false;
204
 
181
 
223
     final BlockNode block = node;
200
     final BlockNode block = node;
224
     final blockStyle = block.style.get(NotusAttribute.block);
201
     final blockStyle = block.style.get(NotusAttribute.block);
225
     if (blockStyle == NotusAttribute.block.code) {
202
     if (blockStyle == NotusAttribute.block.code) {
226
-      return new ZefyrCode(node: node);
203
+      return new ZefyrCode(node: block);
227
     } else if (blockStyle == NotusAttribute.block.bulletList) {
204
     } else if (blockStyle == NotusAttribute.block.bulletList) {
228
-      return new ZefyrList(node: node);
205
+      return new ZefyrList(node: block);
229
     } else if (blockStyle == NotusAttribute.block.numberList) {
206
     } else if (blockStyle == NotusAttribute.block.numberList) {
230
-      return new ZefyrList(node: node);
207
+      return new ZefyrList(node: block);
231
     } else if (blockStyle == NotusAttribute.block.quote) {
208
     } else if (blockStyle == NotusAttribute.block.quote) {
232
-      return new ZefyrQuote(node: node);
209
+      return new ZefyrQuote(node: block);
233
     }
210
     }
234
 
211
 
235
     throw new UnimplementedError('Block format $blockStyle.');
212
     throw new UnimplementedError('Block format $blockStyle.');
237
 
214
 
238
   void _updateSubscriptions([ZefyrEditableText oldWidget]) {
215
   void _updateSubscriptions([ZefyrEditableText oldWidget]) {
239
     if (oldWidget == null) {
216
     if (oldWidget == null) {
240
-      _renderContext.addListener(_handleRenderContextChange);
241
       widget.controller.addListener(_handleLocalValueChange);
217
       widget.controller.addListener(_handleLocalValueChange);
242
       focusNode.addListener(_handleFocusChange);
218
       focusNode.addListener(_handleFocusChange);
243
       return;
219
       return;
257
 
233
 
258
   void _cancelSubscriptions() {
234
   void _cancelSubscriptions() {
259
     _renderContext.removeListener(_handleRenderContextChange);
235
     _renderContext.removeListener(_handleRenderContextChange);
260
-    _renderContext.dispose();
261
     widget.controller.removeListener(_handleLocalValueChange);
236
     widget.controller.removeListener(_handleLocalValueChange);
262
     focusNode.removeListener(_handleFocusChange);
237
     focusNode.removeListener(_handleFocusChange);
263
     _input.closeConnection();
238
     _input.closeConnection();
297
     });
272
     });
298
   }
273
   }
299
 }
274
 }
300
-
301
-/// Helper class that keeps state relevant to the cursor.
302
-class _CursorTimer {
303
-  static const _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
304
-
305
-  Timer _timer;
306
-  final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
307
-
308
-  ValueNotifier<bool> get value => _showCursor;
309
-
310
-  void _cursorTick(Timer timer) {
311
-    _showCursor.value = !_showCursor.value;
312
-  }
313
-
314
-  /// Starts cursor timer.
315
-  void start() {
316
-    _showCursor.value = true;
317
-    _timer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
318
-  }
319
-
320
-  /// Stops cursor timer.
321
-  void stop() {
322
-    _timer?.cancel();
323
-    _timer = null;
324
-    _showCursor.value = false;
325
-  }
326
-
327
-  /// Starts or stops cursor timer based on current state of [focusNode]
328
-  /// and [selection].
329
-  void startOrStop(FocusNode focusNode, TextSelection selection) {
330
-    final hasFocus = focusNode.hasFocus;
331
-    final selectionCollapsed = selection.isCollapsed;
332
-    if (_timer == null && hasFocus && selectionCollapsed) {
333
-      start();
334
-    } else if (_timer != null && (!hasFocus || !selectionCollapsed)) {
335
-      stop();
336
-    }
337
-  }
338
-}

+ 13
- 167
packages/zefyr/lib/src/widgets/editor.dart View File

2
 // for details. All rights reserved. Use of this source code is governed by a
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.
3
 // BSD-style license that can be found in the LICENSE file.
4
 import 'package:flutter/widgets.dart';
4
 import 'package:flutter/widgets.dart';
5
-import 'package:notus/notus.dart';
6
 
5
 
7
 import 'controller.dart';
6
 import 'controller.dart';
8
 import 'editable_text.dart';
7
 import 'editable_text.dart';
9
 import 'image.dart';
8
 import 'image.dart';
10
 import 'scaffold.dart';
9
 import 'scaffold.dart';
10
+import 'scope.dart';
11
 import 'theme.dart';
11
 import 'theme.dart';
12
 import 'toolbar.dart';
12
 import 'toolbar.dart';
13
 
13
 
14
-class ZefyrEditorScope extends ChangeNotifier {
15
-  ZefyrEditorScope({
16
-    @required ZefyrImageDelegate imageDelegate,
17
-    @required ZefyrController controller,
18
-    @required FocusNode focusNode,
19
-    @required FocusScopeNode focusScope,
20
-  })  : _controller = controller,
21
-        _imageDelegate = imageDelegate,
22
-        _focusScope = focusScope,
23
-        _focusNode = focusNode {
24
-    _selectionStyle = _controller.getSelectionStyle();
25
-    _selection = _controller.selection;
26
-    _controller.addListener(_handleControllerChange);
27
-    _focusNode.addListener(_handleFocusChange);
28
-  }
29
-
30
-  bool _disposed = false;
31
-
32
-  ZefyrImageDelegate _imageDelegate;
33
-  ZefyrImageDelegate get imageDelegate => _imageDelegate;
34
-
35
-  FocusScopeNode _focusScope;
36
-  FocusNode _focusNode;
37
-
38
-  ZefyrController _controller;
39
-  NotusStyle get selectionStyle => _selectionStyle;
40
-  NotusStyle _selectionStyle;
41
-  TextSelection get selection => _selection;
42
-  TextSelection _selection;
43
-
44
-  @override
45
-  void dispose() {
46
-    assert(!_disposed);
47
-    _controller.removeListener(_handleControllerChange);
48
-    _focusNode.removeListener(_handleFocusChange);
49
-    _disposed = true;
50
-    super.dispose();
51
-  }
52
-
53
-  void _updateControllerIfNeeded(ZefyrController value) {
54
-    if (_controller != value) {
55
-      _controller.removeListener(_handleControllerChange);
56
-      _controller = value;
57
-      _selectionStyle = _controller.getSelectionStyle();
58
-      _selection = _controller.selection;
59
-      _controller.addListener(_handleControllerChange);
60
-      notifyListeners();
61
-    }
62
-  }
63
-
64
-  void _updateFocusNodeIfNeeded(FocusNode value) {
65
-    if (_focusNode != value) {
66
-      _focusNode.removeListener(_handleFocusChange);
67
-      _focusNode = value;
68
-      _focusNode.addListener(_handleFocusChange);
69
-      notifyListeners();
70
-    }
71
-  }
72
-
73
-  void _updateImageDelegateIfNeeded(ZefyrImageDelegate value) {
74
-    if (_imageDelegate != value) {
75
-      _imageDelegate = value;
76
-      notifyListeners();
77
-    }
78
-  }
79
-
80
-  void _handleControllerChange() {
81
-    assert(!_disposed);
82
-    final attrs = _controller.getSelectionStyle();
83
-    final selection = _controller.selection;
84
-    if (_selectionStyle != attrs || _selection != selection) {
85
-      _selectionStyle = attrs;
86
-      _selection = _controller.selection;
87
-      notifyListeners();
88
-    }
89
-  }
90
-
91
-  void _handleFocusChange() {
92
-    assert(!_disposed);
93
-    if (focusOwner == FocusOwner.none && !_selection.isCollapsed) {
94
-      // Collapse selection if there is nothing focused.
95
-      _controller.updateSelection(_selection.copyWith(
96
-        baseOffset: _selection.extentOffset,
97
-        extentOffset: _selection.extentOffset,
98
-      ));
99
-    }
100
-    notifyListeners();
101
-  }
102
-
103
-  FocusNode _toolbarFocusNode;
104
-
105
-  void setToolbarFocusNode(FocusNode node) {
106
-    assert(!_disposed || node == null);
107
-    if (_toolbarFocusNode != node) {
108
-      _toolbarFocusNode?.removeListener(_handleFocusChange);
109
-      _toolbarFocusNode = node;
110
-      _toolbarFocusNode?.addListener(_handleFocusChange);
111
-      // We do not notify listeners here because it will happen when
112
-      // focus state changes, see [_handleFocusChange].
113
-    }
114
-  }
115
-
116
-  FocusOwner get focusOwner {
117
-    assert(!_disposed);
118
-    if (_focusNode.hasFocus) {
119
-      return FocusOwner.editor;
120
-    } else if (_toolbarFocusNode?.hasFocus == true) {
121
-      return FocusOwner.toolbar;
122
-    } else {
123
-      return FocusOwner.none;
124
-    }
125
-  }
126
-
127
-  void updateSelection(TextSelection value,
128
-      {ChangeSource source: ChangeSource.remote}) {
129
-    assert(!_disposed);
130
-    _controller.updateSelection(value, source: source);
131
-  }
132
-
133
-  void formatSelection(NotusAttribute value) {
134
-    assert(!_disposed);
135
-    _controller.formatSelection(value);
136
-  }
137
-
138
-  void focus() {
139
-    assert(!_disposed);
140
-    _focusScope.requestFocus(_focusNode);
141
-  }
142
-
143
-  void hideKeyboard() {
144
-    assert(!_disposed);
145
-    _focusNode.unfocus();
146
-  }
147
-}
148
-
149
-class _ZefyrEditorScope extends InheritedWidget {
150
-  final ZefyrEditorScope scope;
151
-
152
-  _ZefyrEditorScope({Key key, Widget child, @required this.scope})
153
-      : super(key: key, child: child);
154
-
155
-  @override
156
-  bool updateShouldNotify(_ZefyrEditorScope oldWidget) {
157
-    return oldWidget.scope != scope;
158
-  }
159
-}
160
-
161
 /// Widget for editing Zefyr documents.
14
 /// Widget for editing Zefyr documents.
162
 class ZefyrEditor extends StatefulWidget {
15
 class ZefyrEditor extends StatefulWidget {
163
   const ZefyrEditor({
16
   const ZefyrEditor({
183
   /// Padding around editable area.
36
   /// Padding around editable area.
184
   final EdgeInsets padding;
37
   final EdgeInsets padding;
185
 
38
 
186
-  static ZefyrEditorScope of(BuildContext context) {
187
-    _ZefyrEditorScope widget =
188
-        context.inheritFromWidgetOfExactType(_ZefyrEditorScope);
189
-    return widget.scope;
190
-  }
191
-
192
   @override
39
   @override
193
   _ZefyrEditorState createState() => new _ZefyrEditorState();
40
   _ZefyrEditorState createState() => new _ZefyrEditorState();
194
 }
41
 }
195
 
42
 
196
 class _ZefyrEditorState extends State<ZefyrEditor> {
43
 class _ZefyrEditorState extends State<ZefyrEditor> {
197
   ZefyrImageDelegate _imageDelegate;
44
   ZefyrImageDelegate _imageDelegate;
198
-  ZefyrEditorScope _scope;
45
+  ZefyrScope _scope;
199
   ZefyrThemeData _themeData;
46
   ZefyrThemeData _themeData;
200
   GlobalKey<ZefyrToolbarState> _toolbarKey;
47
   GlobalKey<ZefyrToolbarState> _toolbarKey;
201
   ZefyrScaffoldState _scaffold;
48
   ZefyrScaffoldState _scaffold;
214
     _toolbarKey = null;
61
     _toolbarKey = null;
215
   }
62
   }
216
 
63
 
217
-  Widget buildToolbar(BuildContext) {
64
+  Widget buildToolbar(BuildContext context) {
218
     return ZefyrTheme(
65
     return ZefyrTheme(
219
       data: _themeData,
66
       data: _themeData,
220
       child: ZefyrToolbar(
67
       child: ZefyrToolbar(
231
     } else if (!hasToolbar) {
78
     } else if (!hasToolbar) {
232
       showToolbar();
79
       showToolbar();
233
     } else {
80
     } else {
81
+      // TODO: is there a nicer way to do this?
234
       WidgetsBinding.instance.addPostFrameCallback((_) {
82
       WidgetsBinding.instance.addPostFrameCallback((_) {
235
         _toolbarKey?.currentState?.markNeedsRebuild();
83
         _toolbarKey?.currentState?.markNeedsRebuild();
236
       });
84
       });
246
   @override
94
   @override
247
   void didUpdateWidget(ZefyrEditor oldWidget) {
95
   void didUpdateWidget(ZefyrEditor oldWidget) {
248
     super.didUpdateWidget(oldWidget);
96
     super.didUpdateWidget(oldWidget);
249
-    _scope._updateControllerIfNeeded(widget.controller);
250
-    _scope._updateFocusNodeIfNeeded(widget.focusNode);
97
+    _scope.controller = widget.controller;
98
+    _scope.focusNode = widget.focusNode;
251
     if (widget.imageDelegate != oldWidget.imageDelegate) {
99
     if (widget.imageDelegate != oldWidget.imageDelegate) {
252
       _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
100
       _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
253
-      _scope._updateImageDelegateIfNeeded(_imageDelegate);
101
+      _scope.imageDelegate = _imageDelegate;
254
     }
102
     }
255
   }
103
   }
256
 
104
 
264
         : fallbackTheme;
112
         : fallbackTheme;
265
 
113
 
266
     if (_scope == null) {
114
     if (_scope == null) {
267
-      _scope = ZefyrEditorScope(
115
+      _scope = ZefyrScope.editable(
268
         imageDelegate: _imageDelegate,
116
         imageDelegate: _imageDelegate,
269
         controller: widget.controller,
117
         controller: widget.controller,
270
         focusNode: widget.focusNode,
118
         focusNode: widget.focusNode,
273
       _scope.addListener(_handleChange);
121
       _scope.addListener(_handleChange);
274
     } else {
122
     } else {
275
       final focusScope = FocusScope.of(context);
123
       final focusScope = FocusScope.of(context);
276
-      if (focusScope != _scope._focusScope) {
277
-        _scope._focusScope = focusScope;
278
-      }
124
+      _scope.focusScope = focusScope;
279
     }
125
     }
280
 
126
 
281
     final scaffold = ZefyrScaffold.of(context);
127
     final scaffold = ZefyrScaffold.of(context);
298
   @override
144
   @override
299
   Widget build(BuildContext context) {
145
   Widget build(BuildContext context) {
300
     Widget editable = new ZefyrEditableText(
146
     Widget editable = new ZefyrEditableText(
301
-      controller: widget.controller,
302
-      focusNode: widget.focusNode,
303
-      imageDelegate: _imageDelegate,
147
+      controller: _scope.controller,
148
+      focusNode: _scope.focusNode,
149
+      imageDelegate: _scope.imageDelegate,
304
       autofocus: widget.autofocus,
150
       autofocus: widget.autofocus,
305
       enabled: widget.enabled,
151
       enabled: widget.enabled,
306
       padding: widget.padding,
152
       padding: widget.padding,
309
 
155
 
310
     return ZefyrTheme(
156
     return ZefyrTheme(
311
       data: _themeData,
157
       data: _themeData,
312
-      child: _ZefyrEditorScope(
158
+      child: ZefyrScopeAccess(
313
         scope: _scope,
159
         scope: _scope,
314
         child: editable,
160
         child: editable,
315
       ),
161
       ),

+ 1
- 1
packages/zefyr/lib/src/widgets/image.dart View File

54
 class _ZefyrImageState extends State<ZefyrImage> {
54
 class _ZefyrImageState extends State<ZefyrImage> {
55
   String get imageSource {
55
   String get imageSource {
56
     EmbedAttribute attribute = widget.node.style.get(NotusAttribute.embed);
56
     EmbedAttribute attribute = widget.node.style.get(NotusAttribute.embed);
57
-    return attribute.value['source'];
57
+    return attribute.value['source'] as String;
58
   }
58
   }
59
 
59
 
60
   @override
60
   @override

+ 2
- 2
packages/zefyr/lib/src/widgets/quote.dart View File

36
 
36
 
37
     Widget content;
37
     Widget content;
38
     if (line.style.contains(NotusAttribute.heading)) {
38
     if (line.style.contains(NotusAttribute.heading)) {
39
-      content = new ZefyrHeading(node: node, blockStyle: blockStyle);
39
+      content = new ZefyrHeading(node: line, blockStyle: blockStyle);
40
     } else {
40
     } else {
41
-      content = new ZefyrParagraph(node: node, blockStyle: blockStyle);
41
+      content = new ZefyrParagraph(node: line, blockStyle: blockStyle);
42
     }
42
     }
43
 
43
 
44
     final row = Row(children: <Widget>[Expanded(child: content)]);
44
     final row = Row(children: <Widget>[Expanded(child: content)]);

+ 4
- 2
packages/zefyr/lib/src/widgets/render_context.dart View File

97
   /// Returns closest render box to the specified global [point].
97
   /// Returns closest render box to the specified global [point].
98
   ///
98
   ///
99
   /// If [point] is inside of one of active render boxes that box is returned.
99
   /// If [point] is inside of one of active render boxes that box is returned.
100
-  /// Otherwise this method checks if [point] is to the left or to the right
100
+  /// If no box found this method checks if [point] is to the left or to the right
101
   /// side of a box, e.g. if vertical offset of this point is inside of one of
101
   /// side of a box, e.g. if vertical offset of this point is inside of one of
102
-  /// the active boxes. If it is then that box is returned.
102
+  /// the active boxes. If it is then that box is returned. If not then this
103
+  /// method picks a box with shortest vertical distance to this [point].
103
   RenderEditableProxyBox closestBoxForGlobalPoint(Offset point) {
104
   RenderEditableProxyBox closestBoxForGlobalPoint(Offset point) {
104
     assert(!_disposed);
105
     assert(!_disposed);
106
+    if (_activeBoxes.isEmpty) return null;
105
     RenderEditableProxyBox box = boxForGlobalPoint(point);
107
     RenderEditableProxyBox box = boxForGlobalPoint(point);
106
     if (box != null) return box;
108
     if (box != null) return box;
107
 
109
 

+ 9
- 8
packages/zefyr/lib/src/widgets/rich_text.dart View File

11
 import 'caret.dart';
11
 import 'caret.dart';
12
 import 'editable_box.dart';
12
 import 'editable_box.dart';
13
 
13
 
14
-class EditableRichText extends LeafRenderObjectWidget {
15
-  EditableRichText({
14
+/// Represents single paragraph of Zefyr rich-text.
15
+class ZefyrRichText extends LeafRenderObjectWidget {
16
+  ZefyrRichText({
16
     @required this.node,
17
     @required this.node,
17
     @required this.text,
18
     @required this.text,
18
   }) : assert(node != null && text != null);
19
   }) : assert(node != null && text != null);
22
 
23
 
23
   @override
24
   @override
24
   RenderObject createRenderObject(BuildContext context) {
25
   RenderObject createRenderObject(BuildContext context) {
25
-    return new RenderEditableParagraph(
26
+    return new RenderZefyrParagraph(
26
       text,
27
       text,
27
       node: node,
28
       node: node,
28
       textDirection: Directionality.of(context),
29
       textDirection: Directionality.of(context),
31
 
32
 
32
   @override
33
   @override
33
   void updateRenderObject(
34
   void updateRenderObject(
34
-      BuildContext context, RenderEditableParagraph renderObject) {
35
+      BuildContext context, RenderZefyrParagraph renderObject) {
35
     renderObject
36
     renderObject
36
       ..text = text
37
       ..text = text
37
       ..node = node;
38
       ..node = node;
38
   }
39
   }
39
 }
40
 }
40
 
41
 
41
-class RenderEditableParagraph extends RenderParagraph
42
+class RenderZefyrParagraph extends RenderParagraph
42
     implements RenderEditableBox {
43
     implements RenderEditableBox {
43
-  RenderEditableParagraph(
44
+  RenderZefyrParagraph(
44
     TextSpan text, {
45
     TextSpan text, {
45
-    @required ContainerNode node,
46
+    @required LineNode node,
46
     TextAlign textAlign: TextAlign.start,
47
     TextAlign textAlign: TextAlign.start,
47
     @required TextDirection textDirection,
48
     @required TextDirection textDirection,
48
     bool softWrap: true,
49
     bool softWrap: true,
127
   List<ui.TextBox> getEndpointsForSelection(TextSelection selection) {
128
   List<ui.TextBox> getEndpointsForSelection(TextSelection selection) {
128
     TextSelection local = getLocalSelection(selection);
129
     TextSelection local = getLocalSelection(selection);
129
     if (local.isCollapsed) {
130
     if (local.isCollapsed) {
130
-      final caret = CaretPainter.buildPrototype(preferredLineHeight);
131
+      final caret = CursorPainter.buildPrototype(preferredLineHeight);
131
       final offset = getOffsetForCaret(local.extent, caret);
132
       final offset = getOffsetForCaret(local.extent, caret);
132
       return [
133
       return [
133
         new ui.TextBox.fromLTRBD(
134
         new ui.TextBox.fromLTRBD(

+ 232
- 0
packages/zefyr/lib/src/widgets/scope.dart View File

1
+import 'package:flutter/foundation.dart';
2
+import 'package:flutter/material.dart';
3
+import 'package:notus/notus.dart';
4
+
5
+import 'controller.dart';
6
+import 'cursor_timer.dart';
7
+import 'editor.dart';
8
+import 'image.dart';
9
+import 'render_context.dart';
10
+import 'view.dart';
11
+
12
+/// Provides access to shared state of [ZefyrEditor] or [ZefyrView].
13
+///
14
+/// A scope object can be created by an editable widget like [ZefyrEditor] in
15
+/// which case it provides access to editing state, including focus nodes,
16
+/// selection and such. Editable scope can be created using
17
+/// [ZefyrScope.editable] constructor.
18
+///
19
+/// If a scope object is created by a view-only widget like [ZefyrView] then
20
+/// it only provides access to [imageDelegate].
21
+///
22
+/// Can be retrieved using [ZefyrScope.of].
23
+class ZefyrScope extends ChangeNotifier {
24
+  /// Creates a view-only scope.
25
+  ///
26
+  /// Normally used in [ZefyrView].
27
+  ZefyrScope.view({@required ZefyrImageDelegate imageDelegate})
28
+      : assert(imageDelegate != null),
29
+        isEditable = false,
30
+        _imageDelegate = imageDelegate;
31
+
32
+  /// Creates editable scope.
33
+  ///
34
+  /// Normally used in [ZefyrEditor].
35
+  ZefyrScope.editable({
36
+    @required ZefyrController controller,
37
+    @required ZefyrImageDelegate imageDelegate,
38
+    @required FocusNode focusNode,
39
+    @required FocusScopeNode focusScope,
40
+  })  : assert(controller != null),
41
+        assert(imageDelegate != null),
42
+        assert(focusNode != null),
43
+        assert(focusScope != null),
44
+        isEditable = true,
45
+        _controller = controller,
46
+        _imageDelegate = imageDelegate,
47
+        _focusNode = focusNode,
48
+        _focusScope = focusScope,
49
+        _cursorTimer = CursorTimer(),
50
+        _renderContext = ZefyrRenderContext() {
51
+    _selectionStyle = _controller.getSelectionStyle();
52
+    _selection = _controller.selection;
53
+    _controller.addListener(_handleControllerChange);
54
+    _focusNode.addListener(_handleFocusChange);
55
+  }
56
+
57
+  static ZefyrScope of(BuildContext context) {
58
+    final ZefyrScopeAccess widget =
59
+        context.inheritFromWidgetOfExactType(ZefyrScopeAccess);
60
+    return widget.scope;
61
+  }
62
+
63
+  ZefyrImageDelegate _imageDelegate;
64
+  ZefyrImageDelegate get imageDelegate => _imageDelegate;
65
+  set imageDelegate(ZefyrImageDelegate value) {
66
+    assert(value != null);
67
+    if (_imageDelegate != value) {
68
+      _imageDelegate = value;
69
+      notifyListeners();
70
+    }
71
+  }
72
+
73
+  ZefyrController _controller;
74
+  ZefyrController get controller => _controller;
75
+  set controller(ZefyrController value) {
76
+    assert(isEditable && value != null);
77
+    if (_controller != value) {
78
+      _controller.removeListener(_handleControllerChange);
79
+      _controller = value;
80
+      _selectionStyle = _controller.getSelectionStyle();
81
+      _selection = _controller.selection;
82
+      _controller.addListener(_handleControllerChange);
83
+      notifyListeners();
84
+    }
85
+  }
86
+
87
+  FocusNode _focusNode;
88
+  FocusNode get focusNode => _focusNode;
89
+  set focusNode(FocusNode value) {
90
+    assert(isEditable && value != null);
91
+    if (_focusNode != value) {
92
+      _focusNode.removeListener(_handleFocusChange);
93
+      _focusNode = value;
94
+      _focusNode.addListener(_handleFocusChange);
95
+      notifyListeners();
96
+    }
97
+  }
98
+
99
+  FocusScopeNode _focusScope;
100
+  FocusScopeNode get focusScope => _focusScope;
101
+  set focusScope(FocusScopeNode value) {
102
+    assert(isEditable && value != null);
103
+    if (_focusScope != value) {
104
+      _focusScope = value;
105
+    }
106
+  }
107
+
108
+  CursorTimer _cursorTimer;
109
+  CursorTimer get cursorTimer => _cursorTimer;
110
+  ValueNotifier<bool> get showCursor => cursorTimer.value;
111
+
112
+  ZefyrRenderContext _renderContext;
113
+  ZefyrRenderContext get renderContext => _renderContext;
114
+
115
+  NotusStyle get selectionStyle => _selectionStyle;
116
+  NotusStyle _selectionStyle;
117
+  TextSelection get selection => _selection;
118
+  TextSelection _selection;
119
+
120
+  bool _disposed = false;
121
+  FocusNode _toolbarFocusNode;
122
+
123
+  /// Whether this scope is backed by editable Zefyr widgets or read-only view.
124
+  ///
125
+  /// Returns `true` if this scope provides Zefyr interface that allows editing
126
+  /// (e.g. created by [ZefyrEditor]). Returns `false` if this scope provides
127
+  /// read-only view (e.g. created by [ZefyrView]).
128
+  ///
129
+  /// Editable scope provides access to corresponding [controller], [focusNode],
130
+  /// [focusScope], [showCursor], [renderContext] and other shared objects. For
131
+  /// non-editable scopes these are set to `null`. You can still access
132
+  /// objects which are not dependent on editing flow, e.g. [imageDelegate].
133
+  final bool isEditable;
134
+
135
+  set toolbarFocusNode(FocusNode node) {
136
+    assert(isEditable);
137
+    assert(!_disposed || node == null);
138
+    if (_toolbarFocusNode != node) {
139
+      _toolbarFocusNode?.removeListener(_handleFocusChange);
140
+      _toolbarFocusNode = node;
141
+      _toolbarFocusNode?.addListener(_handleFocusChange);
142
+      // We do not notify listeners here because it will happen when
143
+      // focus state changes, see [_handleFocusChange].
144
+    }
145
+  }
146
+
147
+  FocusOwner get focusOwner {
148
+    assert(isEditable);
149
+    assert(!_disposed);
150
+    if (_focusNode.hasFocus) {
151
+      return FocusOwner.editor;
152
+    } else if (_toolbarFocusNode?.hasFocus == true) {
153
+      return FocusOwner.toolbar;
154
+    } else {
155
+      return FocusOwner.none;
156
+    }
157
+  }
158
+
159
+  void updateSelection(TextSelection value,
160
+      {ChangeSource source: ChangeSource.remote}) {
161
+    assert(isEditable);
162
+    assert(!_disposed);
163
+    _controller.updateSelection(value, source: source);
164
+  }
165
+
166
+  void formatSelection(NotusAttribute value) {
167
+    assert(isEditable);
168
+    assert(!_disposed);
169
+    _controller.formatSelection(value);
170
+  }
171
+
172
+  void focus() {
173
+    assert(isEditable);
174
+    assert(!_disposed);
175
+    _focusScope.requestFocus(_focusNode);
176
+  }
177
+
178
+  void hideKeyboard() {
179
+    assert(isEditable);
180
+    assert(!_disposed);
181
+    _focusNode.unfocus();
182
+  }
183
+
184
+  @override
185
+  void dispose() {
186
+    assert(!_disposed);
187
+    _controller?.removeListener(_handleControllerChange);
188
+    _focusNode?.removeListener(_handleFocusChange);
189
+    _disposed = true;
190
+    super.dispose();
191
+  }
192
+
193
+  void _handleControllerChange() {
194
+    assert(!_disposed);
195
+    final attrs = _controller.getSelectionStyle();
196
+    final selection = _controller.selection;
197
+    if (_selectionStyle != attrs || _selection != selection) {
198
+      _selectionStyle = attrs;
199
+      _selection = selection;
200
+      notifyListeners();
201
+    }
202
+  }
203
+
204
+  void _handleFocusChange() {
205
+    assert(!_disposed);
206
+    if (focusOwner == FocusOwner.none && !_selection.isCollapsed) {
207
+      // Collapse selection if there is nothing focused.
208
+      _controller.updateSelection(_selection.copyWith(
209
+        baseOffset: _selection.extentOffset,
210
+        extentOffset: _selection.extentOffset,
211
+      ));
212
+    }
213
+    notifyListeners();
214
+  }
215
+
216
+  @override
217
+  String toString() {
218
+    return '$ZefyrScope#${shortHash(this)}';
219
+  }
220
+}
221
+
222
+class ZefyrScopeAccess extends InheritedWidget {
223
+  final ZefyrScope scope;
224
+
225
+  ZefyrScopeAccess({Key key, @required this.scope, @required Widget child})
226
+      : super(key: key, child: child);
227
+
228
+  @override
229
+  bool updateShouldNotify(ZefyrScopeAccess oldWidget) {
230
+    return scope != oldWidget.scope;
231
+  }
232
+}

+ 41
- 28
packages/zefyr/lib/src/widgets/selection.dart View File

10
 
10
 
11
 import 'controller.dart';
11
 import 'controller.dart';
12
 import 'editable_box.dart';
12
 import 'editable_box.dart';
13
-import 'editable_text.dart';
14
-import 'editor.dart';
13
+import 'scope.dart';
15
 
14
 
16
 RenderEditableBox _getEditableBox(HitTestResult result) {
15
 RenderEditableBox _getEditableBox(HitTestResult result) {
17
   for (var entry in result.path) {
16
   for (var entry in result.path) {
18
     if (entry.target is RenderEditableBox) {
17
     if (entry.target is RenderEditableBox) {
19
-      return entry.target;
18
+      return entry.target as RenderEditableBox;
20
     }
19
     }
21
   }
20
   }
22
   return null;
21
   return null;
73
   }
72
   }
74
 
73
 
75
   void showToolbar() {
74
   void showToolbar() {
76
-    final editable = ZefyrEditableText.of(context);
77
-    assert(editable != null);
75
+    final scope = ZefyrScope.of(context);
76
+    assert(scope != null);
78
     final toolbarOpacity = _toolbarController.view;
77
     final toolbarOpacity = _toolbarController.view;
79
     _toolbar = new OverlayEntry(
78
     _toolbar = new OverlayEntry(
80
       builder: (context) => new FadeTransition(
79
       builder: (context) => new FadeTransition(
81
             opacity: toolbarOpacity,
80
             opacity: toolbarOpacity,
82
             child: new _SelectionToolbar(
81
             child: new _SelectionToolbar(
83
-              editable: editable,
82
+              scope: scope,
84
               controls: widget.controls,
83
               controls: widget.controls,
85
               delegate: this,
84
               delegate: this,
86
             ),
85
             ),
117
   @override
116
   @override
118
   void didChangeDependencies() {
117
   void didChangeDependencies() {
119
     super.didChangeDependencies();
118
     super.didChangeDependencies();
120
-    final editor = ZefyrEditor.of(context);
119
+    final editor = ZefyrScope.of(context);
121
     if (_editor != editor) {
120
     if (_editor != editor) {
122
       _editor?.removeListener(_handleChange);
121
       _editor?.removeListener(_handleChange);
123
       _editor = editor;
122
       _editor = editor;
174
   OverlayEntry _toolbar;
173
   OverlayEntry _toolbar;
175
   AnimationController _toolbarController;
174
   AnimationController _toolbarController;
176
 
175
 
177
-  ZefyrEditorScope _editor;
176
+  ZefyrScope _editor;
178
   TextSelection _selection;
177
   TextSelection _selection;
179
   FocusOwner _focusOwner;
178
   FocusOwner _focusOwner;
180
 
179
 
190
     if (!mounted) {
189
     if (!mounted) {
191
       return;
190
       return;
192
     }
191
     }
193
-    final editor = ZefyrEditor.of(context);
194
-    final selection = editor.selection;
195
-    final focusOwner = editor.focusOwner;
192
+
193
+    final selection = _editor.selection;
194
+    final focusOwner = _editor.focusOwner;
196
     setState(() {
195
     setState(() {
197
       if (focusOwner != FocusOwner.editor) {
196
       if (focusOwner != FocusOwner.editor) {
198
         hideToolbar();
197
         hideToolbar();
233
 
232
 
234
     RenderEditableProxyBox box = _getEditableBox(result);
233
     RenderEditableProxyBox box = _getEditableBox(result);
235
     if (box == null) {
234
     if (box == null) {
236
-      final editable = ZefyrEditableText.of(context);
237
-      box = editable.renderContext.closestBoxForGlobalPoint(globalPoint);
235
+      box = _editor.renderContext.closestBoxForGlobalPoint(globalPoint);
238
     }
236
     }
239
     if (box == null) return null;
237
     if (box == null) return null;
240
 
238
 
295
 }
293
 }
296
 
294
 
297
 class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
295
 class _SelectionHandleDriverState extends State<SelectionHandleDriver> {
296
+  ZefyrScope _scope;
297
+
298
   /// Current document selection.
298
   /// Current document selection.
299
   TextSelection get selection => _selection;
299
   TextSelection get selection => _selection;
300
   TextSelection _selection;
300
   TextSelection _selection;
327
   @override
327
   @override
328
   void didChangeDependencies() {
328
   void didChangeDependencies() {
329
     super.didChangeDependencies();
329
     super.didChangeDependencies();
330
-    final editable = ZefyrEditableText.of(context);
331
-    _selection = editable.selection;
330
+    final scope = ZefyrScope.of(context);
331
+    if (_scope != scope) {
332
+      _scope?.removeListener(_handleScopeChange);
333
+      _scope = scope;
334
+      _scope.addListener(_handleScopeChange);
335
+    }
336
+    _selection = _scope.selection;
337
+  }
338
+
339
+  @override
340
+  void dispose() {
341
+    _scope?.removeListener(_handleScopeChange);
342
+    super.dispose();
332
   }
343
   }
333
 
344
 
334
   //
345
   //
337
 
348
 
338
   @override
349
   @override
339
   Widget build(BuildContext context) {
350
   Widget build(BuildContext context) {
340
-    final editor = ZefyrEditor.of(context);
341
-    final editable = ZefyrEditableText.of(context);
342
     if (selection == null ||
351
     if (selection == null ||
343
         selection.isCollapsed ||
352
         selection.isCollapsed ||
344
         widget.controls == null ||
353
         widget.controls == null ||
345
-        editor.focusOwner != FocusOwner.editor) {
354
+        _scope.focusOwner != FocusOwner.editor) {
346
       return new Container();
355
       return new Container();
347
     }
356
     }
348
-    final block = editable.renderContext.boxForTextOffset(documentOffset);
357
+    final block = _scope.renderContext.boxForTextOffset(documentOffset);
349
     final position = getPosition(block);
358
     final position = getPosition(block);
350
     Widget handle;
359
     Widget handle;
351
     if (position == null) {
360
     if (position == null) {
388
 
397
 
389
   Offset _dragPosition;
398
   Offset _dragPosition;
390
 
399
 
400
+  void _handleScopeChange() {
401
+    if (_selection != _scope.selection) {
402
+      setState(() {
403
+        _selection = _scope.selection;
404
+      });
405
+    }
406
+  }
407
+
391
   void _handleDragStart(DragStartDetails details) {
408
   void _handleDragStart(DragStartDetails details) {
392
     _dragPosition = details.globalPosition;
409
     _dragPosition = details.globalPosition;
393
   }
410
   }
395
   void _handleDragUpdate(DragUpdateDetails details) {
412
   void _handleDragUpdate(DragUpdateDetails details) {
396
     _dragPosition += details.delta;
413
     _dragPosition += details.delta;
397
     final globalPoint = _dragPosition;
414
     final globalPoint = _dragPosition;
398
-    final editor = ZefyrEditor.of(context);
399
-    final editable = ZefyrEditableText.of(context);
400
-    final paragraph = editable.renderContext.boxForGlobalPoint(globalPoint);
415
+    final paragraph = _scope.renderContext.boxForGlobalPoint(globalPoint);
401
     if (paragraph == null) {
416
     if (paragraph == null) {
402
       return;
417
       return;
403
     }
418
     }
414
     }
429
     }
415
 
430
 
416
     if (newSelection != _selection) {
431
     if (newSelection != _selection) {
417
-      editor.updateSelection(newSelection, source: ChangeSource.local);
432
+      _scope.updateSelection(newSelection, source: ChangeSource.local);
418
     }
433
     }
419
   }
434
   }
420
 }
435
 }
422
 class _SelectionToolbar extends StatefulWidget {
437
 class _SelectionToolbar extends StatefulWidget {
423
   const _SelectionToolbar({
438
   const _SelectionToolbar({
424
     Key key,
439
     Key key,
425
-    @required this.editable,
440
+    @required this.scope,
426
     @required this.controls,
441
     @required this.controls,
427
     @required this.delegate,
442
     @required this.delegate,
428
   }) : super(key: key);
443
   }) : super(key: key);
429
 
444
 
430
-  final ZefyrEditableTextScope editable;
445
+  final ZefyrScope scope;
431
   final TextSelectionControls controls;
446
   final TextSelectionControls controls;
432
   final TextSelectionDelegate delegate;
447
   final TextSelectionDelegate delegate;
433
 
448
 
436
 }
451
 }
437
 
452
 
438
 class _SelectionToolbarState extends State<_SelectionToolbar> {
453
 class _SelectionToolbarState extends State<_SelectionToolbar> {
439
-  ZefyrEditableTextScope get editable => widget.editable;
454
+  ZefyrScope get editable => widget.scope;
440
   TextSelection get selection => widget.delegate.textEditingValue.selection;
455
   TextSelection get selection => widget.delegate.textEditingValue.selection;
441
 
456
 
442
   @override
457
   @override
452
       return Container();
467
       return Container();
453
     }
468
     }
454
     final boxes = block.getEndpointsForSelection(selection);
469
     final boxes = block.getEndpointsForSelection(selection);
455
-
456
     // Find the horizontal midpoint, just above the selected text.
470
     // Find the horizontal midpoint, just above the selected text.
457
     final Offset midpoint = new Offset(
471
     final Offset midpoint = new Offset(
458
       (boxes.length == 1)
472
       (boxes.length == 1)
465
       block.localToGlobal(Offset.zero),
479
       block.localToGlobal(Offset.zero),
466
       block.localToGlobal(block.size.bottomRight(Offset.zero)),
480
       block.localToGlobal(block.size.bottomRight(Offset.zero)),
467
     );
481
     );
468
-
469
     final toolbar = widget.controls
482
     final toolbar = widget.controls
470
         .buildToolbar(context, editingRegion, midpoint, widget.delegate);
483
         .buildToolbar(context, editingRegion, midpoint, widget.delegate);
471
     return new CompositedTransformFollower(
484
     return new CompositedTransformFollower(

+ 6
- 0
packages/zefyr/lib/src/widgets/theme.dart View File

60
   final HeadingTheme headingTheme;
60
   final HeadingTheme headingTheme;
61
   final BlockTheme blockTheme;
61
   final BlockTheme blockTheme;
62
   final Color selectionColor;
62
   final Color selectionColor;
63
+  final Color cursorColor;
63
 
64
 
64
   /// Size of indentation for blocks.
65
   /// Size of indentation for blocks.
65
   final double indentSize;
66
   final double indentSize;
88
       headingTheme: new HeadingTheme.fallback(),
89
       headingTheme: new HeadingTheme.fallback(),
89
       blockTheme: new BlockTheme.fallback(),
90
       blockTheme: new BlockTheme.fallback(),
90
       selectionColor: Colors.lightBlueAccent.shade100,
91
       selectionColor: Colors.lightBlueAccent.shade100,
92
+      cursorColor: Colors.black,
91
       indentSize: 16.0,
93
       indentSize: 16.0,
92
       toolbarTheme: new ZefyrToolbarTheme.fallback(context),
94
       toolbarTheme: new ZefyrToolbarTheme.fallback(context),
93
     );
95
     );
101
     this.headingTheme,
103
     this.headingTheme,
102
     this.blockTheme,
104
     this.blockTheme,
103
     this.selectionColor,
105
     this.selectionColor,
106
+    this.cursorColor,
104
     this.indentSize,
107
     this.indentSize,
105
     this.toolbarTheme,
108
     this.toolbarTheme,
106
   });
109
   });
114
     HeadingTheme headingTheme,
117
     HeadingTheme headingTheme,
115
     BlockTheme blockTheme,
118
     BlockTheme blockTheme,
116
     Color selectionColor,
119
     Color selectionColor,
120
+    Color cursorColor,
117
     double indentSize,
121
     double indentSize,
118
     ZefyrToolbarTheme toolbarTheme,
122
     ZefyrToolbarTheme toolbarTheme,
119
   }) {
123
   }) {
125
       headingTheme: headingTheme ?? this.headingTheme,
129
       headingTheme: headingTheme ?? this.headingTheme,
126
       blockTheme: blockTheme ?? this.blockTheme,
130
       blockTheme: blockTheme ?? this.blockTheme,
127
       selectionColor: selectionColor ?? this.selectionColor,
131
       selectionColor: selectionColor ?? this.selectionColor,
132
+      cursorColor: cursorColor ?? this.cursorColor,
128
       indentSize: indentSize ?? this.indentSize,
133
       indentSize: indentSize ?? this.indentSize,
129
       toolbarTheme: toolbarTheme ?? this.toolbarTheme,
134
       toolbarTheme: toolbarTheme ?? this.toolbarTheme,
130
     );
135
     );
139
       headingTheme: other.headingTheme,
144
       headingTheme: other.headingTheme,
140
       blockTheme: other.blockTheme,
145
       blockTheme: other.blockTheme,
141
       selectionColor: other.selectionColor,
146
       selectionColor: other.selectionColor,
147
+      cursorColor: other.cursorColor,
142
       indentSize: other.indentSize,
148
       indentSize: other.indentSize,
143
       toolbarTheme: other.toolbarTheme,
149
       toolbarTheme: other.toolbarTheme,
144
     );
150
     );

+ 3
- 3
packages/zefyr/lib/src/widgets/toolbar.dart View File

8
 import 'package:notus/notus.dart';
8
 import 'package:notus/notus.dart';
9
 
9
 
10
 import 'buttons.dart';
10
 import 'buttons.dart';
11
-import 'editor.dart';
11
+import 'scope.dart';
12
 import 'theme.dart';
12
 import 'theme.dart';
13
 
13
 
14
 /// List of all button actions supported by [ZefyrToolbar] buttons.
14
 /// List of all button actions supported by [ZefyrToolbar] buttons.
107
   }) : super(key: key);
107
   }) : super(key: key);
108
 
108
 
109
   final ZefyrToolbarDelegate delegate;
109
   final ZefyrToolbarDelegate delegate;
110
-  final ZefyrEditorScope editor;
110
+  final ZefyrScope editor;
111
 
111
 
112
   /// Whether to automatically hide this toolbar when editor loses focus.
112
   /// Whether to automatically hide this toolbar when editor loses focus.
113
   final bool autoHide;
113
   final bool autoHide;
187
 
187
 
188
   bool get hasOverlay => _overlayBuilder != null;
188
   bool get hasOverlay => _overlayBuilder != null;
189
 
189
 
190
-  ZefyrEditorScope get editor => widget.editor;
190
+  ZefyrScope get editor => widget.editor;
191
 
191
 
192
   @override
192
   @override
193
   void initState() {
193
   void initState() {

+ 105
- 0
packages/zefyr/lib/src/widgets/view.dart View File

1
+import 'package:flutter/material.dart';
2
+import 'package:notus/notus.dart';
3
+
4
+import 'code.dart';
5
+import 'common.dart';
6
+import 'image.dart';
7
+import 'list.dart';
8
+import 'paragraph.dart';
9
+import 'quote.dart';
10
+import 'scope.dart';
11
+import 'theme.dart';
12
+
13
+/// Non-scrollable read-only view of Notus rich text documents.
14
+class ZefyrView extends StatefulWidget {
15
+  final NotusDocument document;
16
+  final ZefyrImageDelegate imageDelegate;
17
+
18
+  const ZefyrView({Key key, @required this.document, this.imageDelegate})
19
+      : super(key: key);
20
+
21
+  @override
22
+  ZefyrViewState createState() => ZefyrViewState();
23
+}
24
+
25
+class ZefyrViewState extends State<ZefyrView> {
26
+  ZefyrScope _scope;
27
+  ZefyrThemeData _themeData;
28
+
29
+  ZefyrImageDelegate get imageDelegate => widget.imageDelegate;
30
+
31
+  @override
32
+  void initState() {
33
+    super.initState();
34
+    _scope = ZefyrScope.view(imageDelegate: widget.imageDelegate);
35
+  }
36
+
37
+  @override
38
+  void didUpdateWidget(ZefyrView oldWidget) {
39
+    super.didUpdateWidget(oldWidget);
40
+    _scope.imageDelegate = widget.imageDelegate;
41
+  }
42
+
43
+  @override
44
+  void didChangeDependencies() {
45
+    super.didChangeDependencies();
46
+    final parentTheme = ZefyrTheme.of(context, nullOk: true);
47
+    final fallbackTheme = ZefyrThemeData.fallback(context);
48
+    _themeData = (parentTheme != null)
49
+        ? fallbackTheme.merge(parentTheme)
50
+        : fallbackTheme;
51
+  }
52
+
53
+  @override
54
+  void dispose() {
55
+    _scope.dispose();
56
+    super.dispose();
57
+  }
58
+
59
+  @override
60
+  Widget build(BuildContext context) {
61
+    return ZefyrTheme(
62
+      data: _themeData,
63
+      child: ZefyrScopeAccess(
64
+        scope: _scope,
65
+        child: Column(
66
+          crossAxisAlignment: CrossAxisAlignment.stretch,
67
+          children: _buildChildren(context),
68
+        ),
69
+      ),
70
+    );
71
+  }
72
+
73
+  List<Widget> _buildChildren(BuildContext context) {
74
+    final result = <Widget>[];
75
+    for (var node in widget.document.root.children) {
76
+      result.add(_defaultChildBuilder(context, node));
77
+    }
78
+    return result;
79
+  }
80
+
81
+  Widget _defaultChildBuilder(BuildContext context, Node node) {
82
+    if (node is LineNode) {
83
+      if (node.hasEmbed) {
84
+        return new RawZefyrLine(node: node);
85
+      } else if (node.style.contains(NotusAttribute.heading)) {
86
+        return new ZefyrHeading(node: node);
87
+      }
88
+      return new ZefyrParagraph(node: node);
89
+    }
90
+
91
+    final BlockNode block = node;
92
+    final blockStyle = block.style.get(NotusAttribute.block);
93
+    if (blockStyle == NotusAttribute.block.code) {
94
+      return new ZefyrCode(node: block);
95
+    } else if (blockStyle == NotusAttribute.block.bulletList) {
96
+      return new ZefyrList(node: block);
97
+    } else if (blockStyle == NotusAttribute.block.numberList) {
98
+      return new ZefyrList(node: block);
99
+    } else if (blockStyle == NotusAttribute.block.quote) {
100
+      return new ZefyrQuote(node: block);
101
+    }
102
+
103
+    throw new UnimplementedError('Block format $blockStyle.');
104
+  }
105
+}

+ 2
- 0
packages/zefyr/lib/zefyr.dart View File

22
 export 'src/widgets/paragraph.dart';
22
 export 'src/widgets/paragraph.dart';
23
 export 'src/widgets/quote.dart';
23
 export 'src/widgets/quote.dart';
24
 export 'src/widgets/scaffold.dart';
24
 export 'src/widgets/scaffold.dart';
25
+export 'src/widgets/scope.dart' hide ZefyrScopeAccess;
25
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
26
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
26
 export 'src/widgets/theme.dart';
27
 export 'src/widgets/theme.dart';
27
 export 'src/widgets/toolbar.dart';
28
 export 'src/widgets/toolbar.dart';
29
+export 'src/widgets/view.dart';

+ 1
- 1
packages/zefyr/pubspec.yaml View File

1
 name: zefyr
1
 name: zefyr
2
 description: Clean, minimalistic and collaboration-ready rich text editor for Flutter.
2
 description: Clean, minimalistic and collaboration-ready rich text editor for Flutter.
3
-version: 0.3.0
3
+version: 0.3.1
4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
5
 homepage: https://github.com/memspace/zefyr
5
 homepage: https://github.com/memspace/zefyr
6
 
6
 

+ 4
- 3
packages/zefyr/test/painting/caret_painter_test.dart View File

3
 // BSD-style license that can be found in the LICENSE file.
3
 // BSD-style license that can be found in the LICENSE file.
4
 import 'dart:ui';
4
 import 'dart:ui';
5
 
5
 
6
+import 'package:flutter/material.dart';
6
 import 'package:flutter_test/flutter_test.dart';
7
 import 'package:flutter_test/flutter_test.dart';
7
 import 'package:zefyr/src/widgets/caret.dart';
8
 import 'package:zefyr/src/widgets/caret.dart';
8
 
9
 
9
 void main() {
10
 void main() {
10
-  group('$CaretPainter', () {
11
+  group('$CursorPainter', () {
11
     test('prototype is null before layout', () {
12
     test('prototype is null before layout', () {
12
-      var painter = new CaretPainter();
13
+      var painter = new CursorPainter(Colors.black);
13
       expect(painter.prototype, isNull);
14
       expect(painter.prototype, isNull);
14
     });
15
     });
15
 
16
 
16
     test('prototype is set after layout', () {
17
     test('prototype is set after layout', () {
17
-      var painter = new CaretPainter();
18
+      var painter = new CursorPainter(Colors.black);
18
       painter.layout(16.0);
19
       painter.layout(16.0);
19
       expect(painter.prototype, new Rect.fromLTWH(0.0, 0.0, 1.0, 14.0));
20
       expect(painter.prototype, new Rect.fromLTWH(0.0, 0.0, 1.0, 14.0));
20
     });
21
     });

+ 0
- 41
packages/zefyr/test/rendering/render_editable_paragraph_test.dart View File

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:ui';
5
-
6
-import 'package:flutter/material.dart';
7
-import 'package:flutter/rendering.dart';
8
-import 'package:flutter_test/flutter_test.dart';
9
-import 'package:zefyr/src/widgets/render_context.dart';
10
-import 'package:zefyr/src/widgets/rich_text.dart';
11
-import 'package:zefyr/zefyr.dart';
12
-
13
-void main() {
14
-  group('$RenderEditableParagraph', () {
15
-    final doc = new NotusDocument();
16
-    doc.insert(0, 'This House Is A Circus');
17
-    final text = new TextSpan(text: 'This House Is A Circus');
18
-
19
-    ZefyrRenderContext renderContext;
20
-    RenderEditableParagraph p;
21
-
22
-    setUp(() {
23
-      WidgetsFlutterBinding.ensureInitialized();
24
-      renderContext = new ZefyrRenderContext();
25
-      p = new RenderEditableParagraph(
26
-        text,
27
-        node: doc.root.children.first,
28
-        textDirection: TextDirection.ltr,
29
-      );
30
-    });
31
-
32
-    test('it registers with viewport', () {
33
-      var owner = new PipelineOwner();
34
-      expect(renderContext.active, isNot(contains(p)));
35
-      p.attach(owner);
36
-      expect(renderContext.dirty, contains(p));
37
-      p.layout(new BoxConstraints());
38
-      expect(renderContext.active, contains(p));
39
-    }, skip: 'TODO: move to RenderEditableProxyBox');
40
-  });
41
-}

+ 64
- 0
packages/zefyr/test/rendering/render_editable_proxy_box_test.dart View File

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:ui';
5
+
6
+import 'package:flutter/material.dart';
7
+import 'package:flutter/rendering.dart';
8
+import 'package:flutter_test/flutter_test.dart';
9
+import 'package:zefyr/src/widgets/editable_box.dart';
10
+import 'package:zefyr/src/widgets/render_context.dart';
11
+import 'package:zefyr/src/widgets/rich_text.dart';
12
+import 'package:zefyr/zefyr.dart';
13
+
14
+void main() {
15
+  group('$RenderEditableProxyBox', () {
16
+    final doc = NotusDocument();
17
+    doc.insert(0, 'This House Is A Circus');
18
+    final text = TextSpan(text: 'This House Is A Circus');
19
+
20
+    ZefyrRenderContext renderContext;
21
+    RenderEditableProxyBox p;
22
+
23
+    setUp(() {
24
+      WidgetsFlutterBinding.ensureInitialized();
25
+      renderContext = ZefyrRenderContext();
26
+      final rt = RenderZefyrParagraph(
27
+        text,
28
+        node: doc.root.children.first as LineNode,
29
+        textDirection: TextDirection.ltr,
30
+      );
31
+      p = RenderEditableProxyBox(
32
+        child: rt,
33
+        node: doc.root.children.first as LineNode,
34
+        layerLink: LayerLink(),
35
+        renderContext: renderContext,
36
+        showCursor: ValueNotifier<bool>(true),
37
+        selection: TextSelection.collapsed(offset: 0),
38
+        selectionColor: Color(0),
39
+        cursorColor: Color(0),
40
+      );
41
+    });
42
+
43
+    test('it registers with render context', () {
44
+      var owner = new PipelineOwner();
45
+      expect(renderContext.active, isNot(contains(p)));
46
+      p.attach(owner);
47
+      expect(renderContext.dirty, contains(p));
48
+      p.layout(BoxConstraints());
49
+      expect(renderContext.active, contains(p));
50
+
51
+      p.detach();
52
+      expect(renderContext.active, isNot(contains(p)));
53
+      p.attach(owner);
54
+      expect(renderContext.active, contains(p));
55
+
56
+      p.markNeedsLayout();
57
+      p.detach();
58
+      expect(renderContext.active, isNot(contains(p)));
59
+
60
+      p.layout(BoxConstraints());
61
+      expect(renderContext.active, contains(p));
62
+    });
63
+  });
64
+}

+ 20
- 3
packages/zefyr/test/testing.dart View File

21
     FocusNode focusNode,
21
     FocusNode focusNode,
22
     NotusDocument document,
22
     NotusDocument document,
23
     ZefyrThemeData theme,
23
     ZefyrThemeData theme,
24
+    bool autofocus: false,
24
   }) {
25
   }) {
25
     focusNode ??= FocusNode();
26
     focusNode ??= FocusNode();
26
     document ??= NotusDocument.fromDelta(delta);
27
     document ??= NotusDocument.fromDelta(delta);
27
     var controller = ZefyrController(document);
28
     var controller = ZefyrController(document);
28
 
29
 
29
-    Widget widget = _ZefyrSandbox(controller: controller, focusNode: focusNode);
30
+    Widget widget = _ZefyrSandbox(
31
+      controller: controller,
32
+      focusNode: focusNode,
33
+      autofocus: autofocus,
34
+    );
35
+
30
     if (theme != null) {
36
     if (theme != null) {
31
       widget = ZefyrTheme(data: theme, child: widget);
37
       widget = ZefyrTheme(data: theme, child: widget);
32
     }
38
     }
60
     return tester.pumpAndSettle();
66
     return tester.pumpAndSettle();
61
   }
67
   }
62
 
68
 
63
-  Future<void> tapEditor() async {
69
+  Future<void> pump() async {
64
     await tester.pumpWidget(widget);
70
     await tester.pumpWidget(widget);
71
+  }
72
+
73
+  Future<void> tap() async {
65
     await tester.tap(find.byType(ZefyrParagraph).first);
74
     await tester.tap(find.byType(ZefyrParagraph).first);
66
     await tester.pumpAndSettle();
75
     await tester.pumpAndSettle();
67
     expect(focusNode.hasFocus, isTrue);
76
     expect(focusNode.hasFocus, isTrue);
68
   }
77
   }
69
 
78
 
79
+  Future<void> pumpAndTap() async {
80
+    await pump();
81
+    await tap();
82
+  }
83
+
70
   Future<void> tapHideKeyboardButton() async {
84
   Future<void> tapHideKeyboardButton() async {
71
     await tapButtonWithIcon(Icons.keyboard_hide);
85
     await tapButtonWithIcon(Icons.keyboard_hide);
72
   }
86
   }
101
 }
115
 }
102
 
116
 
103
 class _ZefyrSandbox extends StatefulWidget {
117
 class _ZefyrSandbox extends StatefulWidget {
104
-  const _ZefyrSandbox({Key key, this.controller, this.focusNode})
118
+  const _ZefyrSandbox(
119
+      {Key key, this.controller, this.focusNode, this.autofocus})
105
       : super(key: key);
120
       : super(key: key);
106
   final ZefyrController controller;
121
   final ZefyrController controller;
107
   final FocusNode focusNode;
122
   final FocusNode focusNode;
123
+  final bool autofocus;
108
 
124
 
109
   @override
125
   @override
110
   _ZefyrSandboxState createState() => _ZefyrSandboxState();
126
   _ZefyrSandboxState createState() => _ZefyrSandboxState();
119
       controller: widget.controller,
135
       controller: widget.controller,
120
       focusNode: widget.focusNode,
136
       focusNode: widget.focusNode,
121
       enabled: _enabled,
137
       enabled: _enabled,
138
+      autofocus: widget.autofocus,
122
     );
139
     );
123
   }
140
   }
124
 
141
 

+ 14
- 14
packages/zefyr/test/widgets/buttons_test.dart View File

13
   group('$ZefyrButton', () {
13
   group('$ZefyrButton', () {
14
     testWidgets('toggle style', (tester) async {
14
     testWidgets('toggle style', (tester) async {
15
       final editor = new EditorSandBox(tester: tester);
15
       final editor = new EditorSandBox(tester: tester);
16
-      await editor.tapEditor();
16
+      await editor.pumpAndTap();
17
       await editor.updateSelection(base: 5, extent: 10);
17
       await editor.updateSelection(base: 5, extent: 10);
18
       await editor.tapButtonWithIcon(Icons.format_bold);
18
       await editor.tapButtonWithIcon(Icons.format_bold);
19
 
19
 
24
       expect(bold.value, 'House');
24
       expect(bold.value, 'House');
25
 
25
 
26
       await editor.tapButtonWithIcon(Icons.format_bold);
26
       await editor.tapButtonWithIcon(Icons.format_bold);
27
-      line = editor.document.root.children.first;
27
+      line = editor.document.root.children.first as LineNode;
28
       expect(line.childCount, 1);
28
       expect(line.childCount, 1);
29
     });
29
     });
30
 
30
 
31
     testWidgets('toggle state for different styles of the same attribute',
31
     testWidgets('toggle state for different styles of the same attribute',
32
         (tester) async {
32
         (tester) async {
33
       final editor = new EditorSandBox(tester: tester);
33
       final editor = new EditorSandBox(tester: tester);
34
-      await editor.tapEditor();
34
+      await editor.pumpAndTap();
35
 
35
 
36
       await editor.tapButtonWithIcon(Icons.format_list_bulleted);
36
       await editor.tapButtonWithIcon(Icons.format_list_bulleted);
37
       expect(editor.document.root.children.first, isInstanceOf<BlockNode>());
37
       expect(editor.document.root.children.first, isInstanceOf<BlockNode>());
46
   group('$HeadingButton', () {
46
   group('$HeadingButton', () {
47
     testWidgets('toggle menu', (tester) async {
47
     testWidgets('toggle menu', (tester) async {
48
       final editor = new EditorSandBox(tester: tester);
48
       final editor = new EditorSandBox(tester: tester);
49
-      await editor.tapEditor();
49
+      await editor.pumpAndTap();
50
       await editor.tapButtonWithIcon(Icons.format_size);
50
       await editor.tapButtonWithIcon(Icons.format_size);
51
 
51
 
52
       expect(find.text('H1'), findsOneWidget);
52
       expect(find.text('H1'), findsOneWidget);
60
 
60
 
61
     testWidgets('toggle styles', (tester) async {
61
     testWidgets('toggle styles', (tester) async {
62
       final editor = new EditorSandBox(tester: tester);
62
       final editor = new EditorSandBox(tester: tester);
63
-      await editor.tapEditor();
63
+      await editor.pumpAndTap();
64
       await editor.tapButtonWithIcon(Icons.format_size);
64
       await editor.tapButtonWithIcon(Icons.format_size);
65
       await editor.tapButtonWithText('H3');
65
       await editor.tapButtonWithText('H3');
66
       LineNode line = editor.document.root.children.first;
66
       LineNode line = editor.document.root.children.first;
71
 
71
 
72
     testWidgets('close overlay', (tester) async {
72
     testWidgets('close overlay', (tester) async {
73
       final editor = new EditorSandBox(tester: tester);
73
       final editor = new EditorSandBox(tester: tester);
74
-      await editor.tapEditor();
74
+      await editor.pumpAndTap();
75
       await editor.tapButtonWithIcon(Icons.format_size);
75
       await editor.tapButtonWithIcon(Icons.format_size);
76
       expect(find.text('H1'), findsOneWidget);
76
       expect(find.text('H1'), findsOneWidget);
77
       await editor.tapButtonWithIcon(Icons.close);
77
       await editor.tapButtonWithIcon(Icons.close);
82
   group('$LinkButton', () {
82
   group('$LinkButton', () {
83
     testWidgets('disabled when selection is collapsed', (tester) async {
83
     testWidgets('disabled when selection is collapsed', (tester) async {
84
       final editor = new EditorSandBox(tester: tester);
84
       final editor = new EditorSandBox(tester: tester);
85
-      await editor.tapEditor();
85
+      await editor.pumpAndTap();
86
       await editor.tapButtonWithIcon(Icons.link);
86
       await editor.tapButtonWithIcon(Icons.link);
87
       expect(find.byIcon(Icons.link_off), findsNothing);
87
       expect(find.byIcon(Icons.link_off), findsNothing);
88
     });
88
     });
90
     testWidgets('enabled and toggles menu with non-empty selection',
90
     testWidgets('enabled and toggles menu with non-empty selection',
91
         (tester) async {
91
         (tester) async {
92
       final editor = new EditorSandBox(tester: tester);
92
       final editor = new EditorSandBox(tester: tester);
93
-      await editor.tapEditor();
93
+      await editor.pumpAndTap();
94
       await editor.updateSelection(base: 5, extent: 10);
94
       await editor.updateSelection(base: 5, extent: 10);
95
       await editor.tapButtonWithIcon(Icons.link);
95
       await editor.tapButtonWithIcon(Icons.link);
96
       expect(find.byIcon(Icons.link_off), findsOneWidget);
96
       expect(find.byIcon(Icons.link_off), findsOneWidget);
98
 
98
 
99
     testWidgets('auto cancels edit on selection update', (tester) async {
99
     testWidgets('auto cancels edit on selection update', (tester) async {
100
       final editor = new EditorSandBox(tester: tester);
100
       final editor = new EditorSandBox(tester: tester);
101
-      await editor.tapEditor();
101
+      await editor.pumpAndTap();
102
       await editor.updateSelection(base: 5, extent: 10);
102
       await editor.updateSelection(base: 5, extent: 10);
103
       await editor.tapButtonWithIcon(Icons.link);
103
       await editor.tapButtonWithIcon(Icons.link);
104
       await tester
104
       await tester
111
 
111
 
112
     testWidgets('editing link', (tester) async {
112
     testWidgets('editing link', (tester) async {
113
       final editor = new EditorSandBox(tester: tester);
113
       final editor = new EditorSandBox(tester: tester);
114
-      await editor.tapEditor();
114
+      await editor.pumpAndTap();
115
       await editor.updateSelection(base: 5, extent: 10);
115
       await editor.updateSelection(base: 5, extent: 10);
116
 
116
 
117
       await editor.tapButtonWithIcon(Icons.link);
117
       await editor.tapButtonWithIcon(Icons.link);
139
       await editor.updateSelection(base: 7, extent: 7);
139
       await editor.updateSelection(base: 7, extent: 7);
140
       await editor.tapButtonWithIcon(Icons.link);
140
       await editor.tapButtonWithIcon(Icons.link);
141
       await editor.tapButtonWithIcon(Icons.link_off);
141
       await editor.tapButtonWithIcon(Icons.link_off);
142
-      line = editor.document.root.children.first;
142
+      line = editor.document.root.children.first as LineNode;
143
       expect(line.childCount, 1);
143
       expect(line.childCount, 1);
144
     });
144
     });
145
   });
145
   });
160
 
160
 
161
     testWidgets('toggle overlay', (tester) async {
161
     testWidgets('toggle overlay', (tester) async {
162
       final editor = new EditorSandBox(tester: tester);
162
       final editor = new EditorSandBox(tester: tester);
163
-      await editor.tapEditor();
163
+      await editor.pumpAndTap();
164
       await editor.tapButtonWithIcon(Icons.photo);
164
       await editor.tapButtonWithIcon(Icons.photo);
165
 
165
 
166
       expect(find.byIcon(Icons.photo_camera), findsOneWidget);
166
       expect(find.byIcon(Icons.photo_camera), findsOneWidget);
170
 
170
 
171
     testWidgets('pick from camera', (tester) async {
171
     testWidgets('pick from camera', (tester) async {
172
       final editor = new EditorSandBox(tester: tester);
172
       final editor = new EditorSandBox(tester: tester);
173
-      await editor.tapEditor();
173
+      await editor.pumpAndTap();
174
       await editor.tapButtonWithIcon(Icons.photo);
174
       await editor.tapButtonWithIcon(Icons.photo);
175
       await editor.tapButtonWithIcon(Icons.photo_camera);
175
       await editor.tapButtonWithIcon(Icons.photo_camera);
176
       expect(log, hasLength(1));
176
       expect(log, hasLength(1));
189
 
189
 
190
     testWidgets('pick from gallery', (tester) async {
190
     testWidgets('pick from gallery', (tester) async {
191
       final editor = new EditorSandBox(tester: tester);
191
       final editor = new EditorSandBox(tester: tester);
192
-      await editor.tapEditor();
192
+      await editor.pumpAndTap();
193
       await editor.tapButtonWithIcon(Icons.photo);
193
       await editor.tapButtonWithIcon(Icons.photo);
194
       await editor.tapButtonWithIcon(Icons.photo_library);
194
       await editor.tapButtonWithIcon(Icons.photo_library);
195
       expect(log, hasLength(1));
195
       expect(log, hasLength(1));

+ 1
- 1
packages/zefyr/test/widgets/code_test.dart View File

11
   group('$ZefyrCode', () {
11
   group('$ZefyrCode', () {
12
     testWidgets('format as code', (tester) async {
12
     testWidgets('format as code', (tester) async {
13
       final editor = new EditorSandBox(tester: tester);
13
       final editor = new EditorSandBox(tester: tester);
14
-      await editor.tapEditor();
14
+      await editor.pumpAndTap();
15
       await editor.tapButtonWithIcon(Icons.code);
15
       await editor.tapButtonWithIcon(Icons.code);
16
 
16
 
17
       BlockNode block = editor.document.root.children.first;
17
       BlockNode block = editor.document.root.children.first;

+ 0
- 107
packages/zefyr/test/widgets/editable_text_scope_test.dart View File

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
-
5
-import 'package:flutter/material.dart';
6
-import 'package:flutter_test/flutter_test.dart';
7
-import 'package:zefyr/src/widgets/editable_box.dart';
8
-import 'package:zefyr/src/widgets/render_context.dart';
9
-import 'package:zefyr/zefyr.dart';
10
-
11
-void main() {
12
-  group('$ZefyrEditableTextScope', () {
13
-    setUp(() {
14
-      WidgetsFlutterBinding.ensureInitialized();
15
-    });
16
-
17
-    test('updateShouldNotify for rendering context changes', () {
18
-      var context = new ZefyrRenderContext();
19
-      var paragraph1 = createParagraph(context);
20
-      var paragraph2 = createParagraph(context);
21
-      context.addBox(paragraph1);
22
-      context.markDirty(paragraph1, false);
23
-      var widget1 = createScope(renderingContext: context);
24
-      var widget2 = createScope(renderingContext: context);
25
-
26
-      expect(widget2.updateShouldNotify(widget1), isFalse);
27
-      context.addBox(paragraph2);
28
-      context.markDirty(paragraph2, false);
29
-      widget2 = createScope(renderingContext: context);
30
-      expect(widget2.updateShouldNotify(widget1), isTrue);
31
-    });
32
-
33
-    test('updateShouldNotify for selection changes', () {
34
-      var context = new ZefyrRenderContext();
35
-      var selection = new TextSelection.collapsed(offset: 0);
36
-      var widget1 =
37
-          createScope(renderingContext: context, selection: selection);
38
-      var widget2 =
39
-          createScope(renderingContext: context, selection: selection);
40
-
41
-      expect(widget2.updateShouldNotify(widget1), isFalse);
42
-      selection = new TextSelection.collapsed(offset: 1);
43
-      widget2 = createScope(renderingContext: context, selection: selection);
44
-      expect(widget2.updateShouldNotify(widget1), isTrue);
45
-    });
46
-
47
-    test('updateShouldNotify for showCursor changes', () {
48
-      var context = new ZefyrRenderContext();
49
-      var showCursor = new ValueNotifier<bool>(true);
50
-      var widget1 =
51
-          createScope(renderingContext: context, showCursor: showCursor);
52
-      var widget2 =
53
-          createScope(renderingContext: context, showCursor: showCursor);
54
-
55
-      expect(widget2.updateShouldNotify(widget1), isFalse);
56
-      showCursor = new ValueNotifier<bool>(true);
57
-      widget2 = createScope(renderingContext: context, showCursor: showCursor);
58
-      expect(widget2.updateShouldNotify(widget1), isTrue);
59
-    });
60
-
61
-    test('updateShouldNotify for imageDelegate changes', () {
62
-      var context = new ZefyrRenderContext();
63
-      var delegate = new ZefyrDefaultImageDelegate();
64
-      var widget1 =
65
-          createScope(renderingContext: context, imageDelegate: delegate);
66
-      var widget2 =
67
-          createScope(renderingContext: context, imageDelegate: delegate);
68
-
69
-      expect(widget2.updateShouldNotify(widget1), isFalse);
70
-      delegate = new ZefyrDefaultImageDelegate();
71
-      widget2 = createScope(renderingContext: context, imageDelegate: delegate);
72
-      expect(widget2.updateShouldNotify(widget1), isTrue);
73
-    });
74
-  });
75
-}
76
-
77
-ZefyrEditableTextScope createScope({
78
-  @required ZefyrRenderContext renderingContext,
79
-  TextSelection selection,
80
-  ValueNotifier<bool> showCursor,
81
-  ZefyrImageDelegate imageDelegate,
82
-}) {
83
-  return ZefyrEditableTextScope(
84
-    renderContext: renderingContext,
85
-    selection: selection,
86
-    showCursor: showCursor,
87
-    imageDelegate: imageDelegate,
88
-    child: null,
89
-  );
90
-}
91
-
92
-RenderEditableProxyBox createParagraph(ZefyrRenderContext context) {
93
-  final doc = new NotusDocument();
94
-  doc.insert(0, 'This House Is A Circus');
95
-  final link = new LayerLink();
96
-  final showCursor = new ValueNotifier<bool>(true);
97
-  final selection = new TextSelection.collapsed(offset: 0);
98
-  final selectionColor = Colors.blue;
99
-  return new RenderEditableProxyBox(
100
-    node: doc.root.children.first,
101
-    layerLink: link,
102
-    renderContext: context,
103
-    showCursor: showCursor,
104
-    selection: selection,
105
-    selectionColor: selectionColor,
106
-  );
107
-}

+ 13
- 1
packages/zefyr/test/widgets/editable_text_test.dart View File

11
   group('$ZefyrEditableText', () {
11
   group('$ZefyrEditableText', () {
12
     testWidgets('user input', (tester) async {
12
     testWidgets('user input', (tester) async {
13
       final editor = new EditorSandBox(tester: tester);
13
       final editor = new EditorSandBox(tester: tester);
14
-      await editor.tapEditor();
14
+      await editor.pumpAndTap();
15
       final currentValue = editor.document.toPlainText();
15
       final currentValue = editor.document.toPlainText();
16
       await enterText(tester, 'Added $currentValue');
16
       await enterText(tester, 'Added $currentValue');
17
       expect(editor.document.toPlainText(), 'Added This House Is A Circus\n');
17
       expect(editor.document.toPlainText(), 'Added This House Is A Circus\n');
18
     });
18
     });
19
+
20
+    testWidgets('autofocus', (tester) async {
21
+      final editor = new EditorSandBox(tester: tester, autofocus: true);
22
+      await editor.pump();
23
+      expect(editor.focusNode.hasFocus, isTrue);
24
+    });
25
+
26
+    testWidgets('no autofocus', (tester) async {
27
+      final editor = new EditorSandBox(tester: tester);
28
+      await editor.pump();
29
+      expect(editor.focusNode.hasFocus, isFalse);
30
+    });
19
   });
31
   });
20
 }
32
 }
21
 
33
 

+ 4
- 4
packages/zefyr/test/widgets/editor_test.dart View File

22
       var theme = ZefyrThemeData(linkStyle: TextStyle(color: Colors.red));
22
       var theme = ZefyrThemeData(linkStyle: TextStyle(color: Colors.red));
23
       var editor =
23
       var editor =
24
           new EditorSandBox(tester: tester, document: doc, theme: theme);
24
           new EditorSandBox(tester: tester, document: doc, theme: theme);
25
-      await editor.tapEditor();
25
+      await editor.pumpAndTap();
26
       // TODO: figure out why this extra pump is needed here
26
       // TODO: figure out why this extra pump is needed here
27
       await tester.pumpAndSettle();
27
       await tester.pumpAndSettle();
28
-      EditableRichText p = tester.widget(find.byType(EditableRichText).first);
28
+      ZefyrRichText p = tester.widget(find.byType(ZefyrRichText).first);
29
       expect(p.text.children.first.style.color, Colors.red);
29
       expect(p.text.children.first.style.color, Colors.red);
30
     });
30
     });
31
 
31
 
32
     testWidgets('collapses selection when unfocused', (tester) async {
32
     testWidgets('collapses selection when unfocused', (tester) async {
33
       final editor = new EditorSandBox(tester: tester);
33
       final editor = new EditorSandBox(tester: tester);
34
-      await editor.tapEditor();
34
+      await editor.pumpAndTap();
35
       await editor.updateSelection(base: 0, extent: 3);
35
       await editor.updateSelection(base: 0, extent: 3);
36
       expect(editor.findSelectionHandle(), findsNWidgets(2));
36
       expect(editor.findSelectionHandle(), findsNWidgets(2));
37
       await editor.tapHideKeyboardButton();
37
       await editor.tapHideKeyboardButton();
41
 
41
 
42
     testWidgets('toggle enabled state', (tester) async {
42
     testWidgets('toggle enabled state', (tester) async {
43
       final editor = new EditorSandBox(tester: tester);
43
       final editor = new EditorSandBox(tester: tester);
44
-      await editor.tapEditor();
44
+      await editor.pumpAndTap();
45
       await editor.updateSelection(base: 0, extent: 3);
45
       await editor.updateSelection(base: 0, extent: 3);
46
       await editor.disable();
46
       await editor.disable();
47
       ZefyrEditor widget = tester.widget(find.byType(ZefyrEditor));
47
       ZefyrEditor widget = tester.widget(find.byType(ZefyrEditor));

+ 4
- 4
packages/zefyr/test/widgets/horizontal_rule_test.dart View File

11
   group('$ZefyrHorizontalRule', () {
11
   group('$ZefyrHorizontalRule', () {
12
     testWidgets('format as horizontal rule', (tester) async {
12
     testWidgets('format as horizontal rule', (tester) async {
13
       final editor = new EditorSandBox(tester: tester);
13
       final editor = new EditorSandBox(tester: tester);
14
-      await editor.tapEditor();
14
+      await editor.pumpAndTap();
15
       await editor.tapButtonWithIcon(Icons.remove);
15
       await editor.tapButtonWithIcon(Icons.remove);
16
 
16
 
17
       LineNode line = editor.document.root.children.last;
17
       LineNode line = editor.document.root.children.last;
21
     testWidgets('tap left side of horizontal rule puts caret before it',
21
     testWidgets('tap left side of horizontal rule puts caret before it',
22
         (tester) async {
22
         (tester) async {
23
       final editor = new EditorSandBox(tester: tester);
23
       final editor = new EditorSandBox(tester: tester);
24
-      await editor.tapEditor();
24
+      await editor.pumpAndTap();
25
       await editor.tapButtonWithIcon(Icons.remove);
25
       await editor.tapButtonWithIcon(Icons.remove);
26
       await editor.updateSelection(base: 0, extent: 0);
26
       await editor.updateSelection(base: 0, extent: 0);
27
 
27
 
35
     testWidgets('tap right side of horizontal rule puts caret after it',
35
     testWidgets('tap right side of horizontal rule puts caret after it',
36
         (tester) async {
36
         (tester) async {
37
       final editor = new EditorSandBox(tester: tester);
37
       final editor = new EditorSandBox(tester: tester);
38
-      await editor.tapEditor();
38
+      await editor.pumpAndTap();
39
       await editor.tapButtonWithIcon(Icons.remove);
39
       await editor.tapButtonWithIcon(Icons.remove);
40
       await editor.updateSelection(base: 0, extent: 0);
40
       await editor.updateSelection(base: 0, extent: 0);
41
 
41
 
51
     testWidgets('selects on long press',
51
     testWidgets('selects on long press',
52
         (tester) async {
52
         (tester) async {
53
       final editor = new EditorSandBox(tester: tester);
53
       final editor = new EditorSandBox(tester: tester);
54
-      await editor.tapEditor();
54
+      await editor.pumpAndTap();
55
       await editor.tapButtonWithIcon(Icons.remove);
55
       await editor.tapButtonWithIcon(Icons.remove);
56
       await editor.updateSelection(base: 0, extent: 0);
56
       await editor.updateSelection(base: 0, extent: 0);
57
 
57
 

+ 5
- 4
packages/zefyr/test/widgets/image_test.dart View File

47
 
47
 
48
     testWidgets('embed image', (tester) async {
48
     testWidgets('embed image', (tester) async {
49
       final editor = new EditorSandBox(tester: tester);
49
       final editor = new EditorSandBox(tester: tester);
50
-      await editor.tapEditor();
50
+      await editor.pumpAndTap();
51
       await editor.tapButtonWithIcon(Icons.photo);
51
       await editor.tapButtonWithIcon(Icons.photo);
52
       await editor.tapButtonWithIcon(Icons.photo_camera);
52
       await editor.tapButtonWithIcon(Icons.photo_camera);
53
       LineNode line = editor.document.root.children.last;
53
       LineNode line = editor.document.root.children.last;
63
     testWidgets('tap on left side of image puts caret before it',
63
     testWidgets('tap on left side of image puts caret before it',
64
         (tester) async {
64
         (tester) async {
65
       final editor = new EditorSandBox(tester: tester);
65
       final editor = new EditorSandBox(tester: tester);
66
-      await editor.tapEditor();
66
+      await editor.pumpAndTap();
67
       await editor.tapButtonWithIcon(Icons.photo);
67
       await editor.tapButtonWithIcon(Icons.photo);
68
       await editor.tapButtonWithIcon(Icons.photo_camera);
68
       await editor.tapButtonWithIcon(Icons.photo_camera);
69
       await editor.updateSelection(base: 0, extent: 0);
69
       await editor.updateSelection(base: 0, extent: 0);
78
     testWidgets('tap right side of image puts caret after it',
78
     testWidgets('tap right side of image puts caret after it',
79
         (tester) async {
79
         (tester) async {
80
       final editor = new EditorSandBox(tester: tester);
80
       final editor = new EditorSandBox(tester: tester);
81
-      await editor.tapEditor();
81
+      await editor.pumpAndTap();
82
       await editor.tapButtonWithIcon(Icons.photo);
82
       await editor.tapButtonWithIcon(Icons.photo);
83
       await editor.tapButtonWithIcon(Icons.photo_camera);
83
       await editor.tapButtonWithIcon(Icons.photo_camera);
84
       await editor.updateSelection(base: 0, extent: 0);
84
       await editor.updateSelection(base: 0, extent: 0);
94
 
94
 
95
     testWidgets('selects on long press', (tester) async {
95
     testWidgets('selects on long press', (tester) async {
96
       final editor = new EditorSandBox(tester: tester);
96
       final editor = new EditorSandBox(tester: tester);
97
-      await editor.tapEditor();
97
+      await editor.pumpAndTap();
98
       await editor.tapButtonWithIcon(Icons.photo);
98
       await editor.tapButtonWithIcon(Icons.photo);
99
       await editor.tapButtonWithIcon(Icons.photo_camera);
99
       await editor.tapButtonWithIcon(Icons.photo_camera);
100
       await editor.updateSelection(base: 0, extent: 0);
100
       await editor.updateSelection(base: 0, extent: 0);
105
       EmbedNode embed = line.children.single;
105
       EmbedNode embed = line.children.single;
106
       expect(editor.selection.baseOffset, embed.documentOffset);
106
       expect(editor.selection.baseOffset, embed.documentOffset);
107
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
107
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
108
+      expect(find.text('Paste'), findsOneWidget);
108
     });
109
     });
109
   });
110
   });
110
 }
111
 }

+ 1
- 1
packages/zefyr/test/widgets/list_test.dart View File

11
   group('$ZefyrList', () {
11
   group('$ZefyrList', () {
12
     testWidgets('format as list', (tester) async {
12
     testWidgets('format as list', (tester) async {
13
       final editor = new EditorSandBox(tester: tester);
13
       final editor = new EditorSandBox(tester: tester);
14
-      await editor.tapEditor();
14
+      await editor.pumpAndTap();
15
       await editor.tapButtonWithIcon(Icons.format_list_bulleted);
15
       await editor.tapButtonWithIcon(Icons.format_list_bulleted);
16
       BlockNode block = editor.document.root.children.first;
16
       BlockNode block = editor.document.root.children.first;
17
       expect(block.style.get(NotusAttribute.block),
17
       expect(block.style.get(NotusAttribute.block),

+ 1
- 1
packages/zefyr/test/widgets/quote_test.dart View File

11
   group('$ZefyrQuote', () {
11
   group('$ZefyrQuote', () {
12
     testWidgets('format as quote', (tester) async {
12
     testWidgets('format as quote', (tester) async {
13
       final editor = new EditorSandBox(tester: tester);
13
       final editor = new EditorSandBox(tester: tester);
14
-      await editor.tapEditor();
14
+      await editor.pumpAndTap();
15
       await editor.tapButtonWithIcon(Icons.format_quote);
15
       await editor.tapButtonWithIcon(Icons.format_quote);
16
 
16
 
17
       BlockNode block = editor.document.root.children.first;
17
       BlockNode block = editor.document.root.children.first;

+ 1
- 0
packages/zefyr/test/widgets/render_context_test.dart View File

85
     showCursor: showCursor,
85
     showCursor: showCursor,
86
     selection: selection,
86
     selection: selection,
87
     selectionColor: selectionColor,
87
     selectionColor: selectionColor,
88
+    cursorColor: Color(0),
88
   );
89
   );
89
 }
90
 }

+ 5
- 5
packages/zefyr/test/widgets/rich_text_test.dart View File

9
 import 'package:zefyr/zefyr.dart';
9
 import 'package:zefyr/zefyr.dart';
10
 
10
 
11
 void main() {
11
 void main() {
12
-  group('$EditableRichText', () {
12
+  group('$ZefyrRichText', () {
13
     final doc = new NotusDocument();
13
     final doc = new NotusDocument();
14
     doc.insert(0, 'This House Is A Circus');
14
     doc.insert(0, 'This House Is A Circus');
15
     final text = new TextSpan(text: 'This House Is A Circus');
15
     final text = new TextSpan(text: 'This House Is A Circus');
18
     setUp(() {
18
     setUp(() {
19
       widget = new Directionality(
19
       widget = new Directionality(
20
         textDirection: TextDirection.ltr,
20
         textDirection: TextDirection.ltr,
21
-        child: new EditableRichText(
22
-          node: doc.root.children.first,
21
+        child: new ZefyrRichText(
22
+          node: doc.root.children.first as LineNode,
23
           text: text,
23
           text: text,
24
         ),
24
         ),
25
       );
25
       );
27
 
27
 
28
     testWidgets('initialize', (tester) async {
28
     testWidgets('initialize', (tester) async {
29
       await tester.pumpWidget(widget);
29
       await tester.pumpWidget(widget);
30
-      EditableRichText result =
31
-          tester.firstWidget(find.byType(EditableRichText));
30
+      ZefyrRichText result =
31
+          tester.firstWidget(find.byType(ZefyrRichText));
32
       expect(result, isNotNull);
32
       expect(result, isNotNull);
33
       expect(result.text.text, 'This House Is A Circus');
33
       expect(result.text.text, 'This House Is A Circus');
34
     });
34
     });

+ 75
- 0
packages/zefyr/test/widgets/scope_test.dart View File

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
+
5
+import 'package:flutter/material.dart';
6
+import 'package:flutter_test/flutter_test.dart';
7
+import 'package:zefyr/zefyr.dart';
8
+
9
+void main() {
10
+  group('$ZefyrScope', () {
11
+    ZefyrScope scope;
12
+
13
+    setUp(() {
14
+      WidgetsFlutterBinding.ensureInitialized();
15
+      final doc = NotusDocument();
16
+      scope = ZefyrScope.editable(
17
+        controller: ZefyrController(doc),
18
+        imageDelegate: ZefyrDefaultImageDelegate(),
19
+        focusNode: FocusNode(),
20
+        focusScope: FocusScopeNode(),
21
+      );
22
+    });
23
+
24
+    test('it notifies on image delegate update', () {
25
+      bool notified = false;
26
+      scope.addListener(() {
27
+        notified = true;
28
+      });
29
+      final delegate = ZefyrDefaultImageDelegate();
30
+      scope.imageDelegate = delegate;
31
+      expect(notified, isTrue);
32
+      notified = false;
33
+      scope.imageDelegate = delegate;
34
+      expect(notified, isFalse);
35
+    });
36
+
37
+    test('it notifies on controller update', () {
38
+      bool notified = false;
39
+      scope.addListener(() {
40
+        notified = true;
41
+      });
42
+      final controller = ZefyrController(NotusDocument());
43
+      scope.controller = controller;
44
+      expect(notified, isTrue);
45
+      notified = false;
46
+      scope.controller = controller;
47
+      expect(notified, isFalse);
48
+    });
49
+
50
+    test('it notifies on focus node update', () {
51
+      bool notified = false;
52
+      scope.addListener(() {
53
+        notified = true;
54
+      });
55
+      final focusNode = FocusNode();
56
+      scope.focusNode = focusNode;
57
+      expect(notified, isTrue);
58
+      notified = false;
59
+      scope.focusNode = focusNode;
60
+      expect(notified, isFalse);
61
+    });
62
+
63
+    test('it notifies on selection changes but not text changes', () {
64
+      bool notified = false;
65
+      scope.addListener(() {
66
+        notified = true;
67
+      });
68
+
69
+      scope.controller.replaceText(0, 0, 'Hello');
70
+      expect(notified, isFalse);
71
+      scope.controller.updateSelection(TextSelection.collapsed(offset: 4));
72
+      expect(notified, isTrue);
73
+    });
74
+  });
75
+}

+ 8
- 8
packages/zefyr/test/widgets/selection_test.dart View File

12
   group('$ZefyrSelectionOverlay', () {
12
   group('$ZefyrSelectionOverlay', () {
13
     testWidgets('double tap caret shows toolbar', (tester) async {
13
     testWidgets('double tap caret shows toolbar', (tester) async {
14
       final editor = new EditorSandBox(tester: tester);
14
       final editor = new EditorSandBox(tester: tester);
15
-      await editor.tapEditor();
15
+      await editor.pumpAndTap();
16
 
16
 
17
-      RenderEditableParagraph renderObject =
18
-          tester.firstRenderObject(find.byType(EditableRichText));
17
+      RenderZefyrParagraph renderObject =
18
+          tester.firstRenderObject(find.byType(ZefyrRichText));
19
       var offset = renderObject.localToGlobal(Offset.zero);
19
       var offset = renderObject.localToGlobal(Offset.zero);
20
       offset += Offset(5.0, 5.0);
20
       offset += Offset(5.0, 5.0);
21
       await tester.tapAt(offset);
21
       await tester.tapAt(offset);
27
 
27
 
28
     testWidgets('hides when editor lost focus', (tester) async {
28
     testWidgets('hides when editor lost focus', (tester) async {
29
       final editor = new EditorSandBox(tester: tester);
29
       final editor = new EditorSandBox(tester: tester);
30
-      await editor.tapEditor();
30
+      await editor.pumpAndTap();
31
       await editor.updateSelection(base: 0, extent: 5);
31
       await editor.updateSelection(base: 0, extent: 5);
32
       expect(editor.findSelectionHandle(), findsNWidgets(2));
32
       expect(editor.findSelectionHandle(), findsNWidgets(2));
33
       await editor.unfocus();
33
       await editor.unfocus();
36
 
36
 
37
     testWidgets('tap on padding area finds closest paragraph', (tester) async {
37
     testWidgets('tap on padding area finds closest paragraph', (tester) async {
38
       final editor = new EditorSandBox(tester: tester);
38
       final editor = new EditorSandBox(tester: tester);
39
-      await editor.tapEditor();
39
+      await editor.pumpAndTap();
40
       editor.controller
40
       editor.controller
41
           .updateSelection(new TextSelection.collapsed(offset: 10));
41
           .updateSelection(new TextSelection.collapsed(offset: 10));
42
       await tester.pumpAndSettle();
42
       await tester.pumpAndSettle();
43
       expect(editor.controller.selection.extentOffset, 10);
43
       expect(editor.controller.selection.extentOffset, 10);
44
 
44
 
45
-      RenderEditableParagraph renderObject =
46
-          tester.firstRenderObject(find.byType(EditableRichText));
45
+      RenderZefyrParagraph renderObject =
46
+          tester.firstRenderObject(find.byType(ZefyrRichText));
47
       var offset = renderObject.localToGlobal(Offset.zero);
47
       var offset = renderObject.localToGlobal(Offset.zero);
48
       offset += Offset(-5.0, 5.0);
48
       offset += Offset(-5.0, 5.0);
49
       await tester.tapAt(offset);
49
       await tester.tapAt(offset);
54
 
54
 
55
     testWidgets('tap on empty space finds closest paragraph', (tester) async {
55
     testWidgets('tap on empty space finds closest paragraph', (tester) async {
56
       final editor = new EditorSandBox(tester: tester);
56
       final editor = new EditorSandBox(tester: tester);
57
-      await editor.tapEditor();
57
+      await editor.pumpAndTap();
58
       editor.controller.replaceText(10, 1, '\n',
58
       editor.controller.replaceText(10, 1, '\n',
59
           selection: new TextSelection.collapsed(offset: 0));
59
           selection: new TextSelection.collapsed(offset: 0));
60
       await tester.pumpAndSettle();
60
       await tester.pumpAndSettle();