Browse Source

Toolbar refactor to decouple from editor context

Anatoly Pulyaevskiy 6 years ago
parent
commit
199df0fdcc

+ 1
- 0
packages/zefyr/.gitignore View File

@@ -13,3 +13,4 @@ ios/Flutter/Generated.xcconfig
13 13
 example/ios/.symlinks
14 14
 example/ios/Flutter/Generated.xcconfig
15 15
 doc/api/
16
+build/

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

@@ -65,8 +65,8 @@ class ZefyrButton extends StatelessWidget {
65 65
 
66 66
   @override
67 67
   Widget build(BuildContext context) {
68
-    final editor = ZefyrEditor.of(context);
69 68
     final toolbar = ZefyrToolbar.of(context);
69
+    final editor = toolbar.editor;
70 70
     final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
71 71
     final pressedHandler = _getPressedHandler(editor, toolbar);
72 72
     final iconColor = (pressedHandler == null)
@@ -236,7 +236,7 @@ class _HeadingButtonState extends State<HeadingButton> {
236 236
   }
237 237
 }
238 238
 
239
-/// Controls heading styles.
239
+/// Controls image attribute.
240 240
 ///
241 241
 /// When pressed, this button displays overlay toolbar with three
242 242
 /// buttons for each heading level.
@@ -278,14 +278,14 @@ class _ImageButtonState extends State<ImageButton> {
278 278
   }
279 279
 
280 280
   void _pickFromCamera() async {
281
-    final editor = ZefyrEditor.of(context);
281
+    final editor = ZefyrToolbar.of(context).editor;
282 282
     final image = await editor.imageDelegate.pickImage(ImageSource.camera);
283 283
     if (image != null)
284 284
       editor.formatSelection(NotusAttribute.embed.image(image));
285 285
   }
286 286
 
287 287
   void _pickFromGallery() async {
288
-    final editor = ZefyrEditor.of(context);
288
+    final editor = ZefyrToolbar.of(context).editor;
289 289
     final image = await editor.imageDelegate.pickImage(ImageSource.gallery);
290 290
     if (image != null)
291 291
       editor.formatSelection(NotusAttribute.embed.image(image));
@@ -307,8 +307,8 @@ class _LinkButtonState extends State<LinkButton> {
307 307
 
308 308
   @override
309 309
   Widget build(BuildContext context) {
310
-    final editor = ZefyrEditor.of(context);
311 310
     final toolbar = ZefyrToolbar.of(context);
311
+    final editor = toolbar.editor;
312 312
     final enabled =
313 313
         hasLink(editor.selectionStyle) || !editor.selection.isCollapsed;
314 314
 
@@ -322,7 +322,7 @@ class _LinkButtonState extends State<LinkButton> {
322 322
   bool hasLink(NotusStyle style) => style.contains(NotusAttribute.link);
323 323
 
324 324
   String getLink([String defaultValue]) {
325
-    final editor = ZefyrEditor.of(context);
325
+    final editor = ZefyrToolbar.of(context).editor;
326 326
     final attrs = editor.selectionStyle;
327 327
     if (hasLink(attrs)) {
328 328
       return attrs.value(NotusAttribute.link);
@@ -351,7 +351,6 @@ class _LinkButtonState extends State<LinkButton> {
351 351
   }
352 352
 
353 353
   void doneEdit() {
354
-    final editor = ZefyrEditor.of(context);
355 354
     final toolbar = ZefyrToolbar.of(context);
356 355
     setState(() {
357 356
       var error = false;
@@ -360,7 +359,7 @@ class _LinkButtonState extends State<LinkButton> {
360 359
           var uri = Uri.parse(_inputController.text);
361 360
           if ((uri.isScheme('https') || uri.isScheme('http')) &&
362 361
               uri.host.isNotEmpty) {
363
-            editor.formatSelection(
362
+            toolbar.editor.formatSelection(
364 363
                 NotusAttribute.link.fromString(_inputController.text));
365 364
           } else {
366 365
             error = true;
@@ -377,14 +376,14 @@ class _LinkButtonState extends State<LinkButton> {
377 376
         _inputController.text = '';
378 377
         _inputController.removeListener(_handleInputChange);
379 378
         toolbar.markNeedsRebuild();
380
-        editor.focus(context);
379
+        toolbar.editor.focus(context);
381 380
       }
382 381
     });
383 382
   }
384 383
 
385 384
   void cancelEdit() {
386 385
     if (mounted) {
387
-      final editor = ZefyrEditor.of(context);
386
+      final editor = ZefyrToolbar.of(context).editor;
388 387
       setState(() {
389 388
         _inputKey = null;
390 389
         _inputController.text = '';
@@ -395,7 +394,7 @@ class _LinkButtonState extends State<LinkButton> {
395 394
   }
396 395
 
397 396
   void unlink() {
398
-    final editor = ZefyrEditor.of(context);
397
+    final editor = ZefyrToolbar.of(context).editor;
399 398
     editor.formatSelection(NotusAttribute.link.unset);
400 399
     closeOverlay();
401 400
   }
@@ -407,7 +406,7 @@ class _LinkButtonState extends State<LinkButton> {
407 406
   }
408 407
 
409 408
   void openInBrowser() async {
410
-    final editor = ZefyrEditor.of(context);
409
+    final editor = ZefyrToolbar.of(context).editor;
411 410
     var link = getLink();
412 411
     assert(link != null);
413 412
     if (await canLaunch(link)) {
@@ -425,9 +424,8 @@ class _LinkButtonState extends State<LinkButton> {
425 424
   }
426 425
 
427 426
   Widget buildOverlay(BuildContext context) {
428
-    final editor = ZefyrEditor.of(context);
429 427
     final toolbar = ZefyrToolbar.of(context);
430
-    final style = editor.selectionStyle;
428
+    final style = toolbar.editor.selectionStyle;
431 429
 
432 430
     String value = 'Tap to edit link';
433 431
     if (style.contains(NotusAttribute.link)) {
@@ -439,7 +437,7 @@ class _LinkButtonState extends State<LinkButton> {
439 437
         : _LinkInput(
440 438
             key: _inputKey,
441 439
             controller: _inputController,
442
-            focusNode: editor.toolbarFocusNode,
440
+            focusNode: toolbar.editor.toolbarFocusNode,
443 441
             formatError: _formatError,
444 442
           );
445 443
     final items = <Widget>[Expanded(child: body)];

+ 154
- 120
packages/zefyr/lib/src/widgets/editor.dart View File

@@ -10,6 +10,143 @@ import 'image.dart';
10 10
 import 'theme.dart';
11 11
 import 'toolbar.dart';
12 12
 
13
+class ZefyrEditorScope extends ChangeNotifier {
14
+  ZefyrEditorScope({
15
+    @required ZefyrImageDelegate imageDelegate,
16
+    @required ZefyrController controller,
17
+    @required FocusNode focusNode,
18
+    @required FocusNode toolbarFocusNode,
19
+  })  : _controller = controller,
20
+        _imageDelegate = imageDelegate,
21
+        _focusNode = focusNode,
22
+        _toolbarFocusNode = toolbarFocusNode {
23
+    _selectionStyle = _controller.getSelectionStyle();
24
+    _selection = _controller.selection;
25
+    _controller.addListener(_handleControllerChange);
26
+    toolbarFocusNode.addListener(_handleFocusChange);
27
+    _focusNode.addListener(_handleFocusChange);
28
+  }
29
+
30
+  bool _disposed = false;
31
+
32
+  ZefyrImageDelegate _imageDelegate;
33
+  ZefyrImageDelegate get imageDelegate => _imageDelegate;
34
+
35
+  FocusNode _focusNode;
36
+  FocusNode _toolbarFocusNode;
37
+  FocusNode get toolbarFocusNode => _toolbarFocusNode;
38
+
39
+  ZefyrController _controller;
40
+  NotusStyle get selectionStyle => _selectionStyle;
41
+  NotusStyle _selectionStyle;
42
+  TextSelection get selection => _selection;
43
+  TextSelection _selection;
44
+
45
+  @override
46
+  void dispose() {
47
+    assert(!_disposed);
48
+    _controller.removeListener(_handleControllerChange);
49
+    _toolbarFocusNode.removeListener(_handleFocusChange);
50
+    _focusNode.removeListener(_handleFocusChange);
51
+    _disposed = true;
52
+    super.dispose();
53
+  }
54
+
55
+  void _updateControllerIfNeeded(ZefyrController value) {
56
+    if (_controller != value) {
57
+      _controller.removeListener(_handleControllerChange);
58
+      _controller = value;
59
+      _selectionStyle = _controller.getSelectionStyle();
60
+      _selection = _controller.selection;
61
+      _controller.addListener(_handleControllerChange);
62
+      notifyListeners();
63
+    }
64
+  }
65
+
66
+  void _updateFocusNodeIfNeeded(FocusNode value) {
67
+    if (_focusNode != value) {
68
+      _focusNode.removeListener(_handleFocusChange);
69
+      _focusNode = value;
70
+      _focusNode.addListener(_handleFocusChange);
71
+      notifyListeners();
72
+    }
73
+  }
74
+
75
+  void _updateImageDelegateIfNeeded(ZefyrImageDelegate value) {
76
+    if (_imageDelegate != value) {
77
+      _imageDelegate = value;
78
+      notifyListeners();
79
+    }
80
+  }
81
+
82
+  void _handleControllerChange() {
83
+    assert(!_disposed);
84
+    final attrs = _controller.getSelectionStyle();
85
+    final selection = _controller.selection;
86
+    if (_selectionStyle != attrs || _selection != selection) {
87
+      _selectionStyle = attrs;
88
+      _selection = _controller.selection;
89
+      notifyListeners();
90
+    }
91
+  }
92
+
93
+  void _handleFocusChange() {
94
+    assert(!_disposed);
95
+    if (focusOwner == FocusOwner.none && !_selection.isCollapsed) {
96
+      // Collapse selection if there is nothing focused.
97
+      _controller.updateSelection(_selection.copyWith(
98
+        baseOffset: _selection.extentOffset,
99
+        extentOffset: _selection.extentOffset,
100
+      ));
101
+    }
102
+    notifyListeners();
103
+  }
104
+
105
+  FocusOwner get focusOwner {
106
+    assert(!_disposed);
107
+    if (_focusNode.hasFocus) {
108
+      return FocusOwner.editor;
109
+    } else if (toolbarFocusNode.hasFocus) {
110
+      return FocusOwner.toolbar;
111
+    } else {
112
+      return FocusOwner.none;
113
+    }
114
+  }
115
+
116
+  void updateSelection(TextSelection value,
117
+      {ChangeSource source: ChangeSource.remote}) {
118
+    assert(!_disposed);
119
+    _controller.updateSelection(value, source: source);
120
+  }
121
+
122
+  void formatSelection(NotusAttribute value) {
123
+    assert(!_disposed);
124
+    _controller.formatSelection(value);
125
+  }
126
+
127
+  void focus(BuildContext context) {
128
+    assert(!_disposed);
129
+    FocusScope.of(context).requestFocus(_focusNode);
130
+  }
131
+
132
+  void hideKeyboard() {
133
+    assert(!_disposed);
134
+    _focusNode.unfocus();
135
+  }
136
+}
137
+
138
+class _ZefyrEditorScope extends InheritedWidget {
139
+  final ZefyrEditorScope scope;
140
+
141
+  _ZefyrEditorScope({Key key, Widget child, @required this.scope})
142
+      : super(key: key, child: child);
143
+
144
+  @override
145
+  bool updateShouldNotify(_ZefyrEditorScope oldWidget) {
146
+    return oldWidget.scope != scope;
147
+  }
148
+}
149
+
13 150
 /// Widget for editing Zefyr documents.
14 151
 class ZefyrEditor extends StatefulWidget {
15 152
   const ZefyrEditor({
@@ -34,119 +171,46 @@ class ZefyrEditor extends StatefulWidget {
34 171
   final EdgeInsets padding;
35 172
 
36 173
   static ZefyrEditorScope of(BuildContext context) {
37
-    ZefyrEditorScope scope =
38
-        context.inheritFromWidgetOfExactType(ZefyrEditorScope);
39
-    return scope;
174
+    _ZefyrEditorScope widget =
175
+        context.inheritFromWidgetOfExactType(_ZefyrEditorScope);
176
+    return widget.scope;
40 177
   }
41 178
 
42 179
   @override
43 180
   _ZefyrEditorState createState() => new _ZefyrEditorState();
44 181
 }
45 182
 
46
-/// Inherited widget which provides access to shared state of a Zefyr editor.
47
-class ZefyrEditorScope extends InheritedWidget {
48
-  /// Current selection style
49
-  final NotusStyle selectionStyle;
50
-  final TextSelection selection;
51
-  final FocusOwner focusOwner;
52
-  final FocusNode toolbarFocusNode;
53
-  final ZefyrImageDelegate imageDelegate;
54
-  final ZefyrController _controller;
55
-  final FocusNode _focusNode;
56
-
57
-  ZefyrEditorScope({
58
-    Key key,
59
-    @required Widget child,
60
-    @required this.selectionStyle,
61
-    @required this.selection,
62
-    @required this.focusOwner,
63
-    @required this.toolbarFocusNode,
64
-    @required this.imageDelegate,
65
-    @required ZefyrController controller,
66
-    @required FocusNode focusNode,
67
-  })  : _controller = controller,
68
-        _focusNode = focusNode,
69
-        super(key: key, child: child);
70
-
71
-  void updateSelection(TextSelection value,
72
-      {ChangeSource source: ChangeSource.remote}) {
73
-    _controller.updateSelection(value, source: source);
74
-  }
75
-
76
-  void formatSelection(NotusAttribute value) {
77
-    _controller.formatSelection(value);
78
-  }
79
-
80
-  void focus(BuildContext context) {
81
-    FocusScope.of(context).requestFocus(_focusNode);
82
-  }
83
-
84
-  void hideKeyboard() {
85
-    _focusNode.unfocus();
86
-  }
87
-
88
-  @override
89
-  bool updateShouldNotify(ZefyrEditorScope oldWidget) {
90
-    return (selectionStyle != oldWidget.selectionStyle ||
91
-        selection != oldWidget.selection ||
92
-        focusOwner != oldWidget.focusOwner ||
93
-        imageDelegate != oldWidget.imageDelegate);
94
-  }
95
-}
96
-
97 183
 class _ZefyrEditorState extends State<ZefyrEditor> {
98 184
   final FocusNode _toolbarFocusNode = new FocusNode();
99
-
100
-  NotusStyle _selectionStyle;
101
-  TextSelection _selection;
102
-  FocusOwner _focusOwner;
103 185
   ZefyrImageDelegate _imageDelegate;
104
-
105
-  FocusOwner getFocusOwner() {
106
-    if (widget.focusNode.hasFocus) {
107
-      return FocusOwner.editor;
108
-    } else if (_toolbarFocusNode.hasFocus) {
109
-      return FocusOwner.toolbar;
110
-    } else {
111
-      return FocusOwner.none;
112
-    }
113
-  }
186
+  ZefyrEditorScope _scope;
114 187
 
115 188
   @override
116 189
   void initState() {
117 190
     super.initState();
118
-    _selectionStyle = widget.controller.getSelectionStyle();
119
-    _selection = widget.controller.selection;
120
-    _focusOwner = getFocusOwner();
121 191
     _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
122
-    widget.controller.addListener(_handleControllerChange);
123
-    _toolbarFocusNode.addListener(_handleFocusChange);
124
-    widget.focusNode.addListener(_handleFocusChange);
192
+    _scope = ZefyrEditorScope(
193
+      toolbarFocusNode: _toolbarFocusNode,
194
+      imageDelegate: _imageDelegate,
195
+      controller: widget.controller,
196
+      focusNode: widget.focusNode,
197
+    );
125 198
   }
126 199
 
127 200
   @override
128 201
   void didUpdateWidget(ZefyrEditor oldWidget) {
129 202
     super.didUpdateWidget(oldWidget);
130
-    if (widget.focusNode != oldWidget.focusNode) {
131
-      oldWidget.focusNode.removeListener(_handleFocusChange);
132
-      widget.focusNode.addListener(_handleFocusChange);
133
-    }
134
-    if (widget.controller != oldWidget.controller) {
135
-      oldWidget.controller.removeListener(_handleControllerChange);
136
-      widget.controller.addListener(_handleControllerChange);
137
-      _selectionStyle = widget.controller.getSelectionStyle();
138
-      _selection = widget.controller.selection;
139
-    }
203
+    _scope._updateControllerIfNeeded(widget.controller);
204
+    _scope._updateFocusNodeIfNeeded(widget.focusNode);
140 205
     if (widget.imageDelegate != oldWidget.imageDelegate) {
141 206
       _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
207
+      _scope._updateImageDelegateIfNeeded(_imageDelegate);
142 208
     }
143 209
   }
144 210
 
145 211
   @override
146 212
   void dispose() {
147
-    widget.controller.removeListener(_handleControllerChange);
148
-    widget.focusNode.removeListener(_handleFocusChange);
149
-    _toolbarFocusNode.removeListener(_handleFocusChange);
213
+    _scope.dispose();
150 214
     _toolbarFocusNode.dispose();
151 215
     super.dispose();
152 216
   }
@@ -165,8 +229,8 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
165 229
     final children = <Widget>[];
166 230
     children.add(Expanded(child: editable));
167 231
     final toolbar = ZefyrToolbar(
232
+      editor: _scope,
168 233
       focusNode: _toolbarFocusNode,
169
-      controller: widget.controller,
170 234
       delegate: widget.toolbarDelegate,
171 235
     );
172 236
     children.add(toolbar);
@@ -179,40 +243,10 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
179 243
 
180 244
     return ZefyrTheme(
181 245
       data: actualTheme,
182
-      child: ZefyrEditorScope(
183
-        selection: _selection,
184
-        selectionStyle: _selectionStyle,
185
-        focusOwner: _focusOwner,
186
-        toolbarFocusNode: _toolbarFocusNode,
187
-        imageDelegate: _imageDelegate,
188
-        controller: widget.controller,
189
-        focusNode: widget.focusNode,
246
+      child: _ZefyrEditorScope(
247
+        scope: _scope,
190 248
         child: Column(children: children),
191 249
       ),
192 250
     );
193 251
   }
194
-
195
-  void _handleControllerChange() {
196
-    final attrs = widget.controller.getSelectionStyle();
197
-    final selection = widget.controller.selection;
198
-    if (_selectionStyle != attrs || _selection != selection) {
199
-      setState(() {
200
-        _selectionStyle = attrs;
201
-        _selection = widget.controller.selection;
202
-      });
203
-    }
204
-  }
205
-
206
-  void _handleFocusChange() {
207
-    setState(() {
208
-      _focusOwner = getFocusOwner();
209
-      if (_focusOwner == FocusOwner.none && !_selection.isCollapsed) {
210
-        // Collapse selection if there is nothing focused.
211
-        widget.controller.updateSelection(_selection.copyWith(
212
-          baseOffset: _selection.extentOffset,
213
-          extentOffset: _selection.extentOffset,
214
-        ));
215
-      }
216
-    });
217
-  }
218 252
 }

+ 13
- 0
packages/zefyr/lib/src/widgets/selection.dart View File

@@ -99,6 +99,8 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
99 99
     super.initState();
100 100
     _toolbarController = new AnimationController(
101 101
         duration: _kFadeDuration, vsync: widget.overlay);
102
+    _selection = widget.controller.selection;
103
+    widget.controller.addListener(_handleChange);
102 104
   }
103 105
 
104 106
   static const Duration _kFadeDuration = const Duration(milliseconds: 150);
@@ -112,6 +114,10 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
112 114
       _toolbarController = new AnimationController(
113 115
           duration: _kFadeDuration, vsync: widget.overlay);
114 116
     }
117
+    if (oldWidget.controller != widget.controller) {
118
+      oldWidget.controller.removeListener(_handleChange);
119
+      widget.controller.addListener(_handleChange);
120
+    }
115 121
   }
116 122
 
117 123
   @override
@@ -124,6 +130,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
124 130
 
125 131
   @override
126 132
   void dispose() {
133
+    widget.controller.removeListener(_handleChange);
127 134
     hideToolbar();
128 135
     _toolbarController.dispose();
129 136
     _toolbarController = null;
@@ -172,6 +179,12 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
172 179
 
173 180
   bool _didCaretTap = false;
174 181
 
182
+  void _handleChange() {
183
+    if (_selection != widget.controller.selection) {
184
+      _updateToolbar();
185
+    }
186
+  }
187
+
175 188
   void _updateToolbar() {
176 189
     if (!mounted) {
177 190
       return;

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

@@ -103,14 +103,14 @@ class ZefyrToolbar extends StatefulWidget implements PreferredSizeWidget {
103 103
   const ZefyrToolbar({
104 104
     Key key,
105 105
     @required this.focusNode,
106
-    @required this.controller,
106
+    @required this.editor,
107 107
     this.autoHide: true,
108 108
     this.delegate,
109 109
   }) : super(key: key);
110 110
 
111 111
   final FocusNode focusNode;
112
-  final ZefyrController controller;
113 112
   final ZefyrToolbarDelegate delegate;
113
+  final ZefyrEditorScope editor;
114 114
 
115 115
   /// Whether to automatically hide this toolbar when editor loses focus.
116 116
   final bool autoHide;
@@ -185,12 +185,23 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
185 185
 
186 186
   bool get hasOverlay => _overlayBuilder != null;
187 187
 
188
+  ZefyrEditorScope get editor => widget.editor;
189
+
190
+  void _handleChange() {
191
+    if (_selection != editor.selection) {
192
+      _selection = editor.selection;
193
+      closeOverlay();
194
+    }
195
+    setState(() {});
196
+  }
197
+
188 198
   @override
189 199
   void initState() {
190 200
     super.initState();
191 201
     _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
192 202
     _overlayAnimation = new AnimationController(
193 203
         vsync: this, duration: Duration(milliseconds: 100));
204
+    widget.editor.addListener(_handleChange);
194 205
   }
195 206
 
196 207
   @override
@@ -199,22 +210,20 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
199 210
     if (widget.delegate != oldWidget.delegate) {
200 211
       _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
201 212
     }
213
+    if (widget.editor != oldWidget.editor) {
214
+      oldWidget.editor.removeListener(_handleChange);
215
+      widget.editor.addListener(_handleChange);
216
+    }
202 217
   }
203 218
 
204 219
   @override
205
-  void didChangeDependencies() {
206
-    super.didChangeDependencies();
207
-    final editor = ZefyrEditor.of(context);
208
-    if (_selection != editor.selection) {
209
-      _selection = editor.selection;
210
-      closeOverlay();
211
-    }
220
+  void dispose() {
221
+    widget.editor.removeListener(_handleChange);
222
+    super.dispose();
212 223
   }
213 224
 
214 225
   @override
215 226
   Widget build(BuildContext context) {
216
-    final editor = ZefyrEditor.of(context);
217
-
218 227
     if (editor.focusOwner == FocusOwner.none) {
219 228
       return new Container();
220 229
     }

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

@@ -23,6 +23,8 @@ void main() {
23 23
       var editor =
24 24
           new EditorSandBox(tester: tester, document: doc, theme: theme);
25 25
       await editor.tapEditor();
26
+      // TODO: figure out why this extra pump is needed here
27
+      await tester.pumpAndSettle();
26 28
       EditableRichText p = tester.widget(find.byType(EditableRichText).first);
27 29
       expect(p.text.children.first.style.color, Colors.red);
28 30
     });