Browse Source

Added ZefyrMode to replace enabled flag; added selectionControls fields to ZefyrEditor

Anatoly Pulyaevskiy 5 years ago
parent
commit
bec1e53f2e

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

@@ -1,3 +1,16 @@
1
+## 0.7.0
2
+
3
+This release contains breaking changes.
4
+
5
+* Breaking change: `ZefyrEditor.enabled` field replaced by `ZefyrEditor.mode` which can take
6
+  one of three default values:
7
+    - `ZefyrMode.edit`: the same as `enabled: true`, all editing controls are available to the user
8
+    - `ZefyrMode.select`: user can't modify text itself, but allowed to select it and optionally
9
+       apply formatting.
10
+    - `ZefyrMode.view`: the same as `enabled: false`, read-only.
11
+* Added optional `selectionControls` field to `ZefyrEditor` and `ZefyrEditableText`. If not provided
12
+  then by default uses platform-specific implementation.
13
+
1 14
 ## 0.6.0
2 15
 
3 16
 * Updated to support Flutter 1.7.8

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

@@ -86,7 +86,7 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
86 86
           child: ZefyrEditor(
87 87
             controller: _controller,
88 88
             focusNode: _focusNode,
89
-            enabled: _editing,
89
+            mode: _editing ? ZefyrMode.edit : ZefyrMode.select,
90 90
             imageDelegate: new CustomImageDelegate(),
91 91
           ),
92 92
         ),

+ 7
- 0
packages/zefyr/lib/src/widgets/controller.dart View File

@@ -98,6 +98,13 @@ class ZefyrController extends ChangeNotifier {
98 98
     notifyListeners();
99 99
   }
100 100
 
101
+  /// Replaces [length] characters in the document starting at [index] with
102
+  /// provided [text].
103
+  ///
104
+  /// Resulting change is registered as produced by user action, e.g.
105
+  /// using [ChangeSource.local].
106
+  ///
107
+  /// Optionally updates selection if provided.
101 108
   void replaceText(int index, int length, String text,
102 109
       {TextSelection selection}) {
103 110
     Delta delta;

+ 47
- 17
packages/zefyr/lib/src/widgets/editable_text.dart View File

@@ -2,6 +2,7 @@
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4 4
 import 'package:flutter/cupertino.dart';
5
+import 'package:flutter/material.dart';
5 6
 import 'package:flutter/widgets.dart';
6 7
 import 'package:notus/notus.dart';
7 8
 
@@ -13,6 +14,7 @@ import 'editor.dart';
13 14
 import 'image.dart';
14 15
 import 'input.dart';
15 16
 import 'list.dart';
17
+import 'mode.dart';
16 18
 import 'paragraph.dart';
17 19
 import 'quote.dart';
18 20
 import 'render_context.dart';
@@ -33,19 +35,43 @@ class ZefyrEditableText extends StatefulWidget {
33 35
     @required this.controller,
34 36
     @required this.focusNode,
35 37
     @required this.imageDelegate,
38
+    this.selectionControls,
36 39
     this.autofocus: true,
37
-    this.enabled: true,
40
+    this.mode: ZefyrMode.edit,
38 41
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
39 42
     this.physics,
40
-  }) : super(key: key);
43
+  })  : assert(mode != null),
44
+        assert(controller != null),
45
+        assert(focusNode != null),
46
+        super(key: key);
41 47
 
48
+  /// Controls the document being edited.
42 49
   final ZefyrController controller;
50
+
51
+  /// Controls whether this editor has keyboard focus.
43 52
   final FocusNode focusNode;
44 53
   final ZefyrImageDelegate imageDelegate;
54
+
55
+  /// Whether this text field should focus itself if nothing else is already
56
+  /// focused.
57
+  ///
58
+  /// If true, the keyboard will open as soon as this text field obtains focus.
59
+  /// Otherwise, the keyboard is only shown after the user taps the text field.
60
+  ///
61
+  /// Defaults to true. Cannot be null.
45 62
   final bool autofocus;
46
-  final bool enabled;
63
+
64
+  /// Editing mode of this text field.
65
+  final ZefyrMode mode;
66
+
67
+  /// Controls physics of scrollable text field.
47 68
   final ScrollPhysics physics;
48 69
 
70
+  /// Optional delegate for building the text selection handles and toolbar.
71
+  ///
72
+  /// If not provided then platform-specific implementation is used by default.
73
+  final TextSelectionControls selectionControls;
74
+
49 75
   /// Padding around editable area.
50 76
   final EdgeInsets padding;
51 77
 
@@ -83,16 +109,24 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
83 109
   }
84 110
 
85 111
   void focusOrUnfocusIfNeeded() {
86
-    if (!_didAutoFocus && widget.autofocus && widget.enabled) {
112
+    if (!_didAutoFocus && widget.autofocus && widget.mode.canEdit) {
87 113
       FocusScope.of(context).autofocus(_focusNode);
88 114
       _didAutoFocus = true;
89 115
     }
90
-    if (!widget.enabled && _focusNode.hasFocus) {
116
+    if (!widget.mode.canEdit && _focusNode.hasFocus) {
91 117
       _didAutoFocus = false;
92 118
       _focusNode.unfocus();
93 119
     }
94 120
   }
95 121
 
122
+  TextSelectionControls defaultSelectionControls(BuildContext context) {
123
+    TargetPlatform platform = Theme.of(context).platform;
124
+    if (platform == TargetPlatform.iOS) {
125
+      return cupertinoTextSelectionControls;
126
+    }
127
+    return materialTextSelectionControls;
128
+  }
129
+
96 130
   //
97 131
   // Overridden members of State
98 132
   //
@@ -104,23 +138,19 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
104 138
 
105 139
     Widget body = ListBody(children: _buildChildren(context));
106 140
     if (widget.padding != null) {
107
-      body = new Padding(padding: widget.padding, child: body);
141
+      body = Padding(padding: widget.padding, child: body);
108 142
     }
109
-    final scrollable = SingleChildScrollView(
143
+
144
+    body = SingleChildScrollView(
110 145
       physics: widget.physics,
111 146
       controller: _scrollController,
112 147
       child: body,
113 148
     );
114 149
 
115
-    final overlay = Overlay.of(context, debugRequiredFor: widget);
116
-    final layers = <Widget>[scrollable];
117
-    if (widget.enabled) {
118
-      layers.add(ZefyrSelectionOverlay(
119
-        controller: widget.controller,
120
-        controls: cupertinoTextSelectionControls,
121
-        overlay: overlay,
122
-      ));
123
-    }
150
+    final layers = <Widget>[body];
151
+    layers.add(ZefyrSelectionOverlay(
152
+      controls: widget.selectionControls ?? defaultSelectionControls(context),
153
+    ));
124 154
 
125 155
     return Stack(fit: StackFit.expand, children: layers);
126 156
   }
@@ -249,7 +279,7 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
249 279
 
250 280
   // Triggered for both text and selection changes.
251 281
   void _handleLocalValueChange() {
252
-    if (widget.enabled &&
282
+    if (widget.mode.canEdit &&
253 283
         widget.controller.lastChangeSource == ChangeSource.local) {
254 284
       // Only request keyboard for user actions.
255 285
       requestKeyboard();

+ 40
- 4
packages/zefyr/lib/src/widgets/editor.dart View File

@@ -1,11 +1,14 @@
1 1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4
+import 'package:flutter/cupertino.dart';
5
+import 'package:flutter/material.dart';
4 6
 import 'package:flutter/widgets.dart';
5 7
 
6 8
 import 'controller.dart';
7 9
 import 'editable_text.dart';
8 10
 import 'image.dart';
11
+import 'mode.dart';
9 12
 import 'scaffold.dart';
10 13
 import 'scope.dart';
11 14
 import 'theme.dart';
@@ -18,19 +21,49 @@ class ZefyrEditor extends StatefulWidget {
18 21
     @required this.controller,
19 22
     @required this.focusNode,
20 23
     this.autofocus: true,
21
-    this.enabled: true,
24
+    this.mode: ZefyrMode.edit,
22 25
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
23 26
     this.toolbarDelegate,
24 27
     this.imageDelegate,
28
+    this.selectionControls,
25 29
     this.physics,
26
-  }) : super(key: key);
30
+  })  : assert(mode != null),
31
+        assert(controller != null),
32
+        assert(focusNode != null),
33
+        super(key: key);
27 34
 
35
+  /// Controls the document being edited.
28 36
   final ZefyrController controller;
37
+
38
+  /// Controls whether this editor has keyboard focus.
29 39
   final FocusNode focusNode;
40
+
41
+  /// Whether this editor should focus itself if nothing else is already
42
+  /// focused.
43
+  ///
44
+  /// If true, the keyboard will open as soon as this text field obtains focus.
45
+  /// Otherwise, the keyboard is only shown after the user taps the text field.
46
+  ///
47
+  /// Defaults to true. Cannot be null.
30 48
   final bool autofocus;
31
-  final bool enabled;
49
+
50
+  /// Editing mode of this editor.
51
+  final ZefyrMode mode;
52
+
53
+  /// Optional delegate for customizing this editor's toolbar.
32 54
   final ZefyrToolbarDelegate toolbarDelegate;
55
+
56
+  /// Delegate for resolving embedded images.
57
+  ///
58
+  /// This delegate is required if embedding images is allowed.
33 59
   final ZefyrImageDelegate imageDelegate;
60
+
61
+  /// Optional delegate for building the text selection handles and toolbar.
62
+  ///
63
+  /// If not provided then platform-specific implementation is used by default.
64
+  final TextSelectionControls selectionControls;
65
+
66
+  /// Controls physics of scrollable editor.
34 67
   final ScrollPhysics physics;
35 68
 
36 69
   /// Padding around editable area.
@@ -94,6 +127,7 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
94 127
   @override
95 128
   void didUpdateWidget(ZefyrEditor oldWidget) {
96 129
     super.didUpdateWidget(oldWidget);
130
+    _scope.mode = widget.mode;
97 131
     _scope.controller = widget.controller;
98 132
     _scope.focusNode = widget.focusNode;
99 133
     if (widget.imageDelegate != oldWidget.imageDelegate) {
@@ -113,6 +147,7 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
113 147
 
114 148
     if (_scope == null) {
115 149
       _scope = ZefyrScope.editable(
150
+        mode: widget.mode,
116 151
         imageDelegate: _imageDelegate,
117 152
         controller: widget.controller,
118 153
         focusNode: widget.focusNode,
@@ -147,8 +182,9 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
147 182
       controller: _scope.controller,
148 183
       focusNode: _scope.focusNode,
149 184
       imageDelegate: _scope.imageDelegate,
185
+      selectionControls: widget.selectionControls,
150 186
       autofocus: widget.autofocus,
151
-      enabled: widget.enabled,
187
+      mode: widget.mode,
152 188
       padding: widget.padding,
153 189
       physics: widget.physics,
154 190
     );

+ 5
- 4
packages/zefyr/lib/src/widgets/field.dart View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
3 3
 import 'controller.dart';
4 4
 import 'editor.dart';
5 5
 import 'image.dart';
6
+import 'mode.dart';
6 7
 import 'toolbar.dart';
7 8
 
8 9
 /// Zefyr editor with material design decorations.
@@ -15,7 +16,7 @@ class ZefyrField extends StatefulWidget {
15 16
   final ZefyrController controller;
16 17
   final FocusNode focusNode;
17 18
   final bool autofocus;
18
-  final bool enabled;
19
+  final ZefyrMode mode;
19 20
   final ZefyrToolbarDelegate toolbarDelegate;
20 21
   final ZefyrImageDelegate imageDelegate;
21 22
   final ScrollPhysics physics;
@@ -27,7 +28,7 @@ class ZefyrField extends StatefulWidget {
27 28
     this.controller,
28 29
     this.focusNode,
29 30
     this.autofocus: false,
30
-    this.enabled,
31
+    this.mode,
31 32
     this.toolbarDelegate,
32 33
     this.imageDelegate,
33 34
     this.physics,
@@ -45,7 +46,7 @@ class _ZefyrFieldState extends State<ZefyrField> {
45 46
       controller: widget.controller,
46 47
       focusNode: widget.focusNode,
47 48
       autofocus: widget.autofocus,
48
-      enabled: widget.enabled ?? true,
49
+      mode: widget.mode ?? ZefyrMode.edit,
49 50
       toolbarDelegate: widget.toolbarDelegate,
50 51
       imageDelegate: widget.imageDelegate,
51 52
       physics: widget.physics,
@@ -78,7 +79,7 @@ class _ZefyrFieldState extends State<ZefyrField> {
78 79
         (widget.decoration ?? const InputDecoration())
79 80
             .applyDefaults(Theme.of(context).inputDecorationTheme)
80 81
             .copyWith(
81
-              enabled: widget.enabled ?? true,
82
+              enabled: widget.mode == ZefyrMode.edit,
82 83
             );
83 84
 
84 85
     return effectiveDecoration;

+ 61
- 0
packages/zefyr/lib/src/widgets/mode.dart View File

@@ -0,0 +1,61 @@
1
+import 'package:meta/meta.dart';
2
+import 'package:quiver_hashcode/hashcode.dart';
3
+
4
+/// Controls level of interactions allowed by Zefyr editor.
5
+///
6
+// TODO: consider extending with following:
7
+//       - linkTapBehavior: none|launch
8
+//       - allowedStyles: ['bold', 'italic', 'image', ... ]
9
+class ZefyrMode {
10
+  /// Editing mode provides full access to all editing features: keyboard,
11
+  /// editor toolbar with formatting tools, selection controls and selection
12
+  /// toolbar with clipboard tools.
13
+  ///
14
+  /// Tapping on links in edit mode shows selection toolbar with contextual
15
+  /// actions instead of launching the link in a web browser.
16
+  static const edit =
17
+      ZefyrMode(canEdit: true, canSelect: true, canFormat: true);
18
+
19
+  /// Select-only mode allows users to select a range of text and have access
20
+  /// to selection toolbar including clipboard tools, any custom actions
21
+  /// registered by current text selection controls implementation.
22
+  ///
23
+  /// Tapping on links in select-only mode launches the link in a web browser.
24
+  static const select =
25
+      ZefyrMode(canEdit: false, canSelect: true, canFormat: true);
26
+
27
+  /// View-only mode disables almost all user interactions except the ability
28
+  /// to launch links in a web browser when tapped.
29
+  static const view =
30
+      ZefyrMode(canEdit: false, canSelect: false, canFormat: false);
31
+
32
+  /// Returns `true` if user is allowed to change text in a document.
33
+  final bool canEdit;
34
+
35
+  /// Returns `true` if user is allowed to select a range of text in a document.
36
+  final bool canSelect;
37
+
38
+  /// Returns `true` if user is allowed to change formatting styles in a
39
+  /// document.
40
+  final bool canFormat;
41
+
42
+  /// Creates new mode which describes allowed interactions in Zefyr editor.
43
+  const ZefyrMode({
44
+    @required this.canEdit,
45
+    @required this.canSelect,
46
+    @required this.canFormat,
47
+  });
48
+
49
+  @override
50
+  bool operator ==(Object other) {
51
+    if (identical(this, other)) return true;
52
+    if (other is! ZefyrMode) return false;
53
+    final ZefyrMode that = other;
54
+    return canEdit == that.canEdit &&
55
+        canSelect == that.canSelect &&
56
+        canFormat == that.canFormat;
57
+  }
58
+
59
+  @override
60
+  int get hashCode => hash3(canEdit, canSelect, canFormat);
61
+}

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

@@ -1,5 +1,8 @@
1 1
 import 'package:flutter/material.dart';
2 2
 
3
+import 'editor.dart';
4
+
5
+/// Provides necessary layout for [ZefyrEditor].
3 6
 class ZefyrScaffold extends StatefulWidget {
4 7
   final Widget child;
5 8
 

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

@@ -6,6 +6,7 @@ import 'controller.dart';
6 6
 import 'cursor_timer.dart';
7 7
 import 'editor.dart';
8 8
 import 'image.dart';
9
+import 'mode.dart';
9 10
 import 'render_context.dart';
10 11
 import 'view.dart';
11 12
 
@@ -24,8 +25,10 @@ class ZefyrScope extends ChangeNotifier {
24 25
   /// Creates a view-only scope.
25 26
   ///
26 27
   /// Normally used in [ZefyrView].
27
-  ZefyrScope.view({@required ZefyrImageDelegate imageDelegate})
28
-      : assert(imageDelegate != null),
28
+  ZefyrScope.view({
29
+    @required ZefyrMode mode,
30
+    @required ZefyrImageDelegate imageDelegate,
31
+  })  : assert(imageDelegate != null),
29 32
         isEditable = false,
30 33
         _imageDelegate = imageDelegate;
31 34
 
@@ -33,15 +36,18 @@ class ZefyrScope extends ChangeNotifier {
33 36
   ///
34 37
   /// Normally used in [ZefyrEditor].
35 38
   ZefyrScope.editable({
39
+    @required ZefyrMode mode,
36 40
     @required ZefyrController controller,
37 41
     @required ZefyrImageDelegate imageDelegate,
38 42
     @required FocusNode focusNode,
39 43
     @required FocusScopeNode focusScope,
40
-  })  : assert(controller != null),
44
+  })  : assert(mode != null),
45
+        assert(controller != null),
41 46
         assert(imageDelegate != null),
42 47
         assert(focusNode != null),
43 48
         assert(focusScope != null),
44 49
         isEditable = true,
50
+        _mode = mode,
45 51
         _controller = controller,
46 52
         _imageDelegate = imageDelegate,
47 53
         _focusNode = focusNode,
@@ -70,6 +76,16 @@ class ZefyrScope extends ChangeNotifier {
70 76
     }
71 77
   }
72 78
 
79
+  ZefyrMode _mode;
80
+  ZefyrMode get mode => _mode;
81
+  set mode(ZefyrMode value) {
82
+    assert(value != null);
83
+    if (_mode != value) {
84
+      _mode = value;
85
+      notifyListeners();
86
+    }
87
+  }
88
+
73 89
   ZefyrController _controller;
74 90
   ZefyrController get controller => _controller;
75 91
   set controller(ZefyrController value) {

+ 118
- 104
packages/zefyr/lib/src/widgets/selection.dart View File

@@ -25,16 +25,10 @@ RenderEditableBox _getEditableBox(HitTestResult result) {
25 25
 
26 26
 /// Selection overlay controls selection handles and other gestures.
27 27
 class ZefyrSelectionOverlay extends StatefulWidget {
28
-  const ZefyrSelectionOverlay({
29
-    Key key,
30
-    @required this.controller,
31
-    @required this.controls,
32
-    @required this.overlay,
33
-  }) : super(key: key);
28
+  const ZefyrSelectionOverlay({Key key, @required this.controls})
29
+      : super(key: key);
34 30
 
35
-  final ZefyrController controller;
36 31
   final TextSelectionControls controls;
37
-  final OverlayState overlay;
38 32
 
39 33
   @override
40 34
   _ZefyrSelectionOverlayState createState() =>
@@ -43,16 +37,62 @@ class ZefyrSelectionOverlay extends StatefulWidget {
43 37
 
44 38
 class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
45 39
     implements TextSelectionDelegate {
40
+  TextSelectionControls _controls;
41
+  TextSelectionControls get controls => _controls;
42
+
43
+  /// Global position of last TapDown event.
44
+  Offset _lastTapDownPosition;
45
+
46
+  /// Global position of last TapDown which is potentially a long press.
47
+  Offset _longPressPosition;
48
+
49
+  OverlayState _overlay;
50
+  OverlayEntry _toolbar;
51
+  AnimationController _toolbarController;
52
+
53
+  ZefyrScope _scope;
54
+  ZefyrScope get scope => _scope;
55
+  TextSelection _selection;
56
+  FocusOwner _focusOwner;
57
+
58
+  bool _didCaretTap = false;
59
+
60
+  /// Whether selection controls should be hidden.
61
+  bool get shouldHideControls {
62
+    if (!_scope.mode.canSelect) return true;
63
+    final selection = _scope.selection;
64
+    final isSelectionCollapsed = selection == null || selection.isCollapsed;
65
+    if (_scope.mode.canEdit) {
66
+      return isSelectionCollapsed || _scope.focusOwner != FocusOwner.editor;
67
+    }
68
+    return isSelectionCollapsed;
69
+  }
70
+
71
+  void showToolbar() {
72
+    final toolbarOpacity = _toolbarController.view;
73
+    _toolbar = OverlayEntry(
74
+      builder: (context) => FadeTransition(
75
+        opacity: toolbarOpacity,
76
+        child: _SelectionToolbar(selectionOverlay: this),
77
+      ),
78
+    );
79
+    _overlay.insert(_toolbar);
80
+    _toolbarController.forward(from: 0.0);
81
+  }
82
+
83
+  bool get isToolbarVisible => _toolbar != null;
84
+  bool get isToolbarHidden => _toolbar == null;
85
+
46 86
   @override
47 87
   TextEditingValue get textEditingValue =>
48
-      widget.controller.plainTextEditingValue;
88
+      _scope.controller.plainTextEditingValue;
49 89
 
50 90
   set textEditingValue(TextEditingValue value) {
51 91
     final cursorPosition = value.selection.extentOffset;
52
-    final oldText = widget.controller.document.toPlainText();
92
+    final oldText = _scope.controller.document.toPlainText();
53 93
     final newText = value.text;
54 94
     final diff = fastDiff(oldText, newText, cursorPosition);
55
-    widget.controller.replaceText(
95
+    _scope.controller.replaceText(
56 96
         diff.start, diff.deleted.length, diff.inserted,
57 97
         selection: value.selection);
58 98
   }
@@ -62,73 +102,60 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
62 102
     // TODO: implement bringIntoView
63 103
   }
64 104
 
65
-  bool get isToolbarVisible => _toolbar != null;
66
-  bool get isToolbarHidden => _toolbar == null;
67
-
68 105
   @override
69 106
   void hideToolbar() {
70 107
     _didCaretTap = false; // reset double tap.
71 108
     _toolbar?.remove();
72 109
     _toolbar = null;
73
-    _toolbarController.stop();
110
+    _toolbarController?.stop();
74 111
   }
75 112
 
76
-  void showToolbar() {
77
-    final toolbarOpacity = _toolbarController.view;
78
-    _toolbar = new OverlayEntry(
79
-      builder: (context) => new FadeTransition(
80
-        opacity: toolbarOpacity,
81
-        child: new _SelectionToolbar(
82
-          scope: _editor,
83
-          controls: widget.controls,
84
-          delegate: this,
85
-        ),
86
-      ),
87
-    );
88
-    widget.overlay.insert(_toolbar);
89
-    _toolbarController.forward(from: 0.0);
90
-  }
91
-
92
-  //
93
-  // Overridden members of State
94
-  //
113
+  static const Duration _kFadeDuration = const Duration(milliseconds: 150);
95 114
 
96 115
   @override
97 116
   void initState() {
98 117
     super.initState();
99
-    _toolbarController = new AnimationController(
100
-        duration: _kFadeDuration, vsync: widget.overlay);
118
+    _controls = widget.controls;
101 119
   }
102 120
 
103
-  static const Duration _kFadeDuration = const Duration(milliseconds: 150);
104
-
105 121
   @override
106 122
   void didUpdateWidget(ZefyrSelectionOverlay oldWidget) {
107 123
     super.didUpdateWidget(oldWidget);
108
-    if (oldWidget.overlay != widget.overlay) {
109
-      hideToolbar();
110
-      _toolbarController.dispose();
111
-      _toolbarController = new AnimationController(
112
-          duration: _kFadeDuration, vsync: widget.overlay);
113
-    }
124
+    _controls = widget.controls;
114 125
   }
115 126
 
116 127
   @override
117 128
   void didChangeDependencies() {
118 129
     super.didChangeDependencies();
119
-    final editor = ZefyrScope.of(context);
120
-    if (_editor != editor) {
121
-      _editor?.removeListener(_handleChange);
122
-      _editor = editor;
123
-      _editor.addListener(_handleChange);
124
-      _selection = _editor.selection;
125
-      _focusOwner = _editor.focusOwner;
130
+    final scope = ZefyrScope.of(context);
131
+    if (_scope != scope) {
132
+      _scope?.removeListener(_handleChange);
133
+      _scope = scope;
134
+      _scope.addListener(_handleChange);
135
+      _selection = _scope.selection;
136
+      _focusOwner = _scope.focusOwner;
137
+    }
138
+
139
+    final overlay = Overlay.of(context, debugRequiredFor: widget);
140
+    if (_overlay != overlay) {
141
+      hideToolbar();
142
+      _overlay = overlay;
143
+      _toolbarController?.dispose();
144
+      _toolbarController = null;
145
+    }
146
+    if (_toolbarController == null) {
147
+      _toolbarController = AnimationController(
148
+        duration: _kFadeDuration,
149
+        vsync: _overlay,
150
+      );
126 151
     }
152
+
153
+    _toolbar?.markNeedsBuild();
127 154
   }
128 155
 
129 156
   @override
130 157
   void dispose() {
131
-    _editor.removeListener(_handleChange);
158
+    _scope.removeListener(_handleChange);
132 159
     hideToolbar();
133 160
     _toolbarController.dispose();
134 161
     _toolbarController = null;
@@ -148,11 +175,11 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
148 175
         children: <Widget>[
149 176
           new SelectionHandleDriver(
150 177
             position: _SelectionHandlePosition.base,
151
-            controls: widget.controls,
178
+            selectionOverlay: this,
152 179
           ),
153 180
           new SelectionHandleDriver(
154 181
             position: _SelectionHandlePosition.extent,
155
-            controls: widget.controls,
182
+            selectionOverlay: this,
156 183
           ),
157 184
         ],
158 185
       ),
@@ -164,23 +191,8 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
164 191
   // Private members
165 192
   //
166 193
 
167
-  /// Global position of last TapDown event.
168
-  Offset _lastTapDownPosition;
169
-
170
-  /// Global position of last TapDown which is potentially a long press.
171
-  Offset _longPressPosition;
172
-
173
-  OverlayEntry _toolbar;
174
-  AnimationController _toolbarController;
175
-
176
-  ZefyrScope _editor;
177
-  TextSelection _selection;
178
-  FocusOwner _focusOwner;
179
-
180
-  bool _didCaretTap = false;
181
-
182 194
   void _handleChange() {
183
-    if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) {
195
+    if (_selection != _scope.selection || _focusOwner != _scope.focusOwner) {
184 196
       _updateToolbar();
185 197
     }
186 198
   }
@@ -190,14 +202,16 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
190 202
       return;
191 203
     }
192 204
 
193
-    final selection = _editor.selection;
194
-    final focusOwner = _editor.focusOwner;
205
+    final selection = _scope.selection;
206
+    final focusOwner = _scope.focusOwner;
195 207
     setState(() {
196
-      if (focusOwner != FocusOwner.editor) {
208
+      if (shouldHideControls && isToolbarVisible) {
197 209
         hideToolbar();
198 210
       } else {
199 211
         if (_selection != selection) {
200
-          if (selection.isCollapsed && isToolbarVisible) hideToolbar();
212
+          if (selection.isCollapsed && isToolbarVisible) {
213
+            hideToolbar();
214
+          }
201 215
           _toolbar?.markNeedsBuild();
202 216
           if (!selection.isCollapsed && isToolbarHidden) showToolbar();
203 217
         } else {
@@ -232,7 +246,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
232 246
 
233 247
     RenderEditableProxyBox box = _getEditableBox(result);
234 248
     if (box == null) {
235
-      box = _editor.renderContext.closestBoxForGlobalPoint(globalPoint);
249
+      box = _scope.renderContext.closestBoxForGlobalPoint(globalPoint);
236 250
     }
237 251
     if (box == null) return null;
238 252
 
@@ -252,7 +266,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
252 266
     } else {
253 267
       _didCaretTap = true;
254 268
     }
255
-    widget.controller.updateSelection(selection, source: ChangeSource.local);
269
+    _scope.controller.updateSelection(selection, source: ChangeSource.local);
256 270
   }
257 271
 
258 272
   void _handleLongPress() {
@@ -271,21 +285,20 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
271 285
       baseOffset: word.start,
272 286
       extentOffset: word.end,
273 287
     );
274
-    widget.controller.updateSelection(selection, source: ChangeSource.local);
288
+    _scope.controller.updateSelection(selection, source: ChangeSource.local);
275 289
   }
276 290
 
277
-  // TODO: these methods should also take into account enabled state.
278 291
   @override
279
-  bool get copyEnabled => _editor.isEditable;
292
+  bool get copyEnabled => _scope.mode.canSelect && !_selection.isCollapsed;
280 293
 
281 294
   @override
282
-  bool get cutEnabled => _editor.isEditable;
295
+  bool get cutEnabled => _scope.mode.canEdit && !_selection.isCollapsed;
283 296
 
284 297
   @override
285
-  bool get pasteEnabled => _editor.isEditable;
298
+  bool get pasteEnabled => _scope.mode.canEdit;
286 299
 
287 300
   @override
288
-  bool get selectAllEnabled => _editor.isEditable;
301
+  bool get selectAllEnabled => _scope.mode.canSelect;
289 302
 }
290 303
 
291 304
 enum _SelectionHandlePosition { base, extent }
@@ -294,11 +307,12 @@ class SelectionHandleDriver extends StatefulWidget {
294 307
   const SelectionHandleDriver({
295 308
     Key key,
296 309
     @required this.position,
297
-    @required this.controls,
298
-  }) : super(key: key);
310
+    @required this.selectionOverlay,
311
+  })  : assert(selectionOverlay != null),
312
+        super(key: key);
299 313
 
300 314
   final _SelectionHandlePosition position;
301
-  final TextSelectionControls controls;
315
+  final _ZefyrSelectionOverlayState selectionOverlay;
302 316
 
303 317
   @override
304 318
   _SelectionHandleDriverState createState() =>
@@ -361,10 +375,7 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
361 375
 
362 376
   @override
363 377
   Widget build(BuildContext context) {
364
-    if (selection == null ||
365
-        selection.isCollapsed ||
366
-        widget.controls == null ||
367
-        _scope.focusOwner != FocusOwner.editor) {
378
+    if (widget.selectionOverlay.shouldHideControls) {
368 379
       return new Container();
369 380
     }
370 381
     final block = _scope.renderContext.boxForTextOffset(documentOffset);
@@ -404,11 +415,12 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
404 415
       point.dy.clamp(0.0, viewport.height),
405 416
     );
406 417
 
407
-    final Offset handleAnchor = widget.controls.getHandleAnchor(
418
+    final Offset handleAnchor =
419
+        widget.selectionOverlay.controls.getHandleAnchor(
408 420
       type,
409 421
       block.preferredLineHeight,
410 422
     );
411
-    final Size handleSize = widget.controls.getHandleSize(
423
+    final Size handleSize = widget.selectionOverlay.controls.getHandleSize(
412 424
       block.preferredLineHeight,
413 425
     );
414 426
     final Rect handleRect = Rect.fromLTWH(
@@ -451,7 +463,7 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
451 463
               right: padding.right,
452 464
               bottom: padding.bottom,
453 465
             ),
454
-            child: widget.controls.buildHandle(
466
+            child: widget.selectionOverlay.controls.buildHandle(
455 467
               context,
456 468
               type,
457 469
               block.preferredLineHeight,
@@ -525,22 +537,20 @@ class _SelectionHandleDriverState extends State<SelectionHandleDriver>
525 537
 class _SelectionToolbar extends StatefulWidget {
526 538
   const _SelectionToolbar({
527 539
     Key key,
528
-    @required this.scope,
529
-    @required this.controls,
530
-    @required this.delegate,
540
+    @required this.selectionOverlay,
531 541
   }) : super(key: key);
532 542
 
533
-  final ZefyrScope scope;
534
-  final TextSelectionControls controls;
535
-  final TextSelectionDelegate delegate;
543
+  final _ZefyrSelectionOverlayState selectionOverlay;
536 544
 
537 545
   @override
538 546
   _SelectionToolbarState createState() => new _SelectionToolbarState();
539 547
 }
540 548
 
541 549
 class _SelectionToolbarState extends State<_SelectionToolbar> {
542
-  ZefyrScope get editable => widget.scope;
543
-  TextSelection get selection => widget.delegate.textEditingValue.selection;
550
+  TextSelectionControls get controls => widget.selectionOverlay.controls;
551
+  ZefyrScope get scope => widget.selectionOverlay.scope;
552
+  TextSelection get selection =>
553
+      widget.selectionOverlay.textEditingValue.selection;
544 554
 
545 555
   @override
546 556
   Widget build(BuildContext context) {
@@ -549,8 +559,7 @@ class _SelectionToolbarState extends State<_SelectionToolbar> {
549 559
 
550 560
   Widget _buildToolbar(BuildContext context) {
551 561
     final base = selection.baseOffset;
552
-    // TODO: Editable is not refreshed and may contain stale renderContext instance.
553
-    final block = editable.renderContext.boxForTextOffset(base);
562
+    final block = scope.renderContext.boxForTextOffset(base);
554 563
     if (block == null) {
555 564
       return Container();
556 565
     }
@@ -584,8 +593,13 @@ class _SelectionToolbarState extends State<_SelectionToolbar> {
584 593
       block.localToGlobal(block.size.bottomRight(Offset.zero)),
585 594
     );
586 595
 
587
-    final toolbar = widget.controls.buildToolbar(context, editingRegion,
588
-        block.preferredLineHeight, midpoint, endpoints, widget.delegate);
596
+    final toolbar = controls.buildToolbar(
597
+        context,
598
+        editingRegion,
599
+        block.preferredLineHeight,
600
+        midpoint,
601
+        endpoints,
602
+        widget.selectionOverlay);
589 603
     return new CompositedTransformFollower(
590 604
       link: block.layerLink,
591 605
       showWhenUnlinked: false,

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

@@ -1,3 +1,6 @@
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.
1 4
 import 'package:flutter/material.dart';
2 5
 import 'package:meta/meta.dart';
3 6
 import 'package:notus/notus.dart';
@@ -6,6 +9,7 @@ import 'code.dart';
6 9
 import 'common.dart';
7 10
 import 'image.dart';
8 11
 import 'list.dart';
12
+import 'mode.dart';
9 13
 import 'paragraph.dart';
10 14
 import 'quote.dart';
11 15
 import 'scope.dart';
@@ -33,7 +37,10 @@ class ZefyrViewState extends State<ZefyrView> {
33 37
   @override
34 38
   void initState() {
35 39
     super.initState();
36
-    _scope = ZefyrScope.view(imageDelegate: widget.imageDelegate);
40
+    _scope = ZefyrScope.view(
41
+      mode: ZefyrMode.view,
42
+      imageDelegate: widget.imageDelegate,
43
+    );
37 44
   }
38 45
 
39 46
   @override

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

@@ -19,6 +19,7 @@ export 'src/widgets/field.dart';
19 19
 export 'src/widgets/horizontal_rule.dart';
20 20
 export 'src/widgets/image.dart';
21 21
 export 'src/widgets/list.dart';
22
+export 'src/widgets/mode.dart';
22 23
 export 'src/widgets/paragraph.dart';
23 24
 export 'src/widgets/quote.dart';
24 25
 export 'src/widgets/scaffold.dart';

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

@@ -1,6 +1,6 @@
1 1
 name: zefyr
2 2
 description: Clean, minimalistic and collaboration-ready rich text editor for Flutter.
3
-version: 0.6.0
3
+version: 0.7.0
4 4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
5 5
 homepage: https://github.com/memspace/zefyr
6 6
 
@@ -16,6 +16,7 @@ dependencies:
16 16
   quill_delta: ^1.0.0-dev.1.0
17 17
   notus: ^0.1.0
18 18
   meta: ^1.1.0
19
+  quiver_hashcode: ^2.0.0
19 20
 
20 21
 dev_dependencies:
21 22
   flutter_test:

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

@@ -0,0 +1,41 @@
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('$RenderZefyrParagraph', () {
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
+    RenderZefyrParagraph p;
21
+
22
+    setUp(() {
23
+      WidgetsFlutterBinding.ensureInitialized();
24
+      renderContext = new ZefyrRenderContext();
25
+      p = new RenderZefyrParagraph(
26
+        text,
27
+        node: doc.root.children.first as LineNode,
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
+}

+ 1
- 1
packages/zefyr/test/testing.dart View File

@@ -134,7 +134,7 @@ class _ZefyrSandboxState extends State<_ZefyrSandbox> {
134 134
     return new ZefyrEditor(
135 135
       controller: widget.controller,
136 136
       focusNode: widget.focusNode,
137
-      enabled: _enabled,
137
+      mode: _enabled ? ZefyrMode.edit : ZefyrMode.view,
138 138
       autofocus: widget.autofocus,
139 139
     );
140 140
   }

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

@@ -45,7 +45,7 @@ void main() {
45 45
       await editor.updateSelection(base: 0, extent: 3);
46 46
       await editor.disable();
47 47
       ZefyrEditor widget = tester.widget(find.byType(ZefyrEditor));
48
-      expect(widget.enabled, isFalse);
48
+      expect(widget.mode, ZefyrMode.view);
49 49
     });
50 50
   });
51 51
 }

+ 2
- 3
packages/zefyr/test/widgets/image_test.dart View File

@@ -75,8 +75,7 @@ void main() {
75 75
       expect(editor.selection.extentOffset, embed.documentOffset);
76 76
     });
77 77
 
78
-    testWidgets('tap right side of image puts caret after it',
79
-        (tester) async {
78
+    testWidgets('tap right side of image puts caret after it', (tester) async {
80 79
       final editor = new EditorSandBox(tester: tester);
81 80
       await editor.pumpAndTap();
82 81
       await editor.tapButtonWithIcon(Icons.photo);
@@ -105,7 +104,7 @@ void main() {
105 104
       EmbedNode embed = line.children.single;
106 105
       expect(editor.selection.baseOffset, embed.documentOffset);
107 106
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
108
-      expect(find.text('Paste'), findsOneWidget);
107
+      expect(find.text('PASTE'), findsOneWidget);
109 108
     });
110 109
   });
111 110
 }

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

@@ -14,6 +14,7 @@ void main() {
14 14
       WidgetsFlutterBinding.ensureInitialized();
15 15
       final doc = NotusDocument();
16 16
       scope = ZefyrScope.editable(
17
+        mode: ZefyrMode.edit,
17 18
         controller: ZefyrController(doc),
18 19
         imageDelegate: ZefyrDefaultImageDelegate(),
19 20
         focusNode: FocusNode(),

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

@@ -22,7 +22,7 @@ void main() {
22 22
       await tester.pumpAndSettle();
23 23
       await tester.tapAt(offset);
24 24
       await tester.pumpAndSettle();
25
-      expect(find.text('Paste'), findsOneWidget);
25
+      expect(find.text('PASTE'), findsOneWidget);
26 26
     });
27 27
 
28 28
     testWidgets('hides when editor lost focus', (tester) async {