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
+## 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
 ## 0.6.0
14
 ## 0.6.0
2
 
15
 
3
 * Updated to support Flutter 1.7.8
16
 * Updated to support Flutter 1.7.8

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

86
           child: ZefyrEditor(
86
           child: ZefyrEditor(
87
             controller: _controller,
87
             controller: _controller,
88
             focusNode: _focusNode,
88
             focusNode: _focusNode,
89
-            enabled: _editing,
89
+            mode: _editing ? ZefyrMode.edit : ZefyrMode.select,
90
             imageDelegate: new CustomImageDelegate(),
90
             imageDelegate: new CustomImageDelegate(),
91
           ),
91
           ),
92
         ),
92
         ),

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

98
     notifyListeners();
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
   void replaceText(int index, int length, String text,
108
   void replaceText(int index, int length, String text,
102
       {TextSelection selection}) {
109
       {TextSelection selection}) {
103
     Delta delta;
110
     Delta delta;

+ 47
- 17
packages/zefyr/lib/src/widgets/editable_text.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/cupertino.dart';
4
 import 'package:flutter/cupertino.dart';
5
+import 'package:flutter/material.dart';
5
 import 'package:flutter/widgets.dart';
6
 import 'package:flutter/widgets.dart';
6
 import 'package:notus/notus.dart';
7
 import 'package:notus/notus.dart';
7
 
8
 
13
 import 'image.dart';
14
 import 'image.dart';
14
 import 'input.dart';
15
 import 'input.dart';
15
 import 'list.dart';
16
 import 'list.dart';
17
+import 'mode.dart';
16
 import 'paragraph.dart';
18
 import 'paragraph.dart';
17
 import 'quote.dart';
19
 import 'quote.dart';
18
 import 'render_context.dart';
20
 import 'render_context.dart';
33
     @required this.controller,
35
     @required this.controller,
34
     @required this.focusNode,
36
     @required this.focusNode,
35
     @required this.imageDelegate,
37
     @required this.imageDelegate,
38
+    this.selectionControls,
36
     this.autofocus: true,
39
     this.autofocus: true,
37
-    this.enabled: true,
40
+    this.mode: ZefyrMode.edit,
38
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
41
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
39
     this.physics,
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
   final ZefyrController controller;
49
   final ZefyrController controller;
50
+
51
+  /// Controls whether this editor has keyboard focus.
43
   final FocusNode focusNode;
52
   final FocusNode focusNode;
44
   final ZefyrImageDelegate imageDelegate;
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
   final bool autofocus;
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
   final ScrollPhysics physics;
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
   /// Padding around editable area.
75
   /// Padding around editable area.
50
   final EdgeInsets padding;
76
   final EdgeInsets padding;
51
 
77
 
83
   }
109
   }
84
 
110
 
85
   void focusOrUnfocusIfNeeded() {
111
   void focusOrUnfocusIfNeeded() {
86
-    if (!_didAutoFocus && widget.autofocus && widget.enabled) {
112
+    if (!_didAutoFocus && widget.autofocus && widget.mode.canEdit) {
87
       FocusScope.of(context).autofocus(_focusNode);
113
       FocusScope.of(context).autofocus(_focusNode);
88
       _didAutoFocus = true;
114
       _didAutoFocus = true;
89
     }
115
     }
90
-    if (!widget.enabled && _focusNode.hasFocus) {
116
+    if (!widget.mode.canEdit && _focusNode.hasFocus) {
91
       _didAutoFocus = false;
117
       _didAutoFocus = false;
92
       _focusNode.unfocus();
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
   // Overridden members of State
131
   // Overridden members of State
98
   //
132
   //
104
 
138
 
105
     Widget body = ListBody(children: _buildChildren(context));
139
     Widget body = ListBody(children: _buildChildren(context));
106
     if (widget.padding != null) {
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
       physics: widget.physics,
145
       physics: widget.physics,
111
       controller: _scrollController,
146
       controller: _scrollController,
112
       child: body,
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
     return Stack(fit: StackFit.expand, children: layers);
155
     return Stack(fit: StackFit.expand, children: layers);
126
   }
156
   }
249
 
279
 
250
   // Triggered for both text and selection changes.
280
   // Triggered for both text and selection changes.
251
   void _handleLocalValueChange() {
281
   void _handleLocalValueChange() {
252
-    if (widget.enabled &&
282
+    if (widget.mode.canEdit &&
253
         widget.controller.lastChangeSource == ChangeSource.local) {
283
         widget.controller.lastChangeSource == ChangeSource.local) {
254
       // Only request keyboard for user actions.
284
       // Only request keyboard for user actions.
255
       requestKeyboard();
285
       requestKeyboard();

+ 40
- 4
packages/zefyr/lib/src/widgets/editor.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 'package:flutter/cupertino.dart';
5
+import 'package:flutter/material.dart';
4
 import 'package:flutter/widgets.dart';
6
 import 'package:flutter/widgets.dart';
5
 
7
 
6
 import 'controller.dart';
8
 import 'controller.dart';
7
 import 'editable_text.dart';
9
 import 'editable_text.dart';
8
 import 'image.dart';
10
 import 'image.dart';
11
+import 'mode.dart';
9
 import 'scaffold.dart';
12
 import 'scaffold.dart';
10
 import 'scope.dart';
13
 import 'scope.dart';
11
 import 'theme.dart';
14
 import 'theme.dart';
18
     @required this.controller,
21
     @required this.controller,
19
     @required this.focusNode,
22
     @required this.focusNode,
20
     this.autofocus: true,
23
     this.autofocus: true,
21
-    this.enabled: true,
24
+    this.mode: ZefyrMode.edit,
22
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
25
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
23
     this.toolbarDelegate,
26
     this.toolbarDelegate,
24
     this.imageDelegate,
27
     this.imageDelegate,
28
+    this.selectionControls,
25
     this.physics,
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
   final ZefyrController controller;
36
   final ZefyrController controller;
37
+
38
+  /// Controls whether this editor has keyboard focus.
29
   final FocusNode focusNode;
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
   final bool autofocus;
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
   final ZefyrToolbarDelegate toolbarDelegate;
54
   final ZefyrToolbarDelegate toolbarDelegate;
55
+
56
+  /// Delegate for resolving embedded images.
57
+  ///
58
+  /// This delegate is required if embedding images is allowed.
33
   final ZefyrImageDelegate imageDelegate;
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
   final ScrollPhysics physics;
67
   final ScrollPhysics physics;
35
 
68
 
36
   /// Padding around editable area.
69
   /// Padding around editable area.
94
   @override
127
   @override
95
   void didUpdateWidget(ZefyrEditor oldWidget) {
128
   void didUpdateWidget(ZefyrEditor oldWidget) {
96
     super.didUpdateWidget(oldWidget);
129
     super.didUpdateWidget(oldWidget);
130
+    _scope.mode = widget.mode;
97
     _scope.controller = widget.controller;
131
     _scope.controller = widget.controller;
98
     _scope.focusNode = widget.focusNode;
132
     _scope.focusNode = widget.focusNode;
99
     if (widget.imageDelegate != oldWidget.imageDelegate) {
133
     if (widget.imageDelegate != oldWidget.imageDelegate) {
113
 
147
 
114
     if (_scope == null) {
148
     if (_scope == null) {
115
       _scope = ZefyrScope.editable(
149
       _scope = ZefyrScope.editable(
150
+        mode: widget.mode,
116
         imageDelegate: _imageDelegate,
151
         imageDelegate: _imageDelegate,
117
         controller: widget.controller,
152
         controller: widget.controller,
118
         focusNode: widget.focusNode,
153
         focusNode: widget.focusNode,
147
       controller: _scope.controller,
182
       controller: _scope.controller,
148
       focusNode: _scope.focusNode,
183
       focusNode: _scope.focusNode,
149
       imageDelegate: _scope.imageDelegate,
184
       imageDelegate: _scope.imageDelegate,
185
+      selectionControls: widget.selectionControls,
150
       autofocus: widget.autofocus,
186
       autofocus: widget.autofocus,
151
-      enabled: widget.enabled,
187
+      mode: widget.mode,
152
       padding: widget.padding,
188
       padding: widget.padding,
153
       physics: widget.physics,
189
       physics: widget.physics,
154
     );
190
     );

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

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

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

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

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

6
 import 'cursor_timer.dart';
6
 import 'cursor_timer.dart';
7
 import 'editor.dart';
7
 import 'editor.dart';
8
 import 'image.dart';
8
 import 'image.dart';
9
+import 'mode.dart';
9
 import 'render_context.dart';
10
 import 'render_context.dart';
10
 import 'view.dart';
11
 import 'view.dart';
11
 
12
 
24
   /// Creates a view-only scope.
25
   /// Creates a view-only scope.
25
   ///
26
   ///
26
   /// Normally used in [ZefyrView].
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
         isEditable = false,
32
         isEditable = false,
30
         _imageDelegate = imageDelegate;
33
         _imageDelegate = imageDelegate;
31
 
34
 
33
   ///
36
   ///
34
   /// Normally used in [ZefyrEditor].
37
   /// Normally used in [ZefyrEditor].
35
   ZefyrScope.editable({
38
   ZefyrScope.editable({
39
+    @required ZefyrMode mode,
36
     @required ZefyrController controller,
40
     @required ZefyrController controller,
37
     @required ZefyrImageDelegate imageDelegate,
41
     @required ZefyrImageDelegate imageDelegate,
38
     @required FocusNode focusNode,
42
     @required FocusNode focusNode,
39
     @required FocusScopeNode focusScope,
43
     @required FocusScopeNode focusScope,
40
-  })  : assert(controller != null),
44
+  })  : assert(mode != null),
45
+        assert(controller != null),
41
         assert(imageDelegate != null),
46
         assert(imageDelegate != null),
42
         assert(focusNode != null),
47
         assert(focusNode != null),
43
         assert(focusScope != null),
48
         assert(focusScope != null),
44
         isEditable = true,
49
         isEditable = true,
50
+        _mode = mode,
45
         _controller = controller,
51
         _controller = controller,
46
         _imageDelegate = imageDelegate,
52
         _imageDelegate = imageDelegate,
47
         _focusNode = focusNode,
53
         _focusNode = focusNode,
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
   ZefyrController _controller;
89
   ZefyrController _controller;
74
   ZefyrController get controller => _controller;
90
   ZefyrController get controller => _controller;
75
   set controller(ZefyrController value) {
91
   set controller(ZefyrController value) {

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

25
 
25
 
26
 /// Selection overlay controls selection handles and other gestures.
26
 /// Selection overlay controls selection handles and other gestures.
27
 class ZefyrSelectionOverlay extends StatefulWidget {
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
   final TextSelectionControls controls;
31
   final TextSelectionControls controls;
37
-  final OverlayState overlay;
38
 
32
 
39
   @override
33
   @override
40
   _ZefyrSelectionOverlayState createState() =>
34
   _ZefyrSelectionOverlayState createState() =>
43
 
37
 
44
 class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
38
 class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
45
     implements TextSelectionDelegate {
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
   @override
86
   @override
47
   TextEditingValue get textEditingValue =>
87
   TextEditingValue get textEditingValue =>
48
-      widget.controller.plainTextEditingValue;
88
+      _scope.controller.plainTextEditingValue;
49
 
89
 
50
   set textEditingValue(TextEditingValue value) {
90
   set textEditingValue(TextEditingValue value) {
51
     final cursorPosition = value.selection.extentOffset;
91
     final cursorPosition = value.selection.extentOffset;
52
-    final oldText = widget.controller.document.toPlainText();
92
+    final oldText = _scope.controller.document.toPlainText();
53
     final newText = value.text;
93
     final newText = value.text;
54
     final diff = fastDiff(oldText, newText, cursorPosition);
94
     final diff = fastDiff(oldText, newText, cursorPosition);
55
-    widget.controller.replaceText(
95
+    _scope.controller.replaceText(
56
         diff.start, diff.deleted.length, diff.inserted,
96
         diff.start, diff.deleted.length, diff.inserted,
57
         selection: value.selection);
97
         selection: value.selection);
58
   }
98
   }
62
     // TODO: implement bringIntoView
102
     // TODO: implement bringIntoView
63
   }
103
   }
64
 
104
 
65
-  bool get isToolbarVisible => _toolbar != null;
66
-  bool get isToolbarHidden => _toolbar == null;
67
-
68
   @override
105
   @override
69
   void hideToolbar() {
106
   void hideToolbar() {
70
     _didCaretTap = false; // reset double tap.
107
     _didCaretTap = false; // reset double tap.
71
     _toolbar?.remove();
108
     _toolbar?.remove();
72
     _toolbar = null;
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
   @override
115
   @override
97
   void initState() {
116
   void initState() {
98
     super.initState();
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
   @override
121
   @override
106
   void didUpdateWidget(ZefyrSelectionOverlay oldWidget) {
122
   void didUpdateWidget(ZefyrSelectionOverlay oldWidget) {
107
     super.didUpdateWidget(oldWidget);
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
   @override
127
   @override
117
   void didChangeDependencies() {
128
   void didChangeDependencies() {
118
     super.didChangeDependencies();
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
   @override
156
   @override
130
   void dispose() {
157
   void dispose() {
131
-    _editor.removeListener(_handleChange);
158
+    _scope.removeListener(_handleChange);
132
     hideToolbar();
159
     hideToolbar();
133
     _toolbarController.dispose();
160
     _toolbarController.dispose();
134
     _toolbarController = null;
161
     _toolbarController = null;
148
         children: <Widget>[
175
         children: <Widget>[
149
           new SelectionHandleDriver(
176
           new SelectionHandleDriver(
150
             position: _SelectionHandlePosition.base,
177
             position: _SelectionHandlePosition.base,
151
-            controls: widget.controls,
178
+            selectionOverlay: this,
152
           ),
179
           ),
153
           new SelectionHandleDriver(
180
           new SelectionHandleDriver(
154
             position: _SelectionHandlePosition.extent,
181
             position: _SelectionHandlePosition.extent,
155
-            controls: widget.controls,
182
+            selectionOverlay: this,
156
           ),
183
           ),
157
         ],
184
         ],
158
       ),
185
       ),
164
   // Private members
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
   void _handleChange() {
194
   void _handleChange() {
183
-    if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) {
195
+    if (_selection != _scope.selection || _focusOwner != _scope.focusOwner) {
184
       _updateToolbar();
196
       _updateToolbar();
185
     }
197
     }
186
   }
198
   }
190
       return;
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
     setState(() {
207
     setState(() {
196
-      if (focusOwner != FocusOwner.editor) {
208
+      if (shouldHideControls && isToolbarVisible) {
197
         hideToolbar();
209
         hideToolbar();
198
       } else {
210
       } else {
199
         if (_selection != selection) {
211
         if (_selection != selection) {
200
-          if (selection.isCollapsed && isToolbarVisible) hideToolbar();
212
+          if (selection.isCollapsed && isToolbarVisible) {
213
+            hideToolbar();
214
+          }
201
           _toolbar?.markNeedsBuild();
215
           _toolbar?.markNeedsBuild();
202
           if (!selection.isCollapsed && isToolbarHidden) showToolbar();
216
           if (!selection.isCollapsed && isToolbarHidden) showToolbar();
203
         } else {
217
         } else {
232
 
246
 
233
     RenderEditableProxyBox box = _getEditableBox(result);
247
     RenderEditableProxyBox box = _getEditableBox(result);
234
     if (box == null) {
248
     if (box == null) {
235
-      box = _editor.renderContext.closestBoxForGlobalPoint(globalPoint);
249
+      box = _scope.renderContext.closestBoxForGlobalPoint(globalPoint);
236
     }
250
     }
237
     if (box == null) return null;
251
     if (box == null) return null;
238
 
252
 
252
     } else {
266
     } else {
253
       _didCaretTap = true;
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
   void _handleLongPress() {
272
   void _handleLongPress() {
271
       baseOffset: word.start,
285
       baseOffset: word.start,
272
       extentOffset: word.end,
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
   @override
291
   @override
279
-  bool get copyEnabled => _editor.isEditable;
292
+  bool get copyEnabled => _scope.mode.canSelect && !_selection.isCollapsed;
280
 
293
 
281
   @override
294
   @override
282
-  bool get cutEnabled => _editor.isEditable;
295
+  bool get cutEnabled => _scope.mode.canEdit && !_selection.isCollapsed;
283
 
296
 
284
   @override
297
   @override
285
-  bool get pasteEnabled => _editor.isEditable;
298
+  bool get pasteEnabled => _scope.mode.canEdit;
286
 
299
 
287
   @override
300
   @override
288
-  bool get selectAllEnabled => _editor.isEditable;
301
+  bool get selectAllEnabled => _scope.mode.canSelect;
289
 }
302
 }
290
 
303
 
291
 enum _SelectionHandlePosition { base, extent }
304
 enum _SelectionHandlePosition { base, extent }
294
   const SelectionHandleDriver({
307
   const SelectionHandleDriver({
295
     Key key,
308
     Key key,
296
     @required this.position,
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
   final _SelectionHandlePosition position;
314
   final _SelectionHandlePosition position;
301
-  final TextSelectionControls controls;
315
+  final _ZefyrSelectionOverlayState selectionOverlay;
302
 
316
 
303
   @override
317
   @override
304
   _SelectionHandleDriverState createState() =>
318
   _SelectionHandleDriverState createState() =>
361
 
375
 
362
   @override
376
   @override
363
   Widget build(BuildContext context) {
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
       return new Container();
379
       return new Container();
369
     }
380
     }
370
     final block = _scope.renderContext.boxForTextOffset(documentOffset);
381
     final block = _scope.renderContext.boxForTextOffset(documentOffset);
404
       point.dy.clamp(0.0, viewport.height),
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
       type,
420
       type,
409
       block.preferredLineHeight,
421
       block.preferredLineHeight,
410
     );
422
     );
411
-    final Size handleSize = widget.controls.getHandleSize(
423
+    final Size handleSize = widget.selectionOverlay.controls.getHandleSize(
412
       block.preferredLineHeight,
424
       block.preferredLineHeight,
413
     );
425
     );
414
     final Rect handleRect = Rect.fromLTWH(
426
     final Rect handleRect = Rect.fromLTWH(
451
               right: padding.right,
463
               right: padding.right,
452
               bottom: padding.bottom,
464
               bottom: padding.bottom,
453
             ),
465
             ),
454
-            child: widget.controls.buildHandle(
466
+            child: widget.selectionOverlay.controls.buildHandle(
455
               context,
467
               context,
456
               type,
468
               type,
457
               block.preferredLineHeight,
469
               block.preferredLineHeight,
525
 class _SelectionToolbar extends StatefulWidget {
537
 class _SelectionToolbar extends StatefulWidget {
526
   const _SelectionToolbar({
538
   const _SelectionToolbar({
527
     Key key,
539
     Key key,
528
-    @required this.scope,
529
-    @required this.controls,
530
-    @required this.delegate,
540
+    @required this.selectionOverlay,
531
   }) : super(key: key);
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
   @override
545
   @override
538
   _SelectionToolbarState createState() => new _SelectionToolbarState();
546
   _SelectionToolbarState createState() => new _SelectionToolbarState();
539
 }
547
 }
540
 
548
 
541
 class _SelectionToolbarState extends State<_SelectionToolbar> {
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
   @override
555
   @override
546
   Widget build(BuildContext context) {
556
   Widget build(BuildContext context) {
549
 
559
 
550
   Widget _buildToolbar(BuildContext context) {
560
   Widget _buildToolbar(BuildContext context) {
551
     final base = selection.baseOffset;
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
     if (block == null) {
563
     if (block == null) {
555
       return Container();
564
       return Container();
556
     }
565
     }
584
       block.localToGlobal(block.size.bottomRight(Offset.zero)),
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
     return new CompositedTransformFollower(
603
     return new CompositedTransformFollower(
590
       link: block.layerLink,
604
       link: block.layerLink,
591
       showWhenUnlinked: false,
605
       showWhenUnlinked: false,

+ 8
- 1
packages/zefyr/lib/src/widgets/view.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.
1
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
2
 import 'package:meta/meta.dart';
5
 import 'package:meta/meta.dart';
3
 import 'package:notus/notus.dart';
6
 import 'package:notus/notus.dart';
6
 import 'common.dart';
9
 import 'common.dart';
7
 import 'image.dart';
10
 import 'image.dart';
8
 import 'list.dart';
11
 import 'list.dart';
12
+import 'mode.dart';
9
 import 'paragraph.dart';
13
 import 'paragraph.dart';
10
 import 'quote.dart';
14
 import 'quote.dart';
11
 import 'scope.dart';
15
 import 'scope.dart';
33
   @override
37
   @override
34
   void initState() {
38
   void initState() {
35
     super.initState();
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
   @override
46
   @override

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

19
 export 'src/widgets/horizontal_rule.dart';
19
 export 'src/widgets/horizontal_rule.dart';
20
 export 'src/widgets/image.dart';
20
 export 'src/widgets/image.dart';
21
 export 'src/widgets/list.dart';
21
 export 'src/widgets/list.dart';
22
+export 'src/widgets/mode.dart';
22
 export 'src/widgets/paragraph.dart';
23
 export 'src/widgets/paragraph.dart';
23
 export 'src/widgets/quote.dart';
24
 export 'src/widgets/quote.dart';
24
 export 'src/widgets/scaffold.dart';
25
 export 'src/widgets/scaffold.dart';

+ 2
- 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.6.0
3
+version: 0.7.0
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
 
16
   quill_delta: ^1.0.0-dev.1.0
16
   quill_delta: ^1.0.0-dev.1.0
17
   notus: ^0.1.0
17
   notus: ^0.1.0
18
   meta: ^1.1.0
18
   meta: ^1.1.0
19
+  quiver_hashcode: ^2.0.0
19
 
20
 
20
 dev_dependencies:
21
 dev_dependencies:
21
   flutter_test:
22
   flutter_test:

+ 41
- 0
packages/zefyr/test/rendering/render_zefyr_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('$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
     return new ZefyrEditor(
134
     return new ZefyrEditor(
135
       controller: widget.controller,
135
       controller: widget.controller,
136
       focusNode: widget.focusNode,
136
       focusNode: widget.focusNode,
137
-      enabled: _enabled,
137
+      mode: _enabled ? ZefyrMode.edit : ZefyrMode.view,
138
       autofocus: widget.autofocus,
138
       autofocus: widget.autofocus,
139
     );
139
     );
140
   }
140
   }

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

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));
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
       expect(editor.selection.extentOffset, embed.documentOffset);
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
       final editor = new EditorSandBox(tester: tester);
79
       final editor = new EditorSandBox(tester: tester);
81
       await editor.pumpAndTap();
80
       await editor.pumpAndTap();
82
       await editor.tapButtonWithIcon(Icons.photo);
81
       await editor.tapButtonWithIcon(Icons.photo);
105
       EmbedNode embed = line.children.single;
104
       EmbedNode embed = line.children.single;
106
       expect(editor.selection.baseOffset, embed.documentOffset);
105
       expect(editor.selection.baseOffset, embed.documentOffset);
107
       expect(editor.selection.extentOffset, embed.documentOffset + 1);
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
       WidgetsFlutterBinding.ensureInitialized();
14
       WidgetsFlutterBinding.ensureInitialized();
15
       final doc = NotusDocument();
15
       final doc = NotusDocument();
16
       scope = ZefyrScope.editable(
16
       scope = ZefyrScope.editable(
17
+        mode: ZefyrMode.edit,
17
         controller: ZefyrController(doc),
18
         controller: ZefyrController(doc),
18
         imageDelegate: ZefyrDefaultImageDelegate(),
19
         imageDelegate: ZefyrDefaultImageDelegate(),
19
         focusNode: FocusNode(),
20
         focusNode: FocusNode(),

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

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